Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Login

    Issue collecting all the shaders in a material

    Cinema 4D SDK
    python 2024
    3
    11
    2.3k
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • H
      HerrMay
      last edited by

      Hi @John_Do,

      I could imagine that you stumble upon the same problem I did back then. Have a look at this post.

      @ferdinand helped me back then. The key gotcha was that I didn't account for the branching nature of a shader tree. I have pasted the code to succesfully traverse such shader tree down below.

      Cheers,
      Sebastian

      import c4d
      
      doc: c4d.documents.BaseDocument  # The active document
      
      def TraverseShaders(node: c4d.BaseList2D) -> c4d.BaseShader:
          """Yields all shaders that are hierarchical or shader branch descendants of #node.
      
          Semi-iterative traversal is okay in this case, at least I did not bother with implementing it fully
          iteratively here.
      
          In case it is unclear, you can throw any kind of node at this, BaseObject, BaseMaterial, 
          BaseShader, etc., to discover shaders which a descendants of them.
          """
          if node is None:
              return
      
          while node:
              if isinstance(node, c4d.BaseShader):
                  yield node
      
              # The shader branch traversal.
              for descendant in TraverseShaders(node.GetFirstShader()):
                  yield descendant 
      
              # The hierarchical traversal.
              for descendant in TraverseShaders(node.GetDown()):
                  yield descendant
      
              node = node.GetNext()
              
      def main() -> None:
          """
          """
          material: c4d.BaseMaterial = doc.GetActiveMaterial()
          print (f"{material = }\n")
          for shader in TraverseShaders(material):
              print (shader)
      
      if __name__ == '__main__':
          main()
      
      1 Reply Last reply Reply Quote 0
      • M
        m_adam
        last edited by

        Hi @John_Do the answers provided by @HerrMay is correct thanks a lot to him.

        If this does not work I would invite you to reach the corona support teams, since Maxon can't provide support for 3rd party render and they may have some corner cases we do not know about.

        Cheers,
        Maxime.

        MAXON SDK Specialist

        Development Blog, MAXON Registered Developer

        1 Reply Last reply Reply Quote 0
        • John_DoJ
          John_Do
          last edited by John_Do

          Thanks you for the link and the code @HerrMay !

          These functions are working fine at first, no shader is missing, pretty much the contrary !
          It seems it is 'branching' a bit too much and output all the shaders in the scene and not just the ones in the active material. I guess I have to start the loop one level deeper or something similar.

          bb411f25-244a-43f1-b958-afe241a5b2ee-image.png

          EDIT
          So starting the loop from the first shader in the material work, it no longer output shaders parented to other materials. But now it loops over some shaders up to 3 times, which I guess comes from the way the function traverse the shader tree.
          However I'm noticing that several shaders have the same adress in memory, how can it be possible ? What does it mean ?

          mtl = <c4d.BaseMaterial object called Material/Corona Physical with ID 1056306 at 1365502093504>
          
          <c4d.BaseShader object called Fusion/Fusion with ID 1011109 at 1362487764672> # Same shader different adress
          <c4d.BaseShader object called Filter/Filter with ID 1011128 at 1362487747904> # Different shader same adress 
           <c4d.BaseShader object called AO/Bitmap with ID 5833 at 1362487740608>
          <c4d.BaseShader object called Color/Color with ID 5832 at 1362487747904> # Different shader same adress 
          <c4d.BaseShader object called CoronaNormal/Normal with ID 1035405 at 1362487720000>
          <c4d.BaseShader object called Projector/Projector with ID 1011115 at 1362487747904>
          <c4d.BaseShader object called Normal/Bitmap with ID 5833 at 1362487766080>
          <c4d.BaseShader object called Roughness/Bitmap with ID 5833 at 1362487747904>
          <c4d.BaseShader object called Fusion/Fusion with ID 1011109 at 1362487766976> # Same shader different adress
          <c4d.BaseShader object called Filter/Filter with ID 1011128 at 1362487747904>
          <c4d.BaseShader object called AO/Bitmap with ID 5833 at 1362487766080>
          <c4d.BaseShader object called Color/Color with ID 5832 at 1362487747904>
          <c4d.BaseShader object called Roughness/Bitmap with ID 5833 at 1362487815488>
          <c4d.BaseShader object called Fusion/Fusion with ID 1011109 at 1362487766976>
          <c4d.BaseShader object called Filter/Filter with ID 1011128 at 1362487743936>
          <c4d.BaseShader object called AO/Bitmap with ID 5833 at 1362504263360>
          <c4d.BaseShader object called Color/Color with ID 5832 at 1362487743936>
          
          John_DoJ 1 Reply Last reply Reply Quote 0
          • John_DoJ
            John_Do @John_Do
            last edited by

            I'm not a developer but even so I'm guessing it's ugly and hacky, but I've found a solution.
            Adding the elements in a set eliminate duplicate shaders and returns me the expected result.

            import c4d
            
            doc: c4d.documents.BaseDocument  # The active document
            
            def TraverseShaders(node):
                """Yields all shaders that are hierarchical or shader branch descendants of #node.
            
                Semi-iterative traversal is okay in this case, at least I did not bother with implementing it fully
                iteratively here.
            
                In case it is unclear, you can throw any kind of node at this, BaseObject, BaseMaterial, 
                BaseShader, etc., to discover shaders which a descendants of them.
                """
            
                if node is None:
                    return
            
                while node:
                    if isinstance(node, c4d.BaseShader):
                            yield node
                        
            
                    # The shader branch traversal.
                    for descendant in TraverseShaders(node.GetFirstShader()):
                            yield descendant 
                            
            
                    # The hierarchical traversal.
                    for descendant in TraverseShaders(node.GetDown()):
                            yield descendant 
            
                    node = node.GetNext()
                    
            def main():
                """
                """
                mtls = doc.GetActiveMaterials()
                
                if mtls:
                    for mtl in mtls:
                        print (f"{mtl = }\n")
                        # Looking for Corona Physical Mtl or Corona Legacy Mtl
                        if mtl.CheckType(1056306) or mtl.CheckType(1032100):
                            mtl_bc = mtl.GetDataInstance()
                            shaders = set()
                            for bcid, value in mtl_bc:
                                if isinstance(value, c4d.BaseShader):
                                    for shader in TraverseShaders(value):
                                        shaders.add(shader)
                            if shaders:
                                for i in shaders:
                                    print(i)
                            del shaders
            
                
            
            if __name__ == '__main__':
                main()
            

            Now, if you know a better way to do this, please share it, I'm eager to learn.

            H 1 Reply Last reply Reply Quote 0
            • H
              HerrMay @John_Do
              last edited by

              Hi @John_Do,

              the multiple loop of some shaders can indeed come from the way the TraverseShaders function iterates the shader tree. Additionally theat function has another drawback as its iterating the tree utilizing recursion which can lead to problems on heavy scenes.

              Find below a script that uses an iterative approach of walking a shader tree. As I don't have access to Corona I could only test it for standard c4d materials.

              Cheers,
              Sebastian

              import c4d
              
              doc: c4d.documents.BaseDocument  # The active document
              
              def iter_shaders(node):
                  """Credit belongs to @ferdinand from the Plugincafe. I added only the part with the material and First Shader checking.
                  
                  Yields all descendants of ``node`` in a truly iterative fashion.
              
                  The passed node itself is yielded as the first node and the node graph is
                  being traversed in depth first fashion.
              
                  This will not fail even on the most complex scenes due to truly
                  hierarchical iteration. The lookup table to do this, is here solved with
                  a dictionary which yields favorable look-up times in especially larger
                  scenes but results in a more convoluted code. The look-up could
                  also be solved with a list and then searching in the form ``if node in
                  lookupTable`` in it, resulting in cleaner code but worse runtime metrics
                  due to the difference in lookup times between list and dict collections.
                  """
                  if not node:
                      return
              
                  # The lookup dictionary and a terminal node which is required due to the
                  # fact that this is truly iterative, and we otherwise would leak into the
                  # ancestors and siblings of the input node. The terminal node could be
                  # set to a different node, for example ``node.GetUp()`` to also include
                  # siblings of the passed in node.
                  visisted = {}
                  terminator = node
              
                  while node:
              
                      if isinstance(node, c4d.Material) and not node.GetFirstShader():
                         break
                      
                      if isinstance(node, c4d.Material) and node.GetFirstShader():
                          node = node.GetFirstShader()
                      
                      # C4DAtom is not natively hashable, i.e., cannot be stored as a key
                      # in a dict, so we have to hash them by their unique id.
                      node_uuid = node.FindUniqueID(c4d.MAXON_CREATOR_ID)
                      if not node_uuid:
                          raise RuntimeError("Could not retrieve UUID for {}.".format(node))
              
                      # Yield the node when it has not been encountered before.
                      if not visisted.get(bytes(node_uuid)):
                          yield node
                          visisted[bytes(node_uuid)] = True
              
                      # Attempt to get the first child of the node and hash it.
                      child = node.GetDown()
              
                      if child:
                          child_uuid = child.FindUniqueID(c4d.MAXON_CREATOR_ID)
                          if not child_uuid:
                              raise RuntimeError("Could not retrieve UUID for {}.".format(child))
              
                      # Walk the graph in a depth first fashion.
                      if child and not visisted.get(bytes(child_uuid)):
                          node = child
              
                      elif node == terminator:
                          break
              
                      elif node.GetNext():
                          node = node.GetNext()
              
                      else:
                          node = node.GetUp()
                      
              def main():
                  
                  materials = doc.GetActiveMaterials()
                  tab = " \t"
              
                  for material in materials:
                  
                      # Step over non Corona Physical materials or Corona Legacy materials.
                      if material.GetType() not in (1056306, 1032100):
                          continue
                      
                      print (f"{material = }")
              
                      for shader in iter_shaders(material):
                          print (f"{tab}{shader = }")
                          
                      print(end="\n")
              
              if __name__ == '__main__':
                  main()
              
              John_DoJ 1 Reply Last reply Reply Quote 1
              • John_DoJ
                John_Do @HerrMay
                last edited by

                Thank you for the script @HerrMay it works great on a Cinema 4D Standard Material !

                Without recursion I find this one easier to understand, I just had to change the class check to from c4d.Material to c4d.BaseMaterial to made it working with Corona.

                One thing though I'm not sure to understand is the terminator thing, with this in the function :

                terminator = node
                

                and this a little further in the loop

                        elif node == terminator:
                            break
                

                Since the terminator assignation is done out of the loop, does it mean it is equal to the material in this case, so when the loop is getting back to the first assigned element = the material, it ends ?

                H 1 Reply Last reply Reply Quote 0
                • H
                  HerrMay @John_Do
                  last edited by

                  Hi @John_Do,

                  great to hear that I could be of help and that it works for you. 🙂

                  Yes, I find it myself often enough hard to understand recursive code as well. I guess it's true what they say.
                  "To understand recursion one must first understand recursion." 😄

                  You guessed correctly. The variable terminator is doing nothing in this case. As does the equality check against node. It is (for each loop) assigned to the incoming node which is the current material that is processed in each loop cycle. So node and terminator are essentially the same in this context. 😉

                  Cheers,
                  Sebastian

                  1 Reply Last reply Reply Quote 0
                  • M
                    m_adam
                    last edited by

                    Just in case with release 2024.2 the debugger is working for script and plugins. You can find documentation about how to set it up in https://developers.maxon.net/docs/py/2024_2_0/manuals/manual_py_vscode_integration.html#visual-studio-code-integration-manual.

                    Once you have the debugger attached you can then place breakpoint in your code and go step by step and inspect all variables at any time, this may help you to have a better understanding about the logic.

                    Cheers,
                    Maxime.

                    MAXON SDK Specialist

                    Development Blog, MAXON Registered Developer

                    John_DoJ 1 Reply Last reply Reply Quote 1
                    • John_DoJ
                      John_Do @m_adam
                      last edited by

                      @m_adam Thanks it's a nice addition, I will look into this

                      John_DoJ 1 Reply Last reply Reply Quote 0
                      • John_DoJ
                        John_Do @John_Do
                        last edited by

                        Hi,

                        One last thing I've noticed.

                        The last code shared by @HerrMay works great on all materials but the one I'm using to test my script, it's odd.
                        On this particular material, the function consistently skip the shader which is in the Blend Channel of the Fusion shader. Even the Mask shader in the Mask channel is discovered and printed.
                        db2bf693-59e6-4eb1-a42e-5c484b38f253-image.png

                        I've ran the debugger on the function :

                        • when the loop state is on the Color shader, the function is going up on the parent Fusion shader as expected
                        • but then the elif node.GetNext() condition isn't verified (?!) so it skips the last branch and go back to the material

                        The thing is it doesn't happen with other materials nor other shaders :

                        • I've made this particular material's shader tree more complex and all the shaders are seen, even when a node has several children.
                        • A brand new material with the same shader setup is not a problem.

                        There are two cases where the function scan the shader correctly and output the expected result :

                        • When using the recursive function written by @ferdinand
                        • Disconnecting and reconnecting the Filter shader from the Fusion shader. If I run the function again after doing that, the AO and Filter shaders are discovered and printed correctly.

                        Do you have any idea of what it is happening here ?
                        I'm planning to use this function for several commands and I don't like the possibility that it could fail in some cases, even if it is some niche ones.

                        1 Reply Last reply Reply Quote 0
                        • M m_adam referenced this topic on
                        • John_DoJ John_Do referenced this topic on
                        • First post
                          Last post