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

    Undo method for LayerShaderLayer

    Bugs
    python 2024 limitation
    2
    11
    2.2k
    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.
    • John_DoJ
      John_Do
      last edited by ferdinand

      Hi,

      Following the work on this script, I 've now added the support for Layer Shader. Works fine unless for the undo : AddUndo() doesn't work on LayerShaderLayer objects since it isn't a child class of BaseList2D. So I can't add an undo step for the parameters changes on the Layer Shader' layers and it effectively breaks when performing the undo, leaving the Layer Shader with empty layers. What can I do about this ?

      Thank you

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

        Hey @John_Do,

        Thank you for reaching out to us. A LayerShaderLayer is indeed no scene element (BaseList2D). The type is just a convenience interface into the layer data stored in the data container of a LayerShader. You therefore must add the undo's to the LayerShader, even for creation and deletion events.

        Please provide executable code in future threads, linking to old code which does not contain the relevant problem is not sufficient. I have provided a solution below. The thread "How to create and populate a layer shader" might also be helpful in your context.

        Cheers,
        Ferdinand

        Code:

        """Demonstrates how to define undo's for a layer shader.
        
        The layers of a layer shader (LayerShaderLayer instances) are just smoke an mirrors, all the data
        is stored in the data container of the layer shader itself. Undo's for creating, deleting, and 
        modifying layer shader layers are therefore always data container undo's, i.e., CHANGE_SMALL.
        
        Running this script will create a material with a layer shader in its color channel. The material
        will have two undo steps; one for the creation of the material and the layer shader, and one for
        the manipulation of the layer shader.
        
        See also:
            https://developers.maxon.net/forum/topic/14059/how-to-create-and-populate-a-layer-shader/2
        """
        
        import c4d
        from mxutils import *
        
        doc: c4d.documents.BaseDocument  # The active document
        
        
        def main() -> None:
            """
            """
            # Create a material, a layer shader, and a noise shader we are going to use in the layer shader.
            material: c4d.BaseMaterial = CheckType(c4d.BaseList2D(c4d.Mmaterial))
            layerShader: c4d.BaseShader = CheckType(c4d.LayerShader())
            noiseShader: c4d.BaseShader = CheckType(c4d.BaseList2D(c4d.Xnoise))
        
            # Link the shaders and set the layer shader as the color channel of the material.
            material.InsertShader(layerShader)
            noiseShader.InsertUnder(layerShader)
            material[c4d.MATERIAL_COLOR_SHADER] = layerShader
        
            # Add one undo step for adding the material. We do not extra undo's for adding the layer shader
            # attached to the material or the noise shader attached to the layer shader just as we would not
            # add multiple undo's when adding a hierarchy of geometry objects.
            doc.StartUndo()
            doc.InsertMaterial(material)
            doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, material)
            doc.EndUndo()
            doc.SetActiveMaterial(material)
        
            # Now we start a second undo to collect under this all changes to the layer shader. Doing this
            # is not really necessary, we just do this to demonstrate the different nature of such undo 
            # steps.
            doc.StartUndo()
        
            # Although we are calling the method LayerShader.AddLayer there is nothing really added to the
            # scene. A LayerShaderLayer is just a convenance interface for its LayerShader. All layer data
            # is stored in the data container of the layer shader. Adding, removing or modifying layers are
            # therefore all data container operations on the layer shader.
        
            # We summarize all four following operations (add two layers, change a parameter on each layer)
            # in one UNDOTYPE_CHANGE_SMALL undo. 
            doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, layerShader)
        
            layerHsl: c4d.LayerShaderLayer = CheckType(layerShader.AddLayer(c4d.TypeHSL))
            layerNoise: c4d.LayerShaderLayer = CheckType(layerShader.AddLayer(c4d.TypeShader))
            layerHsl.SetParameter(c4d.LAYER_S_PARAM_HSL_LIGHTNESS, -.5)
            layerNoise.SetParameter(c4d.LAYER_S_PARAM_SHADER_LINK, noiseShader)
        
            doc.EndUndo()
        
            # Push and update event to Cinema 4D.
            c4d.EventAdd()
        
        if __name__ == '__main__':
            main()
        

        MAXON SDK Specialist
        developers.maxon.net

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

          Hi @ferdinand,

          Thank you for the sample code ! Indeed I did not provide my code to avoid a complicated request in case I would have missed something really obvious.

          I guessed that your answer was the way to go and already tried it before creating the topic, unfortunately it doesn't work. The good news is it should, so something else must be wrong in my code. You'll find the problematic part below, the part with the Layer Shader layer is the first case in the convert_c4d_bitmap() function :

          def get_layershader_layer(doc: c4d.documents.BaseDocument, layer_shader: c4d.BaseShader, target_shader: c4d.BaseShader = None) -> c4d.LayerShaderLayer:
              # Iterating over Layer Shader layers, looking
              # for the specified type of shaders
              if layer_shader.GetType() == c4d.Xlayer:
                  layer = layer_shader.GetFirstLayer()
                  while layer != None:
                      if layer.GetType() == c4d.TypeShader:
                          shader = layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_LINK)
                          if target_shader and shader == target_shader:
                              return layer
                          layer = layer.GetNext()
                      elif layer.GetNext():
                          layer = layer.GetNext()
                      else:
                          layer = None
          
          
          def convert_c4d_bitmap(doc: c4d.documents.BaseDocument, c4d_bitmap: c4d.BaseShader, remove=True):
              cr_bitmap = c4d.BaseShader(1036473)
              cr_bitmap_added = True
              if c4d_bitmap.GetUp():
                  parent = c4d_bitmap.GetUp()
                  cr_bitmap.InsertUnder(parent)
                  doc.AddUndo(c4d.UNDOTYPE_NEW, cr_bitmap)
          
                  # if parent is a layer shader
                  if parent.CheckType(c4d.Xlayer):
                      layer = get_layershader_layer(doc, parent, c4d_bitmap)
                      cr_bitmap[c4d.ID_BASELIST_NAME] = layer.GetName(doc)
                      doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, parent)
                      layer.SetParameter(c4d.LAYER_S_PARAM_SHADER_LINK, cr_bitmap)
                      
                  # if parent is another type shader (i.e. Fusion Shader)
                  else:
                      parent_shader_slot = get_bc_id(
                          parent.GetDataInstance(), c4d_bitmap)
                      doc.AddUndo(c4d.UNDOTYPE_CHANGE, parent)
                      parent[parent_shader_slot] = cr_bitmap
          
              # if parent is a material
              elif c4d_bitmap.GetMain():
                  parent = c4d_bitmap.GetMain()
                  doc.AddUndo(c4d.UNDOTYPE_CHANGE, parent)
                  parent.InsertShader(cr_bitmap)
                  doc.AddUndo(c4d.UNDOTYPE_NEW, cr_bitmap)
                  parent_shader_slot = get_bc_id(parent.GetDataInstance(), c4d_bitmap)
                  parent[parent_shader_slot] = cr_bitmap
              else:
                  cr_bitmap_added = False
          
              # Removing the old bitmap
              if remove and cr_bitmap_added:
                  doc.AddUndo(c4d.UNDOTYPE_DELETEOBJ, c4d_bitmap)
                  c4d_bitmap.Remove()
              return cr_bitmap
          
          
          def get_bc_id(target_bc, target_value):
              for index, value in target_bc:
                  if value == target_value:
                      return index
          
          
          

          Thank you

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

            Hey @John_Do,

            This is unfortunately not executable code but just a snippet, and I am always very hesitant to make claims about code I cannot run. The part which is not working I assume is this?

            # if parent is a layer shader
            if parent.CheckType(c4d.Xlayer):
                layer = get_layershader_layer(doc, parent, c4d_bitmap)
                cr_bitmap[c4d.ID_BASELIST_NAME] = layer.GetName(doc)
                doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, parent)
                layer.SetParameter(c4d.LAYER_S_PARAM_SHADER_LINK, cr_bitmap)
            

            The code looks correct. What springs to eye is that there is no StartUndo and EndUndo in your code. I assume they are outside of this snippet because you batch-process multiple materials. What also springs to eye is the cr_bitmap, which I assume stands for Corona bitmap. Have you tried what happens when replace all Corona materials/shaders with standard renderer ones? When I understand your code correctly, you are trying to link a Corona bitmap in a Standard renderer layer shader.

            You also type hinted your get_layershader_layer and treat its output as if it would be always LayerShaderLayer but its return type is actually LayerShaderLayer | None. Are you sure that there is no error in your code where layer is None and you try to treat it otherwise? You could also print out the return value of that SetParameter call.

            Cheers,
            Ferdinand

            MAXON SDK Specialist
            developers.maxon.net

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

              Hey @ferdinand ,

              Sorry, full code below !

              layer.SetParameter(...) returns True for each call as the assignation works. The code did not raise any error or exception. But if I undo/redo, bitmap shaders disappears, Corona ones as well as the previous Cinema ones. It seems that only Layer Shader's layers remain, but with a missing/broken shader link ?

              alt text

              # CT_C4DBitmapToCoronaBitmap
              
              import c4d
              
              
              def iter_shaders(material: c4d.BaseMaterial, mask: int = None) -> list:
              
                  shaders = []
              
                  shader = material.GetFirstShader()
              
                  if shader == None:
                      return
              
                  while shader:
              
                      if mask:
                          if shader.GetType() == mask:
                              shaders.append(shader)
                      else:
                          shaders.append(shader)
              
                      if shader.GetDown():
                          shader = shader.GetDown()
                      elif shader.GetNext():
                          shader = shader.GetNext()
                      else:
                          while shader.GetUp():
                              if shader.GetUp().GetNext():
                                  shader = shader.GetUp().GetNext()
                                  break
                              shader = shader.GetUp()
                          else:
                              shader = None
              
                  return shaders
              
              
              def get_layershader_layer(doc: c4d.documents.BaseDocument, layer_shader: c4d.BaseShader, target_shader: c4d.BaseShader = None) -> c4d.LayerShaderLayer:
                  # Iterating over Layer Shader layers, looking
                  # for the specified type of shaders
                  if layer_shader.GetType() == c4d.Xlayer:
                      layer = layer_shader.GetFirstLayer()
                      while layer != None:
                          if layer.GetType() == c4d.TypeShader:
                              shader = layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_LINK)
                              if target_shader and shader == target_shader:
                                  return layer
                              layer = layer.GetNext()
                          elif layer.GetNext():
                              layer = layer.GetNext()
                          else:
                              layer = None
              
              
              def convert_c4d_bitmap(doc: c4d.documents.BaseDocument, c4d_bitmap: c4d.BaseShader, remove=True):
                  cr_bitmap = create_cr_bitmap_shader(
                      c4d_bitmap[c4d.BITMAPSHADER_FILENAME], c4d_bitmap[c4d.BITMAPSHADER_COLORPROFILE], c4d_bitmap[c4d.ID_BASELIST_NAME], c4d_bitmap[c4d.BITMAPSHADER_EXPOSURE], c4d_bitmap)
                  cr_bitmap_added = True
                  if c4d_bitmap.GetUp():
                      parent = c4d_bitmap.GetUp()
                      cr_bitmap.InsertUnder(parent)
                      doc.AddUndo(c4d.UNDOTYPE_NEW, cr_bitmap)
                      if parent.CheckType(c4d.Xlayer):
                          layer = get_layershader_layer(doc, parent, c4d_bitmap)
                          cr_bitmap[c4d.ID_BASELIST_NAME] = layer.GetName(doc)
                          doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, parent)
                          layer.SetParameter(c4d.LAYER_S_PARAM_SHADER_LINK, cr_bitmap)
                      else:
                          parent_shader_slot = get_bc_id(
                              parent.GetDataInstance(), c4d_bitmap)
                          doc.AddUndo(c4d.UNDOTYPE_CHANGE, parent)
                          parent[parent_shader_slot] = cr_bitmap
                  elif c4d_bitmap.GetMain():
                      parent = c4d_bitmap.GetMain()
                      doc.AddUndo(c4d.UNDOTYPE_CHANGE, parent)
                      parent.InsertShader(cr_bitmap)
                      doc.AddUndo(c4d.UNDOTYPE_NEW, cr_bitmap)
                      parent_shader_slot = get_bc_id(parent.GetDataInstance(), c4d_bitmap)
                      parent[parent_shader_slot] = cr_bitmap
                  else:
                      cr_bitmap_added = False
              
                  # Removing the old bitmap
                  if remove and cr_bitmap_added:
                      doc.AddUndo(c4d.UNDOTYPE_DELETEOBJ, c4d_bitmap)
                      c4d_bitmap.Remove()
                  return cr_bitmap
              
              
              def create_cr_bitmap_shader(filepath: str, colorspc: int, basename: str, exposure: float, shader):
                  cr_bitmap = c4d.BaseShader(1036473)
                  cr_bitmap_bc = cr_bitmap.GetDataInstance()
                  cr_bitmap_bc.SetFilename(c4d.CORONA_BITMAP_FILENAME, filepath)
                  cr_bitmap_bc.SetInt32(c4d.CORONA_BITMAP_COLORPROFILE, colorspc)
                  cr_bitmap_bc.SetString(c4d.ID_BASELIST_NAME, basename)
                  cr_bitmap_bc.SetFloat(c4d.CORONA_BITMAP_EXPOSURE,
                                        exposure if colorspc in [1, 3] else exposure * 2.2)
                  return cr_bitmap
              
              
              def get_bc_id(target_bc, target_value):
                  for index, value in target_bc:
                      if value == target_value:
                          return index
              
              
              def main():
              
                  doc = c4d.documents.GetActiveDocument()
              
                  materials = doc.GetActiveMaterials()
              
                  doc.StartUndo()
              
                  # Main loop
                  for material in materials:
              
                      # Step over non Corona Physical materials or Corona Legacy materials.
                      if material.GetType() not in (1056306, 1032100):
                          continue
              
                      for bitmap_shader in iter_shaders(material, c4d.Xbitmap):
                          convert_c4d_bitmap(doc, bitmap_shader)
              
                  doc.EndUndo()
              
                  c4d.EventAdd()
              
              
              if __name__ == '__main__':
                  main()
              
              
              ferdinandF 1 Reply Last reply Reply Quote 0
              • ferdinandF
                ferdinand @John_Do
                last edited by ferdinand

                Hey @John_Do,

                Thank you for the full code. I did what I proposed above and removed Corona from the equation, replacing the corona bitmap shader with the standard one. The problem remains the same, so this is not a problem of Corona. I then went ahead and wrote a clean version of your script, removing all the fluff, but the problem remains. Once one replaces a shader layer in LayerShader, wrapping the action in an undo, the resulting undo stack will not go back to the original shader but an "empty" layer.

                3793fba5-059b-428e-8175-d9e5bc58bcbc-image.png
                69d94bdd-4646-4b6d-ad42-447b50005f9b-image.png
                9d60d539-9a2b-411e-b86c-640cf7e0b6a8-image.png
                Fig. 1: Top to bottom - (1) Initial state of the LayerShader, (2) after the script ran, (3) after pressing undo once, the shader is empty.

                Even though we correctly establish an undo-item for removing the original shader and setting the parameter, going back in the stack then seems to break the linkage. I will try to take a closer look at our code base this week, in the hopes of finding some magic sauce we are missing here.

                But your code is more or less correct. I also tried deliberately different orders of adding the new shader and removing the old one on the stack, with no luck. So, you should not look for a solution, as the solution is either very fringe or there is a bug/limitation in the LayerShader undo handling.

                Cheers,
                Ferdinand

                Code:

                # CT_C4DBitmapToCoronaBitmap
                
                import c4d
                import typing
                
                from mxutils import *
                
                doc: c4d.documents.BaseDocument # The active document.
                
                def iter_shaders(owner: c4d.BaseList2D, node: c4d.BaseList2D, 
                    mask: list[int] | None = None) -> typing.Iterator[tuple[c4d.BaseList2D, c4d.BaseShader]]:
                    """
                    """
                    if owner is None or node is None:
                        return
                    
                    CheckType(owner, c4d.BaseList2D)
                    CheckType(node, c4d.BaseList2D)
                    mask = [] if mask is None else CheckIterable(mask, int, list)
                
                    while node:
                        if isinstance(node, c4d.BaseShader) and node.GetType() in mask:
                            yield owner, node
                            
                        for data in iter_shaders(node, node.GetFirstShader(), mask):
                            yield data 
                                
                        for data in iter_shaders(node, node.GetDown(), mask):
                            yield data
                
                        node = node.GetNext()
                
                
                def replace_bitmap_shaders(material: c4d.BaseMaterial) -> None:
                    """
                    """
                    def get_layer(
                            layers: c4d.LayerShader, shader: c4d.BaseShader) -> c4d.LayerShaderLayer:
                        """
                        """
                        CheckType(shader, c4d.BaseShader)
                        current: c4d.LayerShaderLayer = CheckType(layers, c4d.LayerShader).GetFirstLayer()
                        while current:
                            if (current.GetType() == c4d.TypeShader and 
                                current.GetParameter(c4d.LAYER_S_PARAM_SHADER_LINK) == shader):
                                return current
                            current = current.GetNext()
                        
                        raise RuntimeError(f"Could not find layer for {shader} in {layers}.")
                
                    CheckType(material, c4d.BaseMaterial)
                    for owner, shader in iter_shaders(doc, material, mask=[c4d.Xbitmap]):
                        replacement: c4d.BaseShader = create_standard_bitmap_shader(shader)
                        replacement.InsertUnderLast(owner)
                        doc.AddUndo(c4d.UNDOTYPE_NEW, replacement)
                
                        if isinstance(owner, c4d.LayerShader):
                            layer: c4d.LayerShaderLayer = get_layer(owner, shader)
                            doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, owner)
                            layer.SetParameter(c4d.LAYER_S_PARAM_SHADER_LINK, replacement)
                
                        shader.Remove()
                        doc.AddUndo(c4d.UNDOTYPE_DELETEOBJ, shader)
                
                def create_standard_bitmap_shader(shader: c4d.BaseShader) -> c4d.BaseShader:
                    """
                    """
                    copy: c4d.BaseShader = CheckType(shader.GetClone(c4d.COPYFLAGS_NONE))
                    copy[c4d.BITMAPSHADER_FILENAME] = "asset:///file_fa9c42774dd05049"
                    return copy
                
                def main():
                    """
                    """
                    doc.StartUndo()
                    for material in doc.GetActiveMaterials():
                        replace_bitmap_shaders(material)
                
                    doc.EndUndo()
                    c4d.EventAdd()
                
                
                if __name__ == '__main__':
                    main()
                

                MAXON SDK Specialist
                developers.maxon.net

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

                  Hi @ferdinand ,

                  Thank you so much for taking the time to test all of this. Out of curiosity I tried your code on my side, and the same thing happen on native Cinema 4D materials.

                  Please let me know if you find the root cause of the bug, in the meantime I'll publish the script with all necessary warnings.

                  Have a nice day

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

                    Hey @John_Do,

                    Yes, I only ran this with Cinema 4D Standard Renderer shaders, so the problem is definitively with our code. And I will certainly report back here when I have a more concrete answer.

                    As a little warning, other than the options (1) "one must do a weird thing" and (2) "this is a bug", it could also be that we declare this an intended or accepted limitation of the Python layer. The interfaces of LayerShader are identical in Python and C++, but that does not mean that they did not take a shortcut under the hood.

                    I will have a look within this week, for now I have moved this topic to the bug forum.

                    Cheers,
                    Ferdinand

                    MAXON SDK Specialist
                    developers.maxon.net

                    1 Reply Last reply Reply Quote 0
                    • ferdinandF ferdinand moved this topic from Cinema 4D SDK on
                    • John_DoJ
                      John_Do
                      last edited by

                      Hi @ferdinand , have you got a chance to look at the issue from the Cinema 4D side ?

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

                        Hey @John_Do,

                        please excuse the wait, I now had a look. Unfortunately, we must classify this as an accepted/known limitation and I (at least currently) also cannot offer a workaround.

                        In the public Python and C++ API we have the types LayerShader and LayerShaderLayer. As with most types, we have internal counterparts for them, a backend in our private API, where the actual implementation happens. The problem is here, that this backend is barely used by ourselves, and internally we actually use a shader and layer class that comes directly from the legacy Smells Like Almonds (SLA) plugin system Maxon once bought and the whole layer shader came from. When a user interacts with the LayerShader custom GUI, that custom GUI actually uses these SLA types and not LayerShader and LayerShaderLayer. So, when a user is adding a layer to a LayerShader in the GUI, LayerShader::AddLayer is for example never called, as we use the a method on the internal SLA type instead.

                        I do not fully understand myself why the BaseLink is broken in this very special case, but since we operate here with this 'double structure', it would be way too much work to fix. We have therefore also declared this an accepted limitation.

                        Due to the fact that we operate here with a type which is not really used by ourselves, I also do not see a workaround for you.

                        Cheers,
                        Ferdinand

                        MAXON SDK Specialist
                        developers.maxon.net

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

                          Oof, that's an unfortunate end for me but at least I have a clear explanation. Thanks @ferdinand

                          1 Reply Last reply Reply Quote 0
                          • First post
                            Last post