Copy LayerShader to a new Material
-
Hi,
I'm trying to copy a LayerShader to a new Material and am confused on how to do it.
To catch all possible cases this must be a recursive processing (LayerShaders inside LayerShaders).Would I just set the slot and use InsertShader with the already set up main LayerShader,
then iterate through all LayerShaderLayers and InsertShader these as well?
Or would I need to first clone all shaders with shader.GetClone() ?
If yes, would it be sufficient to clone the main LayerShader ?
And how about the special LayerShaderLayers like HSL Adjustment etc.Finally the inserted shaders need to be nested I guess? So if I first do material.InsertShader(xlayer),
is material.InsertShader(subshader, xlayer) the same as xlayer.InsertShader(subshader) ?best, index
-
Hello @indexofrefraction,
Thank you for reaching out to us. Copying a shader is pretty straight forward in the classic API of Cinema 4D, as shaders carry all their dependencies as children (see BaseList2D.GetFirstShader() for details on the data model of materials and shaders). As indicated by yourself, you can do this with
c4d.C4DAtom.GetClone()
. Find an example at the end of my posting. The 'recursive' quality you are looking for is already guaranteed by the hierarchical data model of shaders.is material.InsertShader(subshader, xlayer) the same as xlayer.InsertShader(subshader) ?
No, the former defines an insertion point, after the shader
xlayer
, the latter does not and defaults to inserting the new shader at the top of the shaderc4d.GeListHead
ofmaterial
. In practice, this should however make no difference, as shaders should not be identified by their index/position.Cheers,
FerdinandThe result:
The code:
"""Demonstrates how to copy a shader from one material to another. Run this example as a Script Manger script with a material selected. """ from typing import Optional import c4d doc: c4d.documents.BaseDocument # The active document op: Optional[c4d.BaseObject] # The active object, None if unselected def main() -> None: """ """ # Get the active material and the shader assigned to its color channel. selectedMaterial = doc.GetActiveMaterial() shader = selectedMaterial[c4d.MATERIAL_COLOR_SHADER] if not isinstance(shader, c4d.BaseShader): raise RuntimeError(f"The material {material} has no shader assigned to its color channel.") print (f"{shader =}") # Copy that shader, including its children. It is important to include the children, as shaders # will carry their dependences, other shaders their output relies on, as children. copiedShader = shader.GetClone(c4d.COPYFLAGS_0) # Create a new material and assign the copy there to the color channel. It is important to # insert the shader into the material for this to work. newMaterial = c4d.BaseList2D(c4d.Mmaterial) newMaterial.SetName(f"{selectedMaterial.GetName()} (Copy)") newMaterial.InsertShader(copiedShader) newMaterial[c4d.MATERIAL_COLOR_SHADER] = copiedShader # Insert the material into the active document and make it the active material. doc.InsertMaterial(newMaterial) doc.SetActiveMaterial(newMaterial) # Inform Cinema 4D about the changes the we made. c4d.EventAdd() if __name__ == '__main__': main()
-
hi ferdinand,
thanks for all of this ! but I have some follow up questions...a)
I guess the shaders inserted with InsertShader should be in the correct hierarchy ?
material.InsertShader(subshader, xlayer) --> inserts subshader after or as a child of xlayer? (should it be a child?)
xlayer.InsertShader(subshader) --> did you notice the xlayer here? material is not even used.
(i thought the latter might be used to insert subshader as child of xlayer)
In general: Can this hierarchy generated outside of the material and inserted as a whole at the end, or does this not work?b)
if you do this from your example :
newMaterial.InsertShader(copiedShader)
newMaterial[c4d.MATERIAL_COLOR_SHADER] = copiedShader
and copiedShader is a LayerShader with other shaders inside, maybe even other LayerShaders having more shaders
wouldnt these subshaders not needed to get inserted as well ?
or is this hierarchy already present with the given copiedShader ?in this example of creating a LayerShader the subshaders are inserted separately as well :
https://developers.maxon.net/forum/topic/13469/python-layershader-addlayer-defining-shaderin my tests i did it already without inserting the sub shaders but I end up with rogue shaders (and crashes)
best, index
-
material.InsertShader(subshader, xlayer) --> inserts subshader after or as a child of xlayer? (should it be a child?)
xlayer.InsertShader(subshader) --> did you notice the xlayer here? material is not even used.No, I did not notice that. I assume
xlayer
is meant to be layer shader in this example. As I hinted at in my previous posting, shaders are organized hierarchically belowBaseList2D
instances. TheseBaseList2D
can be anything, objects, tags, other shaders, and of course also materials. When you have a shader which has dependencies, it will carry these dependencies as children (of its shadersGeListHead
which can be accessed with the mentionedBaseList2D
methods).So, for example, a MoGraph shader effector that has a gradient shader in its shader slot, will look like this (slightly idealized, since the shaders are attached to a
GeListHead
in the branches of the node):BaseList2D 'Shader Effector' + - BaseList2D 'Gradient Shader'
For materials it is the same, when you have a material which has a gradient shader in the color channel, and a noise shader in the bump channel, it will look like this (again, idealized):
BaseList2D 'My Material' + - BaseList2D 'Gradient Shader' + - BaseList2D 'Noise Shader'
So, every
BaseList2D
carries its direct shader dependencies. This also means in consequence that shaders which rely on shaders will carry them in such fashion. If the color channel of the material would contain a layer shader which contains a gradient shader, the node tree would look like this (again, idealized):BaseList2D 'My Material' + - BaseList2D 'Layer Shader' + - BaseList2D 'Gradient Shader' + - BaseList2D 'Noise Shader'
So, your expression
xlayer.InsertShader(subshader)
is likely meant to depict someone inserting a shader dependency under a layer shader. You can also check yourself which shaders are attached to a node with these lines of code:node = myHostingNode.GetFirstShader() while node: print (node) node = node.GetNext()
wouldnt these subshaders not needed to get inserted as well ?
or is this hierarchy already present with the given copiedShader ?No, they would not, because of the data structures lined out above. My screen grab also showed such (double) nested layer shader example, and all shaders were copied correctly.
in my tests i did it already without inserting the sub shaders but ended up with rogue shaders (and crashes)
You either must have copied the shader without children (its own shader dependencies) or something else went wrong there. I just tried again my code from above with such nested layer shader example, and even after deleting the source material, assigning the copied material to some geometry, and rendering the scene, nothing is crashing.
If you are still running into crashes, I would recommend providing executable code, so that we can try to reproduce your problem.
Cheers,
Ferdinand -
concerning the post above ...
the following change inserts the gradient as child of the LayerShader
now printNodeTree gives the same output as for a manually set up LayerShadermat.InsertShader(xlayer) mat[c4d.MATERIAL_COLOR_SHADER] = xlayer ... gradient.InsertUnder(xlayer)
... it also seems this has solved my rogue shader / crash problem.
does InsertShader do something different / more than the normal GeListNode Insert methods ? -
Hello @indexofrefraction,
it is a bit tricky to answer your questions here. First of all, please follow our forum rules regarding creating new topics for new questions. The title of the posting was "Copy LayerShader to a new Material". Copying is different from creating such shader setup. One of your follow up questions was therefore out of scope for this topic. I have forked that question into How to Create and Populate a Layer Shader?.
Secondly, the topic you were referring to, with the code from @m_adam, contained incorrect code. See How to Create and Populate a Layer Shader? for more information on the subject, but in short, that code parented the shader contained in a layer shader to the wrong node.
does InsertShader do something different / more than the normal GeListNode Insert methods ?
Yes, it does. These are different branches. You seem to be unaware of the concept of branches. Shaders must be placed in the "Shaders" branch of a node, not in the normal hierarchical relations of the node (the layer shader is there an annoying exception, see the linked thread above).
Below you find a little script which lines out roughly the concept of branches, or in other words: How scene graphs are organized in the classic API of Cinema 4D.
Cheers,
Ferdinand"""Briefly explains parts of the scene graph model of the Cinema 4D classic API. Run this script with in the Script Manger, to have a more meaningful output, the scene should contain at least one object, with at least one tag, and at least one material with at least one shader. """ from typing import Optional import c4d doc: c4d.documents.BaseDocument # The active document op: Optional[c4d.BaseObject] # The active object, None if unselected TAB: str = " " # A two spaces tab def PrintRelatedNodes(node: c4d.GeListNode, indent: int=0): """Yields all descendants in all node relations of #node. Even in the classic API, scene elements are organized in a graph that contains more relations than just hierarchical relations, just as in any other 3D DCC app. There multiple ways one could look at this, e.g., as a poly-hierarchical taxonomy/tree, or as as an ontology/graph, but the reason is simple, when we have for example the object #parent, with the child object #child, and the tag #tag attached to #parent, we must be able to distinguish between the relation #is_child_of and #is_tag_of. E.g., we cannot express these relations as a hierarchy: parent + - tag + - child But instead must express it as a graph with dedicated relation types: parent / \ #is_child #is_tag / \ child tag Cinema 4D takes in the classic API more the view of a tree with a twist, which is realized with the type GeListHead. Nodes can have "direct" hierarchical relations, and indirect ones over branches. The example looks like this in the classic API of Cinema 4D: parent -------tags_branch-------> GeListHead Branch Tags + - child + - tag The relations of a node are tied together with GeListNode.GetBranchInfo() which returns the branches of a node, i.e., all relations that are not direct hierarchial ones, where each branch is represented by a GeListHead which then can contain hierarchial relations of arbitrary complexity and also branches for each node in that hierarchy. Almost everything is tied together in this manner in a classic API scene, documents, objects, tags, tracks, curves (but not keys), materials, shaders, and other things that a user is usually not aware of. This realizes in the classic API what is commonly referred to as a scene graph, a traversable graph that contains all elements in a scene. Note: This function was written for educational purposes, it could be optimized and also does not depict a classic API scene graph in its entirety, as it ignores the concept of caches. """ def RepresentNode(node: c4d.GeListNode) -> None: """Represents a node over its string representation and branches. """ # Print the node at the current indentation. print(f"{TAB * indent}{node}") # This node has no branches. if node.GetBranchInfo() == None: return # Iterate over the branches of the node. for item in node.GetBranchInfo(): # The GeListHead of this branch, it will contain all nodes as direct children which # are in a direct relation of type #name with #node. branchHead = item["head"] # The name of the branch, i.e., the type od relation, as for example "objects", "tags", # or "shaders". name = item["name"] # The type ID of the branch, for a tracks branch this will for example be c4d.CTrack. It # describes the id of the base type of the nodes to find below the list head. tid = item["id"] # This branch is empty, i.e., the branch has been created, but no relations have been # yet established. if branchHead.GetDown() == None: continue # Print the name of the branch and unpack print (f"{TAB * (indent + 1)}Branching into '{name}:'") PrintRelatedNodes(branchHead, indent + 2) if not isinstance(node, c4d.GeListNode): return None if indent == 0: print (f"{'-' * 100}\n") # Walk all direct hierarchial relations of #node. while node: RepresentNode(node) if node.GetDown(): print (f"{TAB * (indent + 1)}Direct Children:'") PrintRelatedNodes(node.GetDown(), indent + 2) node = node.GetNext() if indent == 0: print (f"{'-' * 100}\n") def main() -> None: """ """ # We can use #PrintRelatedNodes to walk the relations of many node types ... # A document ... PrintRelatedNodes(doc) # An object ... obj = doc.GetFirstObject() PrintRelatedNodes(obj) # A tag ... if obj: tag = obj.GetFirstTag() PrintRelatedNodes(tag) # A material ... material = doc.GetFirstMaterial() PrintRelatedNodes(material) # A shader ... if material: shader = material[c4d.MATERIAL_COLOR_SHADER] PrintRelatedNodes(shader) # and everything else that is a GeListNode (and therefore can have children and branches), as # for example tracks, curves, layers, etc. if __name__ == '__main__': main()
And here you can find an example output for this scene:
-
@ferdinand
Thanks a lot for taking the time!
The copy LayerShader problem solved plus good infos on how things work under the hood -
I have a follow up question.... and post it here because of the sample code above...
if that is not good, could you move it to a new thread?Isn't there a code example somewhere how to properly walk the shader tree?
i struggle with extending a "simple" walker function like the one belowalso if there are different node types wouldnt that need/allow an argument on which ones to follow ?
e.g. would it be possible to walk from doc.GetFirstObject() and just get the tags?
and finally is the walker below at least ok for objects?
(its not my code, just copied from some old library)def walker(op): if not op: return None if op.GetDown(): return op.GetDown() while op.GetUp() and not op.GetNext(): op = op.GetUp() return op.GetNext()
-
trying myself...
a walker as above is probably not possible
but this might be something handy for others :def nodesToList(op, li=None): ''' returns a list of a complete shader tree ''' if not li: li = [] if isinstance(op, c4d.GeListNode): while op: if not isinstance(op, c4d.GeListHead): li.append(op) if op.GetBranchInfo(): for branch in op.GetBranchInfo(): li = nodesToList(branch["head"].GetDown(), type, li) li = nodesToList(op.GetDown(), type, li) op = op.GetNext() return li # example for op in nodesToList(Mat): print(op)
-
Hey @indexofrefraction,
yeah this looks good, but it also depends a bit on what you want to do. I would recommend two things:
- Only branch into shaders (I think the branch-head is labeled 'shader') as there are a lot of branches and branching into everything will increase your runtime significantly.
- Consider using a generator/yield. Imagine you have a node graph which contains including its direct hierarchy and branches 10,000 nodes. But the node you are looking for, is actually already the second node in your list. And now you have run your
nodesToList
very often for some reason. Your variant would have to traverse all 10,000 nodes every time, a generator will only traverse what is required, only 2 nodes in our example.
For shader graphs both points are not so important, as there are usually less than 1000 nodes inside a material and there the impact is quite low. But if you want to traverse a whole document, it will make a difference.
Cheers,
FerdinandPS: I now just realized that this thread I was pointing too from the other thread was actually also by you. Mea culpa ^^
-
tx ferdinand,
and yes intertwined threads .-)