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 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)
-
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:
- Load this document with your xpresso setup.
- Rename all cameras with what's the user defined as a name.
- 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?
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. -
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.
Cheers,
Maxime.