Script that converts Redshift materials back to C4D standard ones

Hey there!
first time posting here (I come from the other forum, backstage as a beta tester) and someone advised me to post my question there so... here am I :)

I know we will be more and more pushed to use RS as the default Render engine in future versions of C4D, BUT, what I really need right now (for a big project) is to go exactly the opposite direction (we need NPR rendering and we also need to export some of the textured objects in various game ready formats that can't handle RS mats... yet)!

I have huge scene files that used RS for materials, lighting and rendering, and I need to convert as much materials as possible back to Standard materials (not the RS Standard materials, but our old good C4D Standard Renderer).

We really can't do that manually because there are really too much of them (hundreds of them), so I need to find a solution.
Any chance someone has already managed to write some python scripts that:

1/ analyze the bitmap files used in the most common RS materials channels (diffuse, at the very least, but if it could also do it for Emission and Roughness, it would be great ;

2/ and apply these same files in some newly created C4D Standard materials in the proper channels (Diffuse in Color, Emission in Luminance, Roughness in Reflectance roughness...)

3/ and replace each RS material by its C4D standard mat equivalent on each object

I'm pretty sure I can't be the only one who actually need such a script...
Thanks in advance for your help

What a time to be alive!

Hello @Fremox,

Welcome to the Plugin Café forum and the Cinema 4D development community, it is great to have you with us!

Getting Started

Before creating your next postings, we would recommend making yourself accustomed with our Forum and Support Guidelines, as they line out details about the Maxon SDK Group support procedures. Of special importance are:

About your First Question

Phew, that first question is certainly something :) There are however some ambiguities and general problems, let's tackle them in bullet points:

  1. Although you do not state it directly, your posting could be interpreted as such that you expect some else, Maxon or a third party, to write code for you. You posted in General Talk and say things like 'I'm pretty sure I can't be the only one who actually need such a script...'. When you are looking for someone else to write a solution for you, you should state so explicitly. The members of the SDK group will not provide end-user solutions on the forum.
  2. You do not state clearly what your inputs and outputs are supposed to be. You talk about 'RS materials' and 'standard mat[erials]' which can mean a lot.
    • What are your inputs? Standard material instances which have been adjusted for RS, Xpresso node material for RS, Node Editor materials for RS, or a mix of all of them?
    • What are your desired outputs? You mention 'standard mat[erials]'. Are you talking about the Standard Material or are you talking about materials for the standard renderer, as for example a Node Material?
  3. Between your inputs and outputs, you seem to expect some kind of conversion to happen. The details are quite fuzzy, but I assume the idea is that the solution gathers texture file information from an input over filenames and and the ports/parameters textures are connected to. It would be best if you would provide at least one example screenshot of a before and after, so that readers can gather a sense of what you expect to happen.


To be frank, this sounds like a more substantial task than you seem to be aware of, because mapping from one material space to another is not a trivial thing to do. You said that there are hundreds of materials to convert and that it would not be feasible to do it manually. I am not quite sure if that assessment is true. In the end it depends on what you want to be done, but even in its most abstract and simplistic variant it would probably take me at least two work days to write something sensible. You could probably also punch out a lot of materials in that time manually.

I could speculate here more but that would be pretty meaningless. No matter if you are searching for a developer or seek help in implementing this yourself (you would have posted in the wrong forum then), you should make it more tangible what you want to happen. You should also make it more clear if you are looking for technical support or for a developer for hire.


MAXON SDK Specialist


Thanks a lot for your detailed reply.
And sorry if my question looked a bit fuzzy, I didn't want to confuse anyone!

So, I'm just investigating what's possible as a solution right now, I did think that maybe, such a script would already exist in some sort and could be easily tweakable/adapted for our needs without too much work, but you are right, I may have underrated the amount of work such a task represents.
Not my fault though... Every time I came up with a request on the Maxon backstage forum, I almost always ended up with a genious guy saying "Hey, I've just wrote a script that does that..", so I assumed it would be the same here, sorry ^_^

I will then answer to your 3 points one by one:

  1. I won't lie here, even if I have some experience with development, it's only in Javascript (I create scripts for AfterEffects), and such a script for C4D is way beyond my skills ; so yes, I guess we (because I work for a team) will search for developper for hire on this. BUT, please wait for me to give more details about that, because I need to talk about this with my team and my hierarchy first (as I said, I was "just" investigating a bit in order to see if a solution existed)

  2. The inputs would be either some RS materials or RS Standard materials, that usually only have a plain color or a single bitmap texture plugged in their diffuse (sometimes there are some textures in the emission too), with various roughness values ;
    The output have to be materials for the Standard renderer (the old materials not the Node materials)

  3. you are right with what you assume: the image file plugged in the Diffuse slot of the RS materials should be applied to the Color channel of the Standard renderer materials, with the ability to replace the RS ones, just like if we had made used the "Convert and Replace" command from the Redshift menu, but exactly in the reverse way!

So if we have let say this RS material:
Capture d’écran 2023-06-12 à 15.23.33.png

I would like to create a new Material for the Standard Renderer that looks like this:
Capture d’écran 2023-06-12 à 15.23.03.png

...where, as you can see, the image is the same in both the Diffuse (RS) and the Color channel (STD).

I understand why you had some doubts, but I didn't lie when I said there was hundreds of these materials to convert.
We work for a video game, and each time we have to import an Avatar from the game in C4D, it imports one material per body part, and each character has, at least, 15 different parts... Since we create trailers with sometimes a lot of different characters in a same scene (for a Concert for example), our scenes, often have more than 200 materials!

This why I try to find some viable solution.

What a time to be alive!

Hey @Fremox,

Thank you for your reply. So, your inputs seem to be RS Xpresso materials. In 2023.2, such materials are labeled RS C4D Shader. That might have been obvious for you but it was not for me.

I understood the general assignment, what I was asking for was more an insight into the average complexity of a graph. I.e., you could have something like this where textures are slotted directly into the channels of a material model.


Or you could have something like this where the relations in the graph are much more complex:


And if you have then such "complex" graph, what information must be retained? The RS Mix node could for example be translated into a Fusion or Layer shader, but doing this is extra work. And there are the material models: Which one must be supported? Just RS Standard?


And I am probably not the person you have to answer this in all detail for, but when you tell developers that they should 'analyze the bitmap files used in the most common RS materials channels' their mind will probably be racing with all the corner cases they have to cover. So, the goal is to make as many guarantees as possible regarding to what your inputs will look like, what can be assumed to be always the case, what will never be the case, etc.

But from what I understand, you have RS legacy materials which follow a fixed schema, where texture files are slotted directly into the ports of a RS Standard material node. And the script should then recrate this material as a standard material. Which would be a much more trivial task than at least I read your first posting.


MAXON SDK Specialist


You are right about what you understood.
I think we would just have a simple shema where there are just a bitmap file plugged in to the Diffuse Color of a RS C4D Shader 99% of the times.
But I need to investigate more on my side, in order to ensure my colleagues from the team always rely on XPresso based RS materials, and not the new nodal ones, because otherwise, it would be way more complicated, which I understand completely.

Capture d’écran 2023-06-12 à 17.51.01.png

So, if we stick with this simple shema as an input, since you have written "a much more trivial task" in your last sentence, I was wondering..
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?

What a time to be alive!

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.


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



"""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),
        # Add channel mappings to your liking ...
    # The RS Standard Material model
    "StandardMaterial": {
        "Base Color": (c4d.MATERIAL_COLOR_SHADER, c4d.MATERIAL_USE_COLOR),
        "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):

        # 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":

            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))
                # 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[paramId] = textureShader
        # 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 = }")


            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.
        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])

        # Get the file linked in the texture node and start finding target ports in material model
        # nodes we want to convert to.
        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]")

if __name__ == "__main__":

MAXON SDK Specialist


Whoooo, can't believe I got such an efficient support for what I asked, and in such a little amount of time!
Would kill to have your coding skills ^_^

Thanks for having documented your code with so much comments, it will definitely facilitate my scripting learning journey!

I will first give this nice piece of script a try, in order for me to fully understand what it can and what it can't "do as it is", then I will come back to you, probably with some questions on how I could change or improve it myself, because of course I understand nobody here is willing to do professionnal work for no money :)

Will have to have a deep look at the C4D scripting API and Python of course though, but I'm pretty sure learning at least the basics of all that could already improve my (and my colleagues) workflow so thanks again for your support.

What a time to be alive!

OK, I tried to understand the main code and I think I got most of it.
But when I try to use it on a simple scene with RS materials in them, there is an error at line 54 (I checked the console > Python tab) that says

File "D:\FREMOX\R&D\C4D\Scripts\", line 54, in IterRsGraphGraphs
if not material.CheckType(c4d.Mrsgraph):
AttributeError: module 'c4d' has no attribute 'Mrsgraph'

Where can I find documentation on each attribute available for specific objects in Python C4D?
or maybe the keyword "Mrsgraph" hadn't been written correctly?

Just for your information,
I disabled the roughness, reflection and bump lines in your code by adding a # sign to make them comments, in order for me to just focus on the Diffuse color first.

Any help would be appreciated,
Thanks in advance (and sorry to bother you if it's already to much work on your side, I would totally get it, no worry :)

What a time to be alive!

Hey @Fremox,

AttributeError: module 'c4d' has no attribute 'Mrsgraph'

That is because I have written this code for Cinema 4D 2023.2+. We introduced quite a batch of symbols with 2023.0. I assume you are trying to run the script with an older version?

You can quite simply rectify this by defining the symbol yourself or by replacing all occurrences with the numeric value. E.g., insert this:

# [...]
import c4d
import typing

from c4d.modules.graphview import *

#The line to add ...
c4d.Mrsgraph: int = 1036224
# [...]

But I also used something else in this script which won't run in older versions of Cinema 4D, Python 3.10 style union type hints:

layer: c4d.ReflectionLayer | None = material.GetReflectionLayerIndex(i)

This means layer is of type c4d.ReflectionLayer or None. Older Python versions will have no clue what to do with that, simply remove the optional types then:

layer: c4d.ReflectionLayer = material.GetReflectionLayerIndex(i)

# Or this ...
foo: a | b | c | d = bar()

# will become that ...
foo: a = bar()

You could of course also use typing.Union if you want to retain that additional information. But type hinting is purely cosmetic and for the benefit of the reader, more fringe applications such as runtime assertions and linting aside.

Apart from that, I think I did not use anything particularly "modern" in this code.

PS: We do not track change notes on all documentation units at the moment, the only way to see that information for symbols is to look into the 2023.0 change notes. For stuff like functions and classes, there is usually a note in the documentation unit itself, e.g., "since 2023.0". You can also just put the docs to the version you are using and then search for the symbol. When it is not there, it is not contained in your version ;)


MAXON SDK Specialist

you are right, I tested it in R25 in the first place and that's why it didn't work!

Now that I've tested it in 2023.2, everything works fine, thanks a lot!!

Now I will test different things in order to easily replace RS mats by the STD ones in my scenes.
I've changed the line 198 to


instead of

material.SetName(f"{name} [STD]")

in order for my new STD materials names to be written just like the RS ones, in order to be able to use the Material Exchanger (which needs 2 scenes with exact same materials names to work properly), but I end up with names with ".1" suffixe at the end of the newly created materials.
Since it seems you can't use the Naming tool on materials (I've tested it and it didn't work)
is there a way in our srcipt to kind of "force" the newly STD materials to have the exact same name without this .1 suffixe ?

Sorry to bother you again
i try to wrap my head to find the best solution in the least amount of time (and your script aleardy does 99% of what I wanted so 1000 thanks)

What a time to be alive!

Hey @Fremox,

is there a way in our srcipt to kind of "force" the newly STD materials to have the exact same name without this .1 suffixe ?

Yes, there is. The third argument of BaseDocument.InsertMaterial allows you to turn that smarty-pants behavior of Cinema 4D off.

Alternatively, you can just reorder the instructions, because when you set the name after the material has been inserted, you will overwrite whatever name Cinema 4D came up with.

for name, graph in IterRsGraphGraphs(doc):
        # Create a converted material, rename it, and insert it into the document.
        material: c4d.BaseMaterial = ConvertMaterial(graph)


MAXON SDK Specialist


Perfect, works like a charm!
I will definitely digg the whole code in order to fully understand each instruction, so that I will be able to adapt it for my needs (for example, finding the textures for the Emission channel in RS Shaders and map it correctly to the luminance channel of the STD ones ; Shouln't be a problem since you have neatly documented all your code :)

What a time to be alive!

Hey @ferdinand
Hope you are doing well

Continuing my script testing I realized that the code above didn't work for a colleague who works on a Mac, but I don't really understand why a Python script wouldn't work on both Windows and Mac systems, or am I missing something?
Isn't Python working the same way in both cases?

What a time to be alive!

Hey @Fremox,

Python script wouldn't work on both Windows and Mac systems, or am I missing something? Isn't Python working the same way in both cases?

Well, just as for JavaScript, you can certainly write Python code that only works on one OS. But Cinema 4D Python code in general and my code from above in particular is usually OS agnostic.

The big question would be what is not working for your colleague? I just threw the script on a simple material in macOS 2023.2, and everything worked fine:

Screenshot 2023-06-19 at 10.04.15.png

Please also remember our forum guidelines, we cannot design or debug applications for you. So, in short, you cannot ask us "what is going wrong?", but instead should ask "this error is raised, this Cinema 4D function/class does not do what I would expect it do: why?" To a certain extent, we will also help you with your own code, but we cannot debug applications for users ("my plugin is crashing, what is going wrong?").

Finally, if you intend to update/modify the script, we would ask you to create new topics for technical questions as also lined out in our forum guidelines.


MAXON SDK Specialist


Thanks for your reply.
I was asking by curiosity rather than asking for a debug on your side.
But I get why you understood it that way, so, sorry I'll try to write my posts with that in mind in the future, sorry for the misunderstanding.

As Javascript developper, I never had any issue in After Effects with some code only working on one OS and not the other, this is why I got curious and I was thinking that, maybe, Python wasn't OS agnostic after all. But you answered my question so, as far as I understand there isn't any reason why it shouldn't work on my colleague workstation.

When he runs the same script on his computer, nothing happens at all, while, with the exact same scene, it does work properly on my PC machine.
So I will investigate on my side in order to see what could be wrong with his setup (he uses 2023.2 version too).

Anyway, thanks again for the precision

What a time to be alive!

Hey @Fremox,

Well, you should look at the console output, because when something goes wrong, it will tell you that.

And OS-agnostic is a big term that never really applies. While Python and JS are certainly not as bad as "write-once-debug-everywhere"-Java, you can certainly produce code in both languages which does not run on all OS, e.g., use Python's os.pipe2() (does not run on Windows) or similar thing for Node.js's os module. JS is in a technical sense more OS agnostic than Python by simply not having a std lib where such problems then occur.

But yes, Python just as JS is more or less OS-agnostic.


MAXON SDK Specialist


I get it now, thanks for your detailed explanation!

What a time to be alive!