UNSOLVED Insert a shader into a shader hierarchy

Hi...

I know how to add shaders to materials or other shaders, set their parameters etc.
but i can't wrap my head around on how to insert a shader into an unknown tree.

explanation ..
lets take Mat[c4d.MATERIAL_COLOR_SHADER] with an unknown content.
now lets use a walker to process the complete shader hierarchy
and for any Layer Shader, create a Filter Shader in place, with the Layer Shader in the link.

creating the Filter Shader and adding the Layer Shader inside is easy,
but how can you add the Filter Shader to the (former) parent of the Layer Shader ?
Is there a possibility to know which slot the Layer Shader was in?
Eg. even if you'd type check and know the parent is a Fusion Shader,
this shader would have 3 possible slots (Blend, Mask and Channel)
and you'd not know which slot the Layer Shader has been in.
Is this possible somehow?

hope this is explained in an understandable way .-)

ok, I've come up with this and it kind of works...
in the GUI it looks correct, but the shader tree is broken.
only the filter shader is in the tree, the layer shader is missing
i'd be thankful for a hint what i'm missing here.

import c4d

'''
before running the script, 
create a material with a layer shader in the color slot
'''

def main():
	li = doc.GetActiveMaterials()
	if not li: li = doc.GetMaterials()
	mat = li[0]

	layerShaders = []
	sh = mat.GetFirstShader()
	while sh is not None:
		if sh.GetType() == c4d.Xlayer:
			layerShaders.append(sh)
		sh = walker(sh)
	
	for shLayer in layerShaders:
		parent = shLayer.GetUp()
		if parent is None:
			parent = mat
		# index = findIndex(parent, shLayer) # old
		index = findDescId(parent, shLayer) # new :)
		if index:
			shLayer = shLayer.GetClone()

			parent[index].Remove() # the original != the clone
			shFilter = c4d.BaseShader(c4d.Xfilter)
			shFilter.InsertShader(shLayer)
			shFilter[c4d.SLA_FILTER_TEXTURE] = shLayer

			parent.InsertShader(shFilter)
			parent[index] = shFilter

#def findIndex(parent, shader):
#	description = parent.GetDescription(c4d.DESCFLAGS_DESC_0)
#	for bc, paramid, groupid in description: 
#		if paramid:
#			index = paramid[0].id + 0
#			try:
#				if parent[index] == shader:
#					return index
#			except AttributeError:
#				pass
#	return None

def findDescId(parent, shader):
	description = parent.GetDescription(c4d.DESCFLAGS_DESC_NONE)
	for bc, descId, _ in description:
		if bc[c4d.DESC_SHADERLINKFLAG] == True:
			if parent[descId] == shader:
				return descId

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()

if __name__=='__main__':
	main()

Hello @indexofrefraction,

Thank you for reaching out to us. I am having a bit trouble understanding what you want to do, but from what I understood, you have a shader graph something like this:

MyMaterial M
   +- BranchHead 'Shaders'
      +- LayerShader L

and want to go to this:

MyMaterial M
   +- BranchHead 'Shaders'
      +- FilterShader F
         +- LayerShader L

while also replacing all base links in the Material M that formerly pointed to the layer shader L with a link to the filter shader F. And your actual question is then (which you did not explicitly ask):

How do I find out which parameters of a node point to another node?

Answer

First of all, I would point out this thread as we recently dealt there with shader hierarchies, which can be anything else than trivial. I will not deal with complex hierarchies here, but when your layer shader is nested inside some other shader (and not directly attached to a material), you will have to.

To find out which parameter types a node has, and by extension if the node has a certain value as one of its parameter values, one must traverse the description of the node. The description of a node defines among other things the structure of its parameters. For a material we can look for all shader links (or a specific value) by testing for DESC_SHADERLINKFLAG in the description data container. See the example at the end of this posting for details.

Cheers,
Ferdinand

The result for a material with a noise shader in its color channel:

Found a shader link parameter: (8000, 133, 5703).
	Its value <c4d.BaseShader object called Noise/Noise with ID 1011116 at 1636161331392> is a shader of the material.
Found a shader link parameter: (8001, 133, 5703).
Found a shader link parameter: (8002, 133, 5703).
Found a shader link parameter: (8003, 133, 5703).
Found a shader link parameter: (8004, 133, 5703).
Found a shader link parameter: (526376, 133, 5703).
Found a shader link parameter: (526377, 133, 5703).
Found a shader link parameter: (526378, 133, 5703).
Found a shader link parameter: (526381, 133, 5703).
Found a shader link parameter: (526394, 133, 5703).
Found a shader link parameter: (526403, 133, 5703).
Found a shader link parameter: (526419, 133, 5703).
Found a shader link parameter: (8005, 133, 5703).
Found a shader link parameter: (8006, 133, 5703).
Found a shader link parameter: (8012, 133, 5703).
Found a shader link parameter: (8007, 133, 5703).
Found a shader link parameter: (8008, 133, 5703).
Found a shader link parameter: (8009, 133, 5703).
Found a shader link parameter: (3225, 133, 5703).

The code:

"""Traverses the description of a material to find shader link parameters with a specific value.

Run this in the script manager with at least one material in the scene.
"""

import c4d

doc: c4d.documents.BaseDocument

