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

    Rebuild a scene with Python

    Cinema 4D SDK
    3
    9
    1.5k
    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.
    • W
      woodstar
      last edited by woodstar

      I've got a xpresso camera setup that I would love to turn into a simple script if possible.

      I just wondered if there's a way to rebuild a document purely through python so once the script is run, it asks the user for a camera name...Then it names everything based on that user input.

      Can you build a document containing xpresso purely from Python or are there limitations? I don't even know where to start 😕

      1 Reply Last reply Reply Quote 0
      • ?
        A Former User
        last edited by A Former User

        Hi @woodstar
        There's definitely a way! What did you want to connect in Xpresso with Python? I wrote a script that does some of what you described: it prompts the user for a camera name, then creates an Xpresso rig based on that. The rig connects a user data checkbox 'Follow Target' to a couple of properties in the rig (Target tag's 'Enable' and the camera's 'Use Target Object''s checkbox for setting the Focus Distance). That would allow you to follow an object for some of the camera move and then keyframe it off.

        Camera_Controls.png

        """
        Name-US:Xpresso Camera Rig
        Description-US:Creates an Xpresso Camera Rig
        author:blastframe
        
        credits:
        Creating User Data - https://www.cineversity.com/wiki/Python%3A_User_Data/
        Python Xpresso - https://www.cineversity.com/vidplaytut/xpresso_maker_overview
        """
        
        import c4d
        from c4d import gui
        
        def create_user_data_group(obj, name, parentGroup=None, columns=None, shortname=None):
            # see Creating User Data url above
            if obj is None: return False
            if shortname is None: shortname = name
            bc = c4d.GetCustomDatatypeDefault(c4d.DTYPE_GROUP)
            bc[c4d.DESC_NAME] = name
            bc[c4d.DESC_SHORT_NAME] = shortname
            bc[c4d.DESC_TITLEBAR] = 1
            if parentGroup is not None:
                bc[c4d.DESC_PARENTGROUP] = parentGroup
            if columns is not None:
                bc[22] = columns
            return obj.AddUserData(bc)
        
        def create_user_data_bool(obj, name, val=True, parentGroup=None):
            # see Creating User Data url above
            if obj is None: return False
            bc = c4d.GetCustomDatatypeDefault(c4d.DTYPE_BOOL)
            bc[c4d.DESC_NAME] = name
            bc[c4d.DESC_SHORT_NAME] = name
            bc[c4d.DESC_DEFAULT] = val
            bc[c4d.DESC_ANIMATE] = c4d.DESC_ANIMATE_ON
        
            if parentGroup is not None:
                bc[c4d.DESC_PARENTGROUP] = parentGroup
        
            element = obj.AddUserData(bc)
            obj[element] = val
            return element
        
        def connect_xpresso(camera,camera_truck_ctrl,targetTag):
            # Xpresso documentation:
            # https://developers.maxon.net/docs/py/2023_2/modules/c4d.modules/graphview/index.html
            xtag = camera_truck_ctrl.MakeTag(c4d.Texpresso) # create Xpresso tag
            doc.AddUndo(c4d.UNDOTYPE_NEW, xtag)
        
            # A Graph View Node Master stores a collection of Graph View Nodes.
            gv = xtag.GetNodeMaster()
        
            # create node for the camera truck control
            camera_truck_ctrlNode = gv.CreateNode(parent=gv.GetRoot(), id=c4d.ID_OPERATOR_OBJECT, insert=None, x=100, y=0)
            doc.AddUndo(c4d.UNDOTYPE_NEW, camera_truck_ctrlNode)
        
            doc.AddUndo(c4d.UNDOTYPE_CHANGE, camera_truck_ctrlNode)
            udPort = None
            # create output port for the user data boolean (from Rick Barrett script)
            for id, bc in camera_truck_ctrl.GetUserDataContainer():
                if bc[c4d.DESC_CUSTOMGUI] is not None and \
                bc[c4d.DESC_CUSTOMGUI] is not c4d.CUSTOMGUI_SEPARATOR:
                    doc.AddUndo(c4d.UNDOTYPE_CHANGE,camera_truck_ctrlNode)
                    udPort = camera_truck_ctrlNode.AddPort(c4d.GV_PORT_OUTPUT, id)
        
            # create node for the camera Target tag
            targetNode = gv.CreateNode(gv.GetRoot(), c4d.ID_OPERATOR_OBJECT, insert=None, x=300, y=0)
            # get DescID for Target tag's Enable property
            targetEnabled = c4d.DescID(c4d.DescLevel(c4d.EXPRESSION_ENABLE));
        
            doc.AddUndo(c4d.UNDOTYPE_NEW, targetNode)
            # after creating node we need to give it an object
            targetNode[c4d.GV_OBJECT_OBJECT_ID] = targetTag
            # add Target node Enabled input port
            enablePort = targetNode.AddPort(c4d.GV_PORT_INPUT, targetEnabled)
        
            # create node for the camera
            cameraNode = gv.CreateNode(parent=gv.GetRoot(), id=c4d.ID_OPERATOR_OBJECT, insert=None, x=300, y=100)
            # after creating node we need to give it an object
            cameraNode[c4d.GV_OBJECT_OBJECT_ID] = camera
            # add camera input port for using the Target tag's object as the Focus Object
            useTargetObjectPort = cameraNode.AddPort(c4d.GV_PORT_INPUT, c4d.CAMERAOBJECT_USETARGETOBJECT)
        
            # connect the ports
            udPort.Connect(enablePort)
            udPort.Connect(useTargetObjectPort)
        
            # refresh the Graph View
            c4d.modules.graphview.RedrawMaster(gv)
        
        def main(doc):
            doc.StartUndo()
            camera_truck_ctrl = c4d.BaseObject(c4d.Osplinenside) # Create camera_truck_ctrl control
            camera_truck_ctrl[c4d.ID_BASEOBJECT_USECOLOR] = 2 # turn on display color
            camera_truck_ctrl[c4d.ID_BASEOBJECT_COLOR] = c4d.Vector(0,1,1) # set display color
            doc.InsertObject(camera_truck_ctrl)
            doc.AddUndo(c4d.UNDOTYPE_NEW, camera_truck_ctrl)
            cameraControls = create_user_data_group(camera_truck_ctrl,"Camera Controls",c4d.DescID(0)) # create user data group
            followTargetBool = create_user_data_bool(camera_truck_ctrl,"Follow Target",True,cameraControls) # Follow Target boolean checkbox
            camera = c4d.BaseObject(c4d.Ocamera) # Create camera
            name = gui.InputDialog("What would you like to name your camera?", "Camera") # prompt user for name
            camera.SetName(name)
            camera_truck_ctrl.SetName("%s_con+"%name) # use camera name to name control
        
            targetTag = camera.MakeTag(c4d.Ttargetexpression) # create Target tag
            doc.AddUndo(c4d.UNDOTYPE_NEW, targetTag)
        
            focusObject = c4d.BaseObject(c4d.Onull) # Create focus object
            focusObject.SetName("%s Target"%name)
        
            focusObject[c4d.ID_BASEOBJECT_USECOLOR] = 2 # turn on display color
            focusObject[c4d.ID_BASEOBJECT_COLOR] = c4d.Vector(1,0,0) # set display color
            focusObject[c4d.ID_BASEOBJECT_REL_POSITION,c4d.VECTOR_Z]  = 500 # set focus object's position Z to 500
            focusObject[c4d.NULLOBJECT_DISPLAY] = 13  # set null target to display as sphere
            focusObject[c4d.NULLOBJECT_RADIUS] = 30 # set sphere radius to 30
            focusObject[c4d.NULLOBJECT_ORIENTATION] = 1 # set sphere plane to XY
            targetTag[c4d.TARGETEXPRESSIONTAG_LINK] = focusObject # assign focus object to Target tag object link
        
            # insert objects to document
            doc.InsertObject(focusObject)
            doc.AddUndo(c4d.UNDOTYPE_NEW, focusObject)
            doc.InsertObject(camera_truck_ctrl)
            doc.AddUndo(c4d.UNDOTYPE_NEW, camera_truck_ctrl)
            camera.InsertUnder(camera_truck_ctrl) # parent to camera_truck_ctrl
            doc.AddUndo(c4d.UNDOTYPE_NEW, camera)
        
            connect_xpresso(camera,camera_truck_ctrl,targetTag) # create Xpresso connections
            doc.SetActiveObject(camera_truck_ctrl,c4d.SELECTION_NEW) # select camera truck control
        
            c4d.EventAdd()
            doc.EndUndo()
        
        if __name__=='__main__':
            main(doc)
        
        1 Reply Last reply Reply Quote 1
        • M
          m_adam
          last edited by

          Hi @woodstar

          just to be sure we are on the same track. You want to have a file with an xpresso setup. Then the Python script should:

          1. Load this document with your xpresso setup.
          2. Rename all cameras with what's the user defined as a name.
          3. Merge this document with the active(selected) document.

          If it's correct then yes it's possible to do in Python. Please just confirm and if it's correct I will guide you on how to achieve each step.

          Cheers,
          Maxime.

          MAXON SDK Specialist

          Development Blog, MAXON Registered Developer

          W 1 Reply Last reply Reply Quote 1
          • W
            woodstar @m_adam
            last edited by

            Thanks for the fast replies didn't check till I got home!

            @blastframe Thanks man will definitely play with that code!

            @m_adam Thank you so much...That's definitely correct, just so every time I press the button for script it imports the preset with a different name(based on user input) for camera so that it creates a unique layer without merging into current layers...

            I'm going to add the C4D file with xpresso setup...It's alot of simple stuff but it helps when timing animation, it includes textures so you can see what I'm trying to achieve..

            As you probably know if I copy and paste the camera to another file it destroys my layer structure so I have to change the camera name in the scene and save it before merging the file...Otherwise it merges layers with the same name...Very long just to create a new camera! It's rendering me unable to use this setup on tight deadlines.

            If I could change the camera name on import it would create a unique layer SH_001, SH_002, SH_003 (Up to user) etc and the xpresso setup would stay intact for each camera created...

            This is hopefully the start of something I can build on regarding camera scripts, if I can grasp the basic process of generating presets and using python to modify them slightly...

            https://www.dropbox.com/s/pm53s6gvjszunba/MAW_Cam.zip

            Would be awesome and really appreciate you taking the time to help!

            Please let me know if this is too complex to recreate? As long as I know how to create layers assign names from user input etc

            Many thanks,
            Mike.

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

              Note that MerdeDocument only accepts another document file name, so there is no built-in way to merge two already loaded document. If saving a temporary document is a big issue, you can manually iterate over all objects/Material and copy them in the new object, but it's a lot of work.

              import c4d
              import os
              
              
              # Main function
              def main():
                  # Asks for the path of the c4d file to load
                  filePath = c4d.storage.LoadDialog()
              
                  # Checks if the user canceled
                  if filePath is None:
                      return
              
                  # Asks fot the new name
                  newName = c4d.gui.InputDialog("Enter a new name", "The new camera name")
              
                  # Checks if the user canceled or user-entered nothing
                  if not newName:
                      return
              
                  # Loads the documents only in memory
                  tempoDoc = c4d.documents.LoadDocument(filePath, c4d.SCENEFILTER_OBJECTS | c4d.SCENEFILTER_MATERIALS, None)
                  if tempoDoc is None:
                      raise RuntimeError("Failed to load the document.")
              
                  # Finds the camera named "SH_001" and the attached Layer
                  cameraObj = tempoDoc.SearchObject("SH_001")
                  if cameraObj is None:
                      raise RuntimeError("Failed to retrieve the camera to rename in the document.")
              
                  layer = cameraObj[c4d.ID_LAYER_LINK]
                  if cameraObj is None:
                      raise RuntimeError("Failed to retrieve the layer attached to the camera.")
              
                  # Renames both
                  cameraObj.SetName(newName)
                  layer.SetName(newName)
              
                  # Saves this file (temporary)
                  tempoFile = os.path.join(os.path.dirname(filePath), "tempoFile.c4d")
                  c4d.documents.SaveDocument(tempoDoc, tempoFile, c4d.SAVEDOCUMENTFLAGS_DONTADDTORECENTLIST, c4d.FORMAT_C4DEXPORT)
              
                  # Merges the current document with the tempo file
                  c4d.documents.MergeDocument(doc, tempoFile, c4d.SCENEFILTER_OBJECTS | c4d.SCENEFILTER_MATERIALS, None)
              
                  # Deletes the temporary file
                  os.remove(tempoFile)
              
                  # Push an event update to Cinema 4D
                  c4d.EventAdd()
              
              # Execute main()
              if __name__=='__main__':
                  main()
              

              Cheers,
              Maxime.

              MAXON SDK Specialist

              Development Blog, MAXON Registered Developer

              1 Reply Last reply Reply Quote 2
              • W
                woodstar
                last edited by

                Wow amazing, works exactly how I expected! I can also see how this could work in so many instances if you can actually pass a name through to a file which then xpresso is used to pipe through that new name.

                I'm going to thoroughly read this to try to understand exactly what's going on, however I've noticed that it brings additional default layer and creates a duplicate of materials...Is there a way to check if materials exist and replace, almost like how alt dragging works when doing it manually?

                alt text

                In my head this is super complex as you would have to also reassign materials after to the newly imported camera...But maybe you know a way that could do this without much complexity?

                Thanks for the help really appreciated and it's much easier to learn when it's an idea of your own as you can really break down the method.

                Many thanks,
                Mike.

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

                  Hi @woodstar sorry for the delay, unfortunately, there is no way to prevent that. If you don't want "the default" layer to be in, you have to delete it from your source file. Or if you want to keep it, simply delete it, in the temporary document when you also rename stuff. Find an example below you would need to call DeleteContentAndLayerByName(tempoDoc, "Default") just after SetName.

                  def HierarchyIterator(obj):
                      """
                      A Generator to iterate over the Hierarchy
                      :param obj: The starting object of the generator (will be the first result)
                      :return: All objects under and next of the `obj`
                      """
                      while obj:
                          yield obj
                          for opChild in HierarchyIterator(obj.GetDown()):
                              yield opChild
                          obj = obj.GetNext()    
                  
                  def DeleteContentAndLayerByName(doc, layerName):
                      # Iterates all objects to removes the one with the assigned Layer.
                      # Since we remove we need to reverse the lsit to not have issue
                      for obj in reversed(list(HierarchyIterator(doc.GetFirstObject()))):
                          layer = obj[c4d.ID_LAYER_LINK]
                          if layer is None:
                              continue
                          
                          if layer.GetName() == layerName:
                              obj.Remove()
                              
                      # Iterates all materials to removes the one with the assigned Layer.
                      # Since we remove we need to reverse the lsit to not have issue
                      for obj in reversed(list(HierarchyIterator(doc.GetFirstMaterial()))):
                          layer = obj[c4d.ID_LAYER_LINK]
                          if layer is None:
                              continue
                          
                          if layer.GetName() == layerName:
                              obj.Remove()
                              
                      # Finally delete all layer
                      for layer in reversed(list(HierarchyIterator(doc.GetLayerObjectRoot().GetDown()))):
                          if layer.GetName() == layerName:
                              layer.Remove()
                  

                  Regarding duplicate entry, unfortunately, there is nothing built-in to avoid that, you would have to copy everything manually if needed.

                  Cheers,
                  Maxime.

                  MAXON SDK Specialist

                  Development Blog, MAXON Registered Developer

                  1 Reply Last reply Reply Quote 0
                  • W
                    woodstar
                    last edited by

                    Hey @m_adam thanks alot for the script and advice!

                    I think this is definitely what puts me off with Python, the limitations of it are quite obvious even to a novice. I guess the challenge is to try to push it as far as you can and almost make a lot of compromises along the way.

                    Thanks again!

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

                      Hey, I understand your frustration unfortunately here Python is not to blame but Cinema 4D API in general since Python is only a subset of the C++ API and you have the same limitation (in your case) in C++.

                      But yes we try to improve.
                      Cheers,
                      Maxime.

                      MAXON SDK Specialist

                      Development Blog, MAXON Registered Developer

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