Group Details Private

Global Moderators

Forum wide moderators

  • RE: autocreate RS node material based texture sets in tex folder

    Hi,

    I am not quite sure how you mean this, what do you consider to be a 'version agnostic way to call the node space'? Do you mean if there are node space ID symbols exposed in earlier versions than 2025? Unfortunately not. But maxon.NodeSpaceIdentifiers is actually one of the very few things we define in Python (in resource\modules\python\libs\python311\maxon\frameworks\nodes.py), so you could just monkey patch earlier versions.

    import maxon
    
    # Copied from `nodes.py`, I just made the namespaces explicit.
    class NodeSpaceIdentifiers:
        """ 
        Holds the identifiers for all node spaces shipped with Cinema 4D.
    
        .. note::
    
            When you want to reference the node space added by a plugin, you must ask the plugin vendor for the node space
            id literal of that plugin, e.g., `com.fancyplugins.nodespace.foo`. When asked for a node space ID in the API,
            you must then pass `Id("com.fancyplugins.nodespace.foo")`.
        """
        #: References the node space used by node materials for the Standard renderer.
        StandardMaterial: maxon.Id = maxon.Id("net.maxon.nodespace.standard")
    
        #: References the node space used by node materials for the Redshift renderer.
        RedshiftMaterial: maxon.Id = maxon.Id("com.redshift3d.redshift4c4d.class.nodespace")
        
        #: References the node space used by Scene Nodes graphs.
        SceneNodes: maxon.Id = maxon.Id("net.maxon.neutron.nodespace")
    
    # Attache the class to `maxon` under `NodeSpaceIdentifiers` when it is not there. You could also
    # add a version check to be extra sure.
    if not hasattr(maxon, "NodeSpaceIdentifiers"):
        maxon.NodeSpaceIdentifiers = NodeSpaceIdentifiers
        
    
    print (maxon.NodeSpaceIdentifiers.SceneNodes)
    

    edit: NodeSpaceIdentifiers has actually been introduced with 2024.3

    posted in Cinema 4D SDK
  • RE: Start CUSTOMGUI_LINKBOX expanded

    Hey @ECHekman,

    thank you for reaching out to us. This GUI does not support any of these flags (DESC_DEFAULT, i.e., DEFAULT is also not for gadgets but groups).

    This means that you cannot toggle the collapsed state of a link box (be it a normal one or a texture one) by setting a description flag either in a resource file or programmatically. Which makes sense, because other than for example a color field or a group, a link box is not guaranteed to have content which could be unfolded.

    Cheers,
    Ferdinand

    posted in Cinema 4D SDK
  • RE: Managing render settings and getting list of expected output files.

    Hi @BigRoy,

    Please check our Support Procedures in regards of the question structure, namely:

    Singular Question: The initial posting of a support topic must contain a singular question. Do not ask ten things at once, that makes it extremely hard to answer topics. Break up your questions into multiple topics. Asking follow-up questions is allowed but they must be bound to the initial subject.

    Singular Subject: From all this follows that a topic must have a singular and sparse subject tied to a specific problem. 'My Nodes API Questions' is not a valid topic as it splits up into too many subtopics. A valid subject could be 'accessing node attributes' or 'traversing a node graph'. One could for example first ask 'how to get the name attribute of a node?' and then ask a follow up question about 'how to get the icon attribute too?'. This specifically applies to plugins, just because you have two problems with one plugin does not mean that they should be asked in the same topic.

    Regarding you question, there's no high level API that allows you to get a list of all files you've mentioned at once. This effectively means that not only do you need to access this yourself, but you also need to do so for the render engines you'd like to support, as these are likely going to differ among them.

    Specifically for the Redshift AOVs and output files question, please check related threads:

    As for the takes, the only application where they make any sense is the Render Queue. If you're not using it, it's only the active take that's being rendered. Of course you can deal with it by using SetCurrentTake function.

    I suggest you keeping this thread for the "render output files" part of your initial question and encourage you to make separate postings for your other questions that diverse from this topic here.

    Cheers,
    Ilia

    posted in Cinema 4D SDK
  • RE: Discussion on the feasibility of exporting FBX through zscript

    Hey @qq475519905,

    sorry for the delay. As I said in the beginning, you can implement your own serializer using GoZ. But there is no dedicated exporter API in ZScript. I assume you are using some RoutineCall to achieve what you are doing. You cannot escape any GUI associated with such call.

    Cheers,
    Ferdinand

    posted in ZBrush SDK
  • RE: autocreate RS node material based texture sets in tex folder

    Hello @apetownart,

    so, I had a look at your code, and there are unfortunately quite a few issues with it. Did you write this code yourself? We have a code example which is quite close to what you are trying to do. Your code strikes me as AI-generated, but I might be wrong.

    Please understand that we cannot provide support on this level of issues, we expect users to generally follow our documentation, we cannot provide here line-by-line support. I have attached below your script in a commented version up to the point to which I debugged it. What you want to do, is certainly possible with the low level Nodes API in Python.

    I would recommend to use the more artist friendly high level graph description API in your case. I have attached an example below.

    ⚠ maxon.GraphDescription.ApplyDescription currently prints junk to the console when executed. This is a known issue and will be fixed in the next release.

    Cheers,
    Ferdinand

    Result

    ba71a93d-78da-4bb9-968d-c457834e563b-image.png

    Code

    """Demonstrates how to compose materials using graph descriptions for texture bundles provided
    by a texture website or an app like substance painter.
    
    The goal is to construct materials over the regular naming patterns such texture packs exhibit.
    """
    import os
    
    import c4d
    import maxon
    import mxutils
    
    doc: c4d.documents.BaseDocument # The active document.
    
    # The texture formats we support loading, could be extended to support more formats.
    TEXTURE_FORMATS: tuple[str] = (".png", ".jpg", ".tif", ".exr")
    
    # Texture postfix classification data. This map exists so that you could for example map both the
    # keys "color" and "diffuse" to the same internal classification "color". I did not make use of this
    # multi routing here.
    TEXTURE_CLASSIFICATIONS: dict = {
        "color": "color",
        "roughness": "roughness",
        "normal": "normal",
    }
    
    def GetTextureData(doc: c4d.documents.BaseDocument) -> dict[str, dict[str, str]]:
        """Ingests the texture data from the 'tex' folder of the passed document and returns it as a
        dictionary.
    
        This is boilerplate code that is effectively irrelevant for the subject of using graph
        descriptions to construct materials.
        """
        # Get the document's texture folder path.
        path: str = mxutils.CheckType(doc).GetDocumentPath()
        if not path:
            raise RuntimeError("Cannot operate on an unsaved document.")
    
        texFolder: str = os.path.join(path, "tex")
        if not os.path.exists(texFolder):
            raise RuntimeError(f"Texture folder '{texFolder}' does not exist.")
    
        # Start building texture data.
        data: dict[str, dict[str, str]] = {}
        for file in os.listdir(texFolder):
            # Fiddle paths, formats, and the classification label into place.
            fPath: str = os.path.join(texFolder, file)
            if not os.path.isfile(fPath):
                continue
    
            name: str = os.path.splitext(file)[0].lower()
            ext: str = os.path.splitext(file)[1].lower()
            if ext not in TEXTURE_FORMATS:
                continue
    
            items: list[str] = name.rsplit("_", 1)
            if len(items) != 2:
                raise RuntimeError(
                    f"Texture file '{file}' is not following the expected naming convention.")
    
            # Get the label, i.e., unique part of the texture name, and its classification postfix.
            label: str = items[0].lower()
            classification: str = items[1].lower()
            if classification not in TEXTURE_CLASSIFICATIONS.keys():
                continue
            
            # Map the classification to its internal representation and write the data.
            container: dict[str, str] = data.get(label, {})
            container[TEXTURE_CLASSIFICATIONS[classification]] = fPath
            data[label] = container
            
        return data
    
    def CreateMaterials(doc: c4d.documents.BaseDocument) -> None:
        """Creates Redshift materials based on the texture data found in the 'tex' folder of the passed
        document.
        """
        # Get the texture data for the document.
        mxutils.CheckType(doc)
        data: dict[str, dict[str, str]] = GetTextureData(doc)
         
        # Iterate over the texture data and create materials.
        for label, container in data.items():
            # Create a Redshift Material and get its graph.
            graph: maxon.NodesGraphModelRef = maxon.GraphDescription.GetGraph(
                name=label, nodeSpaceId=maxon.NodeSpaceIdentifiers.RedshiftMaterial)
    
            # We could have also created a graph with the default nodes in it in the call above, but
            # it is just easier to do things manually here. So, we create an empty graph and add here
            # an output node with a standard material attached. See
            #
            #   https://developers.maxon.net/docs/py/2025_1_0/manuals/manual_graphdescription.html
            #
            # for more information on how to use the graph description API.
            description: dict = {
                "$type": "Output",
                "Surface": {
                    "$type": "Standard Material",
                }
            }
    
            # Extend the description based on the textures we found in the current texture pack.
    
            # Attach a texture node with the color texture to the "Base/Color" port of the Standard
            # Material node.
            if "color" in container:
                description["Surface"]["Base/Color"] = {
                    "$type": "Texture",
                    "General/Image/Filename/Path": maxon.Url(f"file:///{container['color']}")
                }
            # The same for roughness.
            if "roughness" in container:
                description["Surface"]["Base/Diffuse Roughness"] = {
                    "$type": "Texture",
                    "General/Image/Filename/Path": maxon.Url(f"file:///{container['roughness']}")
                }
            # Basically the same again, just that we also add a "Bump Map" node when we link a
            # normal map.
            if "normal" in container:
                description["Surface"]["Geometry/Bump Map"] = {
                "$type": "Bump Map",
                "Input Map Type": 1, # Tangent Space
                "Height Scale": .2, # Adjust this value to match the normal map intensity.
                "Input": {
                    "$type": "Texture",
                    "Image/Filename/Path": maxon.Url(f"file:///{container['normal']}")
                }
            }
    
            # And finally apply it to the graph.
            maxon.GraphDescription.ApplyDescription(graph, description)
    
    # Execute the script
    if __name__ == '__main__':
        CreateMaterials(doc)
        c4d.EventAdd()
    

    Your Code

    def create_redshift_material(name, texture_files):
        """Creates a Redshift Node Material and assigns textures."""
        doc = c4d.documents.GetActiveDocument()
    
        # (FH): Not necessary and counter productive.
        if not c4d.IsCommandChecked(ID_NODE_EDITOR_MODE_MATERIAL):
            c4d.CallCommand(ID_NODE_EDITOR_MODE_MATERIAL)
    
        # (FH): That is not sufficient, see code examples. You also have to add the post effect.
        # Ensure Redshift is the active render engine.
        render_settings = doc.GetActiveRenderData()
        if render_settings:
            render_settings[c4d.RDATA_RENDERENGINE] = ID_REDSHIFT_ENGINE
    
        # (FH): Avoid commands, use the API instead. Invoking events multiple times in scripts is also
        # pointless as a script manager script is blocking. I.e., having a loop which changes the scene
        # and calls EventAdd() on each iteration, or the same loop and calling EventAdd() once at the
        # end of the script will have the same effect.
        c4d.CallCommand(1036759)  # Redshift Material
        c4d.EventAdd()
    
        # (FH): Could be condensed to a single line using maxon.GraphDescription.GetGraph()
        material = doc.GetActiveMaterial()
        if not material:
            raise RuntimeError("Failed to create Redshift Material.")
    
        material.SetName(name)
        node_material = material.GetNodeMaterialReference()
        if not node_material:
            raise RuntimeError("Failed to retrieve the Node Material reference.")
        redshift_nodespace_id = maxon.NodeSpaceIdentifiers.RedshiftMaterial
        if not node_material.HasSpace(redshift_nodespace_id):
            graph = node_material.AddGraph(redshift_nodespace_id)
        else:
            graph = node_material.GetGraph(redshift_nodespace_id)
    
        if graph is None:
            raise RuntimeError("Failed to retrieve or create the Redshift Node Graph.")
    
        print(f" Created Redshift Node Material: {name}")
    
        # (FH) Incorrect call and the cause for your crashes.
        # with maxon.GraphTransaction(graph) as transaction:
        with graph.BeginTransaction() as transaction:
            texture_nodes = {}
            for channel, file_path in texture_files.items():
                # (FH) Avoid using such exotic Unicode symbols, Python supports them but not every 
                # system in Cinema 4D does.
                print(f"🔹 Creating node for {channel}: {file_path}")
    
                node_id = maxon.Id(TEXTURE_TYPES[channel])
                node = graph.AddChild("", node_id, maxon.DataDictionary())
                if node is None:
                    print(f" Failed to create node for {channel}")
                    continue
    
                # (FH) That cannot work, #filename is not a port which is a direct child of a RS 
                # texture node, but a child port of the "tex0" port bundle, the port is also called
                # "path" and not "filename". 
    
                # See: scripts/05_modules/node/create_redshift_nodematerial_2024.py in
                # https://github.com/Maxon-Computer/Cinema-4D-Python-API-Examples/ for an example for
                # how to construct a Redshift Node Material with Python, likely covering most what you
                # need to know.
                filename_port = node.GetInputs().FindChild("filename")
                if filename_port:
                    # Will fail because filename_port is not a valid port.
                    filename_port.SetDefaultValue(maxon.Url(file_path))
                else:
                    print(f" 'filename' port not found for {channel} node.")
    
    # (FH): Stopped debugging from here on out ...
    
                texture_nodes[channel] = node
                print(f" Successfully created {channel} texture node.")
    
            material_node = graph.GetRoot()
            if material_node:
                for channel, tex_node in texture_nodes.items():
                    input_port_id = f"{channel.lower()}_input"
                    material_input_port = material_node.GetInputs().FindChild(input_port_id)
                    if material_input_port:
                        tex_output_port = tex_node.GetOutputs().FindChild("output")
                        if tex_output_port:
                            material_input_port.Connect(tex_output_port)
                            print(f"🔗 Connected {channel} texture to material.")
                        else:
                            print(f" 'output' port not found on {channel} texture node.")
                    else:
                        print(f" '{input_port_id}' port not found on material node.")
            else:
                print(" Material node (root) not found in the graph.")
    
            transaction.Commit()
    
        return material
    
    def main():
        """Main function to create Redshift materials based on texture sets in 'tex' folder."""
        # (FH) Not necessary, #doc is already available in the global scope.
        doc = c4d.documents.GetActiveDocument()
        tex_folder = os.path.join(doc.GetDocumentPath(), "tex")
    
        texture_sets = get_texture_sets(tex_folder)
        if not texture_sets:
            c4d.gui.MessageDialog("No texture sets found in 'tex' folder!")
            return
    
        for texture_set, texture_files in texture_sets.items():
            create_redshift_material(texture_set, texture_files)
    
    posted in Cinema 4D SDK
  • RE: Apparently incorrect matrices in Python effector when subject to deformer

    Hey @vhardy,

    I am happy to see that you made progress! Please excuse me for being a bit direct, but is here still an open question? Because I am not so sure.

    When I would work on something, it would be your lighting model because not taking the light source position for a directed light source you seem to want to emulate, will often look "wrong". There are many ways you can do this, here is a simple one in pseudo code:

    def calculate_illumination(p: c4d.Vector, pn: c4d.Vector, lp: c4d.Vector, ln: c4d.Vector, lt: float, li: float) -> float:
        """Calculates the illumination intensity for point #p for a given a spotlight.
    
        Parameters:
            p (c4d.Vector): The point to illuminate.
            pn (c4d.Vector): The normal at the point to illuminate.
            lp (c4d.Vector): The position of the spotlight.
            ln (c4d.Vector): The orientation of the spotlight.
            lt (float): The angle theta of the spotlight cone (in radians).
            li (float): The diffuse light intensity.
    
        Returns:
            float: The illumination intensity at point p.
        """
        # A vector pointing from the light source to the point.
        q: c4d.Vector = (p - lp).GetNormalized()
    
        # Calculate the dot product between the q and ln normal, i.e., the cosine of the angle spanned
        # between the light-to-point normal and the spot light orientation normal.
        gamma: float = q.Dot(ln.GetNormalized())
    
        # Our point is in the cone of light.
        if gamma > math.cos(lt):
            # Basically what you were doing.
            intensity: float = li * max(0, q.Dot(pn.GetNormalized()))
        # It is not.
        else:
            intensity = 0
    
        return intensity
    

    You could of course make this fancier by for example not having a harsh contrast between in-cone and not-in-cone via the a bit brutish if gamma > math.cos(lt) and instead blend smoothly or by using a more complex lighting model.

    Or you can just use ray casting whose costs due to the very few rays you will cast here will be negligible, and then have right away full self shadowing (but would then need access to mesh against which to ray cast).

    Cheers,
    Ferdinand

    posted in Cinema 4D SDK
  • RE: Changing OCIO View Transform doesn't work using Python

    Hey @Smolak,

    I understood what you wanted to do, but it is still not possible. First of all, the OCIO color space enum values are dynamically assigned, i.e., the value 3 might not always mean the same space. In the OCIO API there are special conversion functions for that, but you could also do that manually by parsing the description of the document.

    But you will never be able to apply such value then, because to do that, you have to call BaseDocument::UpdateOcioColorSpaces which does not exist in Python (yet). See the C++ Docs for an example.

    Cheers,
    Ferdinand

    posted in Cinema 4D SDK
  • RE: Changing OCIO View Transform doesn't work using Python

    Hey @Smolak,

    Thank you for reaching out to us. I assume your code came from a chat bot, because pretty much every line in it is hallucinated. The Python API currently has no OCIO API, only C++ has (and it does not work like what your code shows).

    We are currently working on the Python OCIO API, which will then also allow you to set the view transform of a document. It will shipped with a future version of Cinema 4D, but as always, we cannot give out more concrete information about a when. But it will be quite soon.

    Cheers,
    Ferdinand

    posted in Cinema 4D SDK
  • RE: c4d.documents.RenderDocument does not apply OCIO Viewtransform

    @hSchoenberger said in c4d.documents.RenderDocument does not apply OCIO Viewtransform:

    It is a blocking render using Commandline.exe [...] And it always output files to disk.

    Are these questions? Renderings in themself are not blocking, as they happen in their own render thread. What can be blocking, is the action that invokes the rendering, as many things happen by default on the main thread of Cinema 4D in Python; e.g., Script Manger scripts run on the main thread. E.g., a c4d.documents.RenderDocument call on its own will only exit once the rendering is done, which makes sense as you otherwise could not interact with the finished rendered bitmap. Similar things apply to the BatchRender. But you can make the BatchRender not blocking by wrapping it in a thread (and I think you can also do that with RenderDocument in Python, but I never tried there). You cannot do this with a CallCommand, they will always be blocking. I.e., when you invoke the lines below, "Hello world!" will only be printed once the rendering is done.

    c4d.CallCommand(12099)
    print("Hello world!")
    

    And more important, since this is blocking the main thread, the user could also not interact with the GUI. When you are in a headless Cinema 4D instance like c4dpy or the CommandLine (not quite sure how you use the CommandLine in this context), then this is of course not really important, as you do not care about having to wait for the renderer to finish.

    On the other side, CallCommand is a wrapper to emulate a button click in the UI as far as I understand.

    Sort of, but at the same time also not really. CallCommand just invokes a command, more specifically the CommandData::Execute function of the command that has the passed plugin ID. But commands are in the GUI often associated with buttons. But that does not mean that you cannot invoke commands in a headless Cinema 4D, i.e., CallCommand and co. work just fine there.

    But you cannot run Script Manager scripts in the CommandLine (which you seem to imply). The only Python code which will run in a CommandLine are plugins or scripting elements. With c4dpy you can also run Python code that is not a plugin or embedded into a document in a headless environment. Unless Maxime implemented the -script argument for the CommandLine, but I do not see any indication that he did.

    BatchRender would probably change the render queue items if C4D is started by an artist as well [...]

    Depends on where this runs. When you implement this in a non-blocking manner, and let this run in a Cinema 4D.exe on which an artist is working, then, yes, this could lead to conflicts when incorrectly implemented (not having a proper while queue.IsRendering() loop). As the artist would have to stop the current batch to make changes, and IsRendering would then be False. But when you start another Cinema 4D.exe, or CommandLine.exe, or c4dpy.exe on the same machine, they would all have their own render queue.

    They question would be how sensible such setup would be when you are using Redshift or pretty much any other GPU renderer, as they tend to try to claim all VRAM resources when initialized. So, when you start two GPU render jobs on the same machine, you end up with horrible performance in both.

    I would really advise against trying to do this, letting GPU render slaves run on a machine an artist is actively working on. Not only artist and render slave renders will have terrible performance, also stuff like particles, fluids, and other simulation stuff, as they all want all your VRAM. The common practice of enlisting unused artist workstations as render slaves is of course fine.

    Cheers,
    Ferdinand

    posted in Bugs
  • RE: Show or Hide Object in Viewport

    Sorry, I am rotating on the spot right now, did not yet get to answering here. @gheyret is right, there is still a bug, will give @m_adam a note on Monday.

    posted in Cinema 4D SDK