Traversing a layer shader with python
-
Hi guys,
Got a question regarding traversing a layer shader with Python.
For an internal plugin I need to go through shaders and localize/globalize the file paths in the bitmaps.
When I run into a layer shader I want to get into said shader and traverse all layers, checking if it's a bitmap and if so localize/globalize the path.
However, the SDK documentation is very sparse for the layer shader itself, specifically the "LayerShaderLayer.GetParameter(self, id)", it says to look at the C++ documentation for ID's, which I did.
For instance "For shaders (TypeShader): LAYER_S_PARAM_SHADER"
Neither:- 2
- LAYER_S_PARAM_SHADER
- [LAYER_S_PARAM_SHADER]
- [c4d.LAYER_S_PARAM_SHADER]
Give me any result.
2 comes back as "none" and the rest gives an attribute error.
0 also comes back as "none".I imagine that the GetParameter is used to get the file path of a layer if it's a bitmap, if not, what should I use?
The LayerShaderLayer only seems to have: GetNext, GetType, GetName, GetPreview, GetParameter, SetParameter.When I run a GetType on the layer, it gives me back "2" which is "TypeShader" and with GetName it returns the name of the Bitmap in that layer.
But how do I then get to the actual file path inside that layer to change it?Kind regards,
Joep
-
Hello @Peek,
Thank you for reaching out to us. Please share your code, as things otherwise tend to become one giant guessing game for us. In general, I would recommend having a look at BaseShader: Access and Structure, as the hierarchy and branching structure of shaders is a bit dicey. I would also recommend having a look at this thread as it lines out some details about the layer shader.
Other than that, I am not quite sure what you want to do with
LAYER_S_PARAM_SHADER
. It is just the stride for a group of parameters, namely these here:/// @addtogroup LAYER_S_PARAM_SHADER /// @ingroup group_containerid /// @{ /// Parameters for @ref TypeShader layers. #define LAYER_S_PARAM_SHADER_MODE 2000 ///< ::Int32 Blend mode: @enumerateEnum{BlendMode} #define LAYER_S_PARAM_SHADER_BLEND 2001 ///< ::Float Blend parameter. #define LAYER_S_PARAM_SHADER_LINK 2002 ///< @c void* Pointer to a BaseLink that contains the shader, read-only. /// @}
Unless you want to ignore the whole
c4d.LayerShaderLayer
interface, you will not have to deal with these strides, which divide the data container of the actualLayerShader
into bins. Without your code I can only guess what you are trying to do. Find an example below.Cheers,
FerdinandResult:
shader = <c4d.LayerShader object called Layer/Layer with ID 1011123 at 2670729460672> layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_MODE) = 2 layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_BLEND) = 1.0 layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_LINK) = <c4d.LayerShader object ... layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_MODE) = 3 layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_BLEND) = 0.75 layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_LINK) = <c4d.BaseShader object ... layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_MODE) = 1 layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_BLEND) = 0.5 layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_LINK) = <c4d.BaseShader object ... shader = <c4d.LayerShader object called Layer/Layer with ID 1011123 at 2670729454400> layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_MODE) = 2 layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_BLEND) = 0.5 layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_LINK) = <c4d.BaseShader object ... layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_MODE) = 1 layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_BLEND) = 1.0 layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_LINK) = <c4d.BaseShader object ... shader = <c4d.BaseShader object called Color/Color with ID 5832 at 2670659243072> shader = <c4d.BaseShader object called Fusion/Fusion with ID 1011109 at 2670659205440> shader = <c4d.BaseShader object called Noise/Noise with ID 1011116 at 2670659241152> shader = <c4d.BaseShader object called Noise/Noise with ID 1011116 at 2670659215552> shader = <c4d.BaseShader object called Gradient/Gradient with ID 1011100 at 2670729457088> shader = <c4d.BaseShader object called Noise/Noise with ID 1011116 at 2670729455424>
Code:
"""Demonstrates how to reach layer shader information. Run this script with at least one material in a scene, with a shader tree in its color channel. See also: https://developers.maxon.net/forum/topic/14059/ """ import c4d import typing doc: c4d.documents.BaseDocument # The active document def iter_shader( shader: c4d.BaseShader, level: int = 0) -> typing.Iterator[tuple[c4d.BaseShader, int]]: """Yields all shader, level tuples in the shader tree #shader. """ if not isinstance(shader, c4d.BaseShader): return yield (shader, level) # Because hierarchical relations between shaders are not standardized, we must traverse both # their hierarchical relations and the shader branching relation. for child in shader.GetChildren(): # hierarchy for item in iter_shader(child, level + 1): yield item for item in iter_shader(shader.GetFirstShader(), level + 1): # branching yield item def main() -> None: """Runs the example. """ # Get the color channel sahder for the first material in the scene. material: c4d.BaseMaterial | None = doc.GetFirstMaterial() if not material: raise RuntimeError("Please add at least one material to the document.") colorShader: c4d.BaseShader | None = material[c4d.MATERIAL_COLOR_SHADER] if not colorShader: raise RuntimeError("Please add a color shader to the first material.") # And traverse the whole tree. for shader, level in iter_shader(colorShader): print (f"{' ' * level}{shader = }") # When it is a layer shader, inspect its layers. if isinstance(shader, c4d.LayerShader): layer: c4d.LayerShaderLayer | None = shader.GetFirstLayer() if not layer: continue li: int = level + 1 while layer: print (f"{' ' * li}{layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_MODE) = }") print (f"{' ' * li}{layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_BLEND) = }") print (f"{' ' * li}{layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_LINK) = }") layer = layer.GetNext() if __name__ == '__main__': main()
-
Hi @ferdinand
First of all thank you very much for the extensive reply, I haven't had the time yet to put the code intro practice but I wanted to answer your question on what I wanted to do with my code first.
I haven't had much effective code since I can't get to the actual bitmap's filepath in the layer shader, but this is what I have, sorry for the ton of print statements but I need to figure out what the code was doing:
if (i.GetType() == 1011123): print ("Layershader found!") print("i inside layershader is ", i) layerShader = i layer = layerShader.GetFirstLayer() while layer.GetNext(): print("Layer is: ", layer) print("Layer name: ", layer.GetName(doc)) print("Layer type is: ", layer.GetType()) print("Layer parameter is: ", layer.GetParameter(0)) print("Next layer is: ", layer.GetNext()) nextLayer = layer.GetNext() print("Layer name: ", nextLayer.GetName(doc)) print("Layer type is: ", nextLayer.GetType()) print("Layer parameter is: ", nextLayer.GetParameter(0)) print("Next layer is: ", nextLayer.GetNext()) print("-----------------------------------------------------") if (layer.GetType() == 5833) or (layer.GetType() == 1036473): print("Bitmap found in layer!") file = os.path.split(shader(i)) print("file is: ", file[1]) if layer.GetNext(): layer = layer.GetNext() else: pass else: print("No bitmap found in layer!") if layer.GetNext(): layer = layer.GetNext() else: pass
What I want to do in code is this:
- Go through all shaders in the scene.
- Go into each shader and into each "slot" in each shader to see if a bitmap is present.
- If a bitmap is present, I either want to make the texture path local, or make the texture path global to our server location.
- If I run into a layer shader in a slot, I want to go into each layer of said layer shader to check if there is a bitmap loaded, and if so to change the texture path to local or global.
So far I've tackled the first 3 things but I'm running into issues with the 4th as I can't read out the texture path from a layer in the layer shader it seems.
-
Hello @Peek,
well, as my example and the C++ Manual I linked to above demonstrate, you do not really need the
LayerShaderLayer
interface for that. A layer shader carries its layered shaders as children. So, with theiter_shader
from above, you could do something like this:for shader in iter_shader(channelRoot): if shader.CheckType(c4d.Xlayer): for nestedShader in iter_shader(shader): if nestedShader.CheckType(c4d.Xbitmap): # This is a bitmap shader inside a layer shader, either as direct child or burried # much deeper. ...
Your code is lacking that recursive aspect, a layer shader can contain a layer shader, which can contain a layer shader, ...
But in general, it looks a bit like that you just want to use c4d.documents.GetAllAssetsNew anyway. It will yield all bitmap assets in a scene including their owners. It will also work for everything that supports
MSG_GETALLASSETS
and not just materials for the standard renderer. We talked about that method multiple times, for example here.Cheers,
Ferdinand -
@ferdinand : The GetAllAssetsNew is a far more elegant solution for the problem I am trying to solve and I've rewritten the code so that it works now. We use Corona render and Redshift so it's a bonus that this works with pretty much all materials.
Thank you very much for the help, I will put your information about the layer shader in my database for future reference as I'm sure it will become helpful in another project!
Kind regards,
Joep -
-
-