def main() -> None:
    """
    """
    # Get the first material in the document.
    material = doc.GetFirstMaterial()

    # I am not going to deal with nested shaders here, and instead look at the simple case of all
    # shaders being attached to the material directly and us only wanting to inspect such shaders.

    # Get the shaders directly used by the material.
    currentShader, shaders = material.GetFirstShader(), []
    while currentShader:
        shaders.append(currentShader)
        currentShader = currentShader.GetNext()

    # Now we are going to traverse the description of the material. Iterating over a description
    # will yield a data container for the current parameter, its DescId, and a third parameter
    # which is here irrelevant.
    for bc, descId, _ in material.GetDescription(c4d.DESCFLAGS_DESC_NONE):
        # We are looking for shader links which is a special case of base links. If a base link
        # is a shader link, that information is stored in its description data container. The nice
        # side effect is that this field is only written for base link parameters, so we do not
        # have to check the parameter type anymore.
        if bc[c4d.DESC_SHADERLINKFLAG] == True:
            print (f"Found a shader link parameter: {descId}.")
            # Get the value of the parameter.
            value = material[descId]
            if value in shaders:
                print (f"\tIts value {value} is a shader of the material.")

if __name__ == '__main__':
    main()

hi ferdinand,

our posts probably crossed each other ...
thanks a lot, and yes you understood it right

could you maybe have a look at the code above ?
i replaced my findIndex() by a new findDescId() using your example
but inserting the Layer Shader into the Filter Shader still doesn't work correctly

best, index

Hey,

Well, your code looks mostly okay at first glance, but you know that we state in our forum guidelines that we cannot debug code for users. What jumps to the eye however is that your walker function only takes hierarchies into account. At least for the layer shader this will work fine, but to be more robust, I would also traverse the shader branch of each node. The subject has been dealt with in the topic I linked to in my first posting.

When your script is not working, please specify what is not working, and where your code fails.

Cheers,
Ferdinand

hm, actually i think the script does work correctly.

i was additionally checking if the shader tree was correct
and i was doing this using my own shader walker
and it failed, it stopps walking after the Filter Shader

if i use your PrintRelatedNodes() everything looks ok

i will post a follow up question in the mentioned thread :
https://plugincafe.maxon.net/topic/14056/copy-layershader-to-a-new-material/5

ok, i think i figured out my issue mentioned above ..
it would be nice if this could be confirmed:

materials and shaders can only have shader type nodes,
there must be a shader gelist head at the beginning and followed by normal nodes.
i guess this is was InsertShader() actually does.

now if you InsertShader again in the middle of a tree, you get a second shader gelist head
and as expected this the point is where my simple shader traversing failed.
but.. if you insert the filter shader manually in the material manager,
there is no second shader gelist head, just the one at the beginning.

so i guess that c4d accepts both ways,
but the proper way would be already to use InsertUnder if we're already in a shader type node list (?)
using InsertShader in the middle of a shader list is not necessary and bad practice (?)
a list with multiple shader gelist heads could (should?) be pruned of excessive heads (?)

Hey @indexofrefraction,

but the proper way would be already to use InsertUnder if we're already in a shader type node list (?) using InsertShader in the middle of a shader list is not necessary and bad practice (?) a list with multiple shader gelist heads could (should?) be pruned of excessive heads (?)

Well, the answer to that all is sort of yesn't. I personally would for example consider the layer shader carrying its shader dependencies as direct children to be at least a regression of the classic API scene architecture, as the proper place would be the shader branch of the layer shader.

The layer shader engineers might have (somewhat) rightfully thought that it does not really matter, or even had some technical reason to store the shaders as direct children. There is nothing special about branches, they are just contextualized hierarchies. For an object and its tags, they are necessary, because if we would also store tags as direct children of objects, it would become very cumbersome to sort out what is what when tags and child objects would be mixed. For shaders this is not the case, as there is nothing which is naturally a child of shaders as child objects are of objects. But in the end, the 'right' place to store shaders attached to a GeListNode is its shader branch. And other shader developers, both internal external, might have solved this differently and let their shaders store their shader dependencies in the 'Shader' branch of their shader. Which at end forces other developers to understand what each shader type does and imitate it when it must be handled in code.

There is no 'satisfying' answer, when a shader type stores its shader dependencies as direct children, you must use InsertUnder, and when it does store its shader dependencies in the shader branch, you must use InsertShader (which is just a shortcut for calling InsertUnder on the shader branch GeListHead of that node).

Cheers,
Ferdinand

hi Ferdinand,

i noticed that there is actually an issue with excessive gelist heads....
if you have this :

<c4d.BaseShader object called 'Filter/Filter' with ID 1011128 at 0x7fcebe74eeb0>
+ <c4d.GeListHead object at 0x7fcebe787a70> branch 'Shaders'
+ + <c4d.LayerShader object called 'Layer/Layer' with ID 1011123 at 0x7fcebe787ab0>
+ + + <c4d.BaseShader object called 'Noise/Noise' with ID 1011116 at 0x7fcebe787b90>

GetUp() on the LayerShader seems to return None,
which is a problem if - like in this case - you need to know if a shader is in the middle inside a shader tree,
or if it is at the top / a direct child of the material

this makes something like this necessary to properly "insert" a shader ....

if isinstance(target, c4d.BaseShader): 
    shader.InsertUnder(target)
else:
    target.InsertShader(shader)

Hey @indexofrefraction,

well, this is not surprising, as the LayerShader is not in a hierarchical relation with the GeListHead but a branch relation. You must call GetListHead on a GeListNode to retrieve its head, i.e., the branch which is containing the node.

Cheers,
Ferdinand

Hey @indexofrefraction,

well, this is not surprising, as the LayerShader is not in a hierarchical relation with the GeListHead but a branch relation. You must call GetListHead on a GeListNode to retrieve its head, i.e., the branch which is containing the node.

Cheers,
Ferdinand

Hi @indexofrefraction,

well that is normal, because your LayerShader has no parent, only a shader branch it is attached to which represented by a GeListHead. You must call GeListNode.GetListHead to retrieve the branch of a node.

Cheers,
Ferdinand