Hey @Fremox,
Would you think someone who doesn't know C4D API, has a strong background in Javascript, and has begun to learn Python (a bit) like me, could make it on his own, with a little bit of help from this forum, or is it too much work for a C4D scripting newbie?
Well, I like to say "when you are stubborn enough, you can implement anything" ;) I am sure you could wrap your head around our Python API, it is not really rocket science, especially when you have already JS programming experience. The question is more if you are willing to put in the time.
When you need help along your learning experience, the SDK Group is here to help. Since I thought this is a fun problem, I gave the problem a spin for a very simplistic implementation. Find my code at the end of my posting. It might help you when you decide to give the problem a spin yourself.
:exclamation: Please understand that we, the SDK group, cannot design applications for you. My code below is provided "as is" and you will have to add features and eliminate possible bugs yourself. |
Cheers,
Ferdinand
Result for two converted RS Materials, in view is a bit more complex RS material with 1:N
relations. The script expresses them as layer shaders. Apart from that, the script is as dense as a brick :)

Code:
"""Demonstrates a simple approach to writing a material mapper.
The general idea is here that we provide a #MAPPINGS dictionary below which expresses mappings
between the RS material space and its various material models and the singular standard renderer
material model. The mapper model is very simple, the only somewhat "advanced" thing it does is that
understands when multiple textures are connected to one port and expresses that as a layer shader.
Simply run the script with a document loaded which contains to be converted materials. I did not
complete the mapping in #MAPPINGS below, you must complete them yourself.
"""
import c4d
import typing
from c4d.modules.graphview import *
# Expresses mappings from RS material space to standard render material space.
MAPPINGS: dict = {
# The "RS Material" model. These keys are type names and you can discover them by uncommenting
# the line at the bottom of the script saying "Uncomment to ...".
"Material": {
# These are mapping from input port names on a "RS Material" to parameter IDs in a standard
# material. Added must also be the channel activation ID, e.g., when we map a diffuse color
# texture, we not only need to set MATERIAL_COLOR_SHADER to the texture, we must also enable
# MATERIAL_USE_COLOR in the first place.
# Port name : (param id, channelId)
"Diffuse Color": (c4d.MATERIAL_COLOR_SHADER, c4d.MATERIAL_USE_COLOR),
"Refl Color": (c4d.REFLECTION_LAYER_COLOR_TEXTURE, c4d.MATERIAL_USE_REFLECTION),
"Refl Roughness": (c4d.REFLECTION_LAYER_MAIN_SHADER_ROUGHNESS, c4d.MATERIAL_USE_REFLECTION)
# Add channel mappings to your liking ...
},
# The RS Standard Material model
"StandardMaterial": {
"Base Color": (c4d.MATERIAL_COLOR_SHADER, c4d.MATERIAL_USE_COLOR),
"Refl Color": (c4d.REFLECTION_LAYER_COLOR_TEXTURE, c4d.MATERIAL_USE_REFLECTION),
"Refl Roughness": (c4d.REFLECTION_LAYER_MAIN_SHADER_ROUGHNESS, c4d.MATERIAL_USE_REFLECTION),
"Bump Input": (c4d.MATERIAL_BUMP_SHADER, c4d.MATERIAL_USE_BUMP)
# Add channel mappings to your liking ...
}
# Define more material models ...
}
RS_TEXTURE_NODE_TYPENAME: str = "TextureSampler" # Type name of a RS Texture Sample node.
ID_NODE_TYPENAME: int = 1001 # The location at which type names are stored in an operator container.
def IterRsGraphGraphs(doc: c4d.documents.BaseDocument) -> typing.Iterator[tuple[str, GvNode]]:
"""Yields all RS Xpresso graph roots in #doc.
"""
# For all materials in #doc ...
for material in doc.GetMaterials():
# but step over everything that is not a legacy RS Xpresso material.
if not material.CheckType(c4d.Mrsgraph):
continue
# Traverse the branches of that node to find the Xpresso data in it. We could also use the
# Redshift API instead to get the graph.
for info in material.GetBranchInfo(c4d.GETBRANCHINFO_ONLYWITHCHILDREN):
if info.get("name", "").lower() != "node":
continue
head: c4d.GeListHead | None = info.get("head", None)
if not isinstance(head, c4d.GeListHead):
raise RuntimeError(f"Malformed RS graph without head: {info}")
# We found a graph which has at least a root element, yield the name of the material
# and the graph root.
if head.GetFirst():
yield material.GetName(), head.GetFirst()
def ConvertMaterial(root: GvNode) -> c4d.BaseMaterial:
"""Returns for the given RS shader graph #root a std renderer material representation.
"""
def iterTree(tree: GvNode) -> typing.Iterator[GvNode]:
"""Yields all nodes in the given shader tree #tree.
"""
yield tree
for child in tree.GetChildren():
for desc in iterTree(child):
yield desc
def getMaterialInputPorts(node: GvNode) -> list[tuple[int, int]]:
"""Traces the connections of #node to ports in a material model as expressed in MAPPINGS.
Will iterate over multiple connections until it terminates or finds a destination port. What
we are doing here is a bit clunky as we trace from the left to the right in the graph, from
a texture node to the in-port on a material it directly or indirectly connects to. Going the
other way around would be indeed easier (to read and write code for), but in Python we can
only traverse upstream and not downstream along connections. There is only a
GvPort.GetDestination but no .GetSource in Python.
"""
result: list[str] = []
# Iterate over all (connected) output ports of #node.
for outPort in node.GetOutPorts():
# Iterate over all the input ports #outPort is connected to.
for inPort in outPort.GetDestination():
# Get the node that hold the #inPort #outPort is connected to and its data.
host: GvNode = inPort.GetNode()
opData: c4d.BaseContainer = host.GetOperatorContainer()
# Iterate over our mappings.
for materialTypename, mappingData in MAPPINGS.items():
# #host is a material model node that we describe in #MAPPINGS.
if materialTypename == opData[ID_NODE_TYPENAME]:
# Iterate over the described ports and their channels.
for portName, (param, channel) in mappingData.items():
# #node connects to one of the input ports in a material model we
# want to track, and the mapped parameter ID and its channel to the#
# result.
if portName == inPort.GetName(inPort.GetNode()):
result.append((param, channel))
break
# Run the recursion so that we can reach deeper than one node, but only #host is
# not a terminal node, i.e., material model node.
if materialTypename != opData[ID_NODE_TYPENAME]:
result += getMaterialInputPorts(host)
return result
def addChannelTexture(material: c4d.Material, paramId: int, channelId: int, file: str) -> None:
"""Adds the given #file as a texture in #material at #paramId and #channelId.
"""
# Turn on the material channel and get the true parameter ID when it is something reflection
# channel related.
material[channelId] = True
if channelId == c4d.MATERIAL_USE_REFLECTION:
paramId += material.GetReflectionLayerIndex(0).GetDataID()
# Allocate the texture shader and set the texture file.
textureShader: c4d.BaseShader = c4d.BaseShader(c4d.Xbitmap)
if not textureShader:
raise MemoryError(f"{textureShader = }")
textureShader[c4d.BITMAPSHADER_FILENAME] = file
# Get the existing shader at the parameter ID we want to write.
channelShader: c4d.BaseShader | None = material[paramId]
# There is nothing, just put our #textureShader there.
if channelShader is None:
material.InsertShader(textureShader)
material[paramId] = textureShader
return
# There is something, but it is not yet a layer shader, wrap it in a layer shader.
elif not channelShader.CheckType(c4d.Xlayer):
layerShader: c4d.LayerShader = c4d.LayerShader(c4d.Xlayer)
if not layerShader:
raise MemoryError(f"{layerShader = }")
material.InsertShader(layerShader)
channelShader.Remove()
channelShader.InsertUnderLast(layerShader)
layer: c4d.LayerShaderLayer = layerShader.AddLayer(c4d.TypeShader)
layer.SetParameter(c4d.LAYER_S_PARAM_SHADER_LINK, channelShader)
material[paramId] = layerShader
channelShader = layerShader
# At this point #channelShader is always a layer shader, we add our texture as a layer.
textureShader.InsertUnderLast(channelShader)
layer: c4d.LayerShaderLayer = channelShader.AddLayer(c4d.TypeShader)
layer.SetParameter(c4d.LAYER_S_PARAM_SHADER_LINK, textureShader)
# ----------------------------------------------------------------------------------------------
# Allocate a material and start iterating over every node in #root.
material: c4d.Material = c4d.BaseMaterial(c4d.Mmaterial)
if not material:
raise MemoryError(f"{material = }")
for node in iterTree(root):
# Get the data of container of #node which among other things contains its real type name.
opData: c4d.BaseContainer = node.GetOperatorContainer()
# Uncomment to get a print of all node type names in the the graph.
# print(node, opData[ID_NODE_TYPENAME])
if RS_TEXTURE_NODE_TYPENAME != opData[ID_NODE_TYPENAME]:
continue
# Get the file linked in the texture node and start finding target ports in material model
# nodes we want to convert to.
file: str = node[c4d.REDSHIFT_SHADER_TEXTURESAMPLER_TEX0, c4d.REDSHIFT_FILE_PATH]
for paramId, channelId in getMaterialInputPorts(node):
addChannelTexture(material, paramId, channelId, file)
return material
doc: c4d.documents.BaseDocument # The active document.
def main() -> None:
"""Runs the example.
"""
# For each RS material name and graph tuple in the document.
for name, graph in IterRsGraphGraphs(doc):
# Create a converted material, rename it, and insert it into the document.
material: c4d.BaseMaterial = ConvertMaterial(graph)
material.SetName(f"{name} [STD]")
doc.InsertMaterial(material)
if __name__ == "__main__":
main()