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


Name-US:Xpresso Camera Rig
Description-US:Creates an Xpresso Camera Rig

Creating User Data -
Python Xpresso -

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

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

    # refresh the Graph View

def main(doc):
    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.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_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.AddUndo(c4d.UNDOTYPE_NEW, focusObject)
    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


if __name__=='__main__':

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.


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

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,

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 =

    # Checks if the user canceled
    if filePath is None:

    # 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:

    # 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

    # 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

    # Push an event update to Cinema 4D

# Execute main()
if __name__=='__main__':


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,

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:
        if layer.GetName() == layerName:
    # 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:
        if layer.GetName() == layerName:
    # Finally delete all layer
    for layer in reversed(list(HierarchyIterator(doc.GetLayerObjectRoot().GetDown()))):
        if layer.GetName() == layerName:

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


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!

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.