Rebuild a scene with Python



  • 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 :/



  • 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/Cinema4DPythonSDK/html/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)
    


  • 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.



  • 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.



  • 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.



  • 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.



  • 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.


Log in to reply