P4D102 - Act on your Selected Objects
Now that we have had a short glimpse on the basics in the first article lets move on to do something meaningful. So far it has been a purely academic discourse.
Usually, we want a script to do something with the objects we have in a scene. So the first step is to look at how we can access the scene file and go through selected objects and perform Cinema 4D commands on them.
Accessing the Cinema 4D Scene
Let’s start with the basics. We import the c4d module and check if we
are running as a script within Cinema 4D (remember:
"__main__" in this case, so we check for that value):
import c4d if __name__ == "__main__": pass
The pass statement does nothing. We use it as placeholder for code we will now place inside the if block. Without it, Python would complain as it expects a statement within if.
When looking through the Cinema 4D Python documentation we find the c4d.documents module that has all the functionality to access our scene. The function GetActiveDocument() function returns the currently active scene as a BaseDocument object.
Now that we know how to access the scene we are left with finding out how to get a list of the selected objects in the scene. Looking at BaseDocument we see that it has a GetActiveObjects() method that returns a list of BaseObject objects.
Now let us implement this:
import c4d if __name__ == "__main__": doc = c4d.documents.GetActiveDocument() selected = doc.GetActiveObjects(0)
So far so good. We fetched the scene using GetActiveDocument() and assigned it to the variable doc so we can reference it later when needed. Then we stored the list of selected objects in the variable selected (NB: other than in the documentation, GetActiveObjects() requires a parameter. 0 works fine for me so I use that).
Now here is something new. What is a list in Python?
Python lists are very versatile and are really powerful and actually
easy to use (at least compared to other programming languages like C++).
I won’t go into much detail on lists here for now. Read up on the
if you want to learn more about them. For now it
is sufficient to learn that lists contain several objects. We can access
them through an index like
a = list. This would access the third
object in the list as they start at index 0 (zero).
For our script we would need to know if there is an object in the list, ie. if there have been objects selected. Python has a build-in function to see how many items a list contains: len(). So len(list) returns the number of objects in the list. Very convenient! So we check if there are objects n the list and then we “go through” them and do “something”.
Traversing Selected Objects
The question is, how do we “go through” the list? This also is quite easy in Python. Say hello to the for statement: Python’s for statement iterates over the items of a sequence (like a list or string). in the order they appear.
for ob in selected: pass
This will “go through” all items in the selected list. Within the for statement we can access the individual item through the ob variable we used in the for statement.
So we make something simple and let our script output all the selected objects’ names to the Python console as a test:
import c4d if __name__ == "__main__": doc = c4d.documents.GetActiveDocument() selected = doc.GetActiveObjects(0) if len(selected) > 0: # we have one or more selected objects in the scene for ob in selected: print ob.GetName()
Now we are getting somewhere!
Collapse Extrude Nurbs Script
Over at the CG Society forum there was a request for a script that collapses Extrude Nurbs to polygon objects. Let us use this as an example.
So what do we want the script to do?
- Go through all selected objects
- Check if it is an Extrude Nurbs object
- Make it a single polygon object
Sound simple enough for a start without being too simple and without purpose.
We already nailed the first step: Go through all selected objects. Done ;)
Checking for an Object Type
So how do we check if it is an Extrude Nurbs object? We could check for the name. By default Cinema 4D calls them “Extrude NURBS”. Doing that would quickly fail though as users can change the name. Looking at the BaseObject class that represents all objects in a Cinema 4D scene we don’t find much useful though.
Now here is a “tricky” thing with classes: They inherit the properties of their ancestors. Looking at the c4d module documentation we see that BaseObject is a descendant of BaseList2D which is a descendant of GeListNode which again is a descendant of C4DAtom. Looking through all those classes we see that C4DAtom has a method GetType() which returns a number containing the type ID of the object. Looking further we discover that BaseList2D has a method of GetTypeName(). We can use either to check if the object is an Extrude Nurbs.
As we do not know the type ID or the type name of an Extrude Nurbs we can use our basic script to give us those values.
import c4d if __name__ == "__main__": doc = c4d.documents.GetActiveDocument() selected = doc.GetActiveObjects(0) if len(selected) > 0: # we have one or more selected objects in the scene for ob in selected: print ob.GetName(), print "is an <" + ob.GetTypeName() + ">", print "ID: " + str(ob.GetType())
Printing the object’s name, type name and type ID to the Python console will show us the values we need for our script. Using a comma (,) after the last object with the print statement will prevent a new line so the output for each object is on one line.
The output should be something like this:
Sphere is an <Sphere> ID: 5160 Extrude NURBS is an <Extrude NURBS> ID: 5116 Cube is an <Cube> ID: 5159
So now that we know both the type name (within the angle <> brackets) and ID for an Extrude Nurbs. By creating an object, selecting it and running the script above we now can determine the values for type ID and type name of any object.
Using these values we can check if objects are of a certain kind:
import c4d if __name__ == "__main__": doc = c4d.documents.GetActiveDocument() selected = doc.GetActiveObjects(0) if len(selected) > 0: # we have one or more selected objects in the scene for ob in selected: if ob.GetTypeName() == "Extrude NURBS": print "Yeeehaaaw" else: print "Drat"
Step 2 - check if it is an Extrude Nurbs - done!
Performing Commands on Objects in a Python Script
Now that we can traverse the selected objects in a scene and know when we encounter an object of a specific type (in our case a Extrude Nurbs) we need to perform some action on them. In this example we want to convert the Extrude Nurbs in a polygon object and have the front and back cap connected with the body (by default, the caps are disconnect from the extruded polygons).
At first, we see what steps would be involved when doing it manually:
- select the object
- perform “Make Editable”
- select all created children
- connect the objects and delete the originals
- use “optimize” to connect the caps to the body
To perform these steps we could write the instructions one after the other within the if-statement. This would result in ugly “spaghetti-code” though and we could use the code we have so far for other purposes as well. Last time we saw how a function is defined in Python so we will make use of that and put the code that performs all the above steps into a function.
def MakeExtrudeEditable(doc,op): pass
This is the body of our function. We give it the name MakeExtrudeEditable. We define two parameters that we pass from the outside. A reference to the current scene (doc) and a reference to the object we want to convert to a polygon object (op). pass is a placeholder for now that does nothing but Python requires a statement within the function.
The whole script looks like this:
import c4d def MakeExtrudeEditable(doc,op): pass if __name__ == "__main__": doc = c4d.documents.GetActiveDocument() selected = doc.GetActiveObjects(0) if len(selected) > 0: # we have one or more selected objects in the scene for ob in selected: if ob.GetTypeName() == "Extrude NURBS": MakeExtrudeEditable(doc,ob)
We need to define our function before the main code so that it is known to Python once we call it from there. Instead of the print-statement from before we now call our own function within the inner most if-statement once we are sure the object is an Extrude Nurbs.
Now for the action. As the list of selected objects might contain several objects we need to make sure the object in question is the only one selected. So we need to select the object. The BaseDocument class provides a method called SetActiveObject that has two parameters: the object and the mode. By default the mode is set to SELECTION_NEW which sets the selection to the object used as first parameter. An existing selection will be gone. That’s exactly what we want. So we call the method using the default parameter, ie. we omit the second parameter mode.
def MakeExtrudeEditable(op,doc): doc.SetActiveObject(op)
To perform a command that is called from a menu or icon in Cinema 4D the
c4d module provides the
CallCommand() function. It expects an ID that
represents the command. To find out that ID we need to open the Command
Manager in Cinema 4D. Using the name filter we can search for the “Make
Editable” command. Once we select it from the list of available commands
the Command Manager shows the ID at the bottom where we can assign
custom shortcuts. This is the number we need to use with the
The commands we need to perform in sequence are: Make Editable which converts the Extrude Nurbs in a hierarchy of separate polygon objects with the topmost selected. To connect these objects we need to select them: using the Select Children command we add the two child objects to the already selected parent polygon object. A last we use Connect+Delete to create a single polygon object and get rid of the separate parts.
def MakeExtrudeEditable(op,doc): doc.SetActiveObject(op) c4d.CallCommand(12236) # Make Editable c4d.CallCommand(100004768) # Select Children c4d.CallCommand(16768) # Connect+Delete
After performing all those steps we end up with a single polygon object that is selected. The front and rear cap are still disconnected from the body though.
One disadvantage of the CallCommand() in Cinema 4D is the way it records separate Undo steps for each call to the function. This makes it impossible to have a single Undo for the whole script as the separate steps are recorded to the Undo stack individually.
For the next step - Optimize - we switch from using
CallCommand() to the
CallCommand() we are
limited to simple functions that do not require settings for the tool.
Optimize has several options though that we cannot set using
SendModelingCommand() requires several parameters. Beside the
command itself it expects a list of objects and an optional
BaseContainer object with the settings.
Here is the script:
def MakeExtrudeEditable(doc,op) : doc.SetActiveObject(op) c4d.CallCommand(12236) # Make Editable c4d.CallCommand(100004768) # Select Children c4d.CallCommand(16768) # Connect+Delete ob = doc.GetActiveObject() # store the active object for later use if ob == None: return bc = c4d.BaseContainer() # create a base container to hold the settings if bc == None: return bc.SetData(c4d.MDATA_OPTIMIZE_POLYGONS, True) # remove 1 or 2 point polygons bc.SetData(c4d.MDATA_OPTIMIZE_UNUSEDPOINTS, True) # remove unused points bc.SetData(c4d.MDATA_OPTIMIZE_POINTS, True) # remove double points bc.SetData(c4d.MDATA_OPTIMIZE_TOLERANCE, 0.5) # tolerance doc.AddUndo( c4d.UNDOTYPE_CHANGE_NOCHILDREN, ob ) c4d.utils.SendModelingCommand(c4d.MCOMMAND_OPTIMIZE, [ob], c4d.MODIFY_ALL, bc, doc) return
First, we store the selected object after the Connect+Delete command for later use. We make sure that we have an selected object. If that is not the case we exit for now using return.
For the SendModelingCommand() function we need some settings. So we create an object of the type BaseContainer(). If that fails (ie. bc == None) we stop and exit the function again. If everything worked we continue.
We need to set all the parameters for the Optimize command by filling the BaseContainer bc with the according values using SetData(). We want to remove 1 and 2 point polygons, unused points, double points and merge all points within a certain tolerance (eg. 0.5 units). You can find the parameter names from the Python documentation. For some parameters you need to check out the C++ SDK documentation though.
To add Optimize to the Undo Stack we call the document’s AddUndo method. To make this work we need to add a StartUndo() and EndUndo() call outside our for-Loop also.
So here is the complete script:
import c4d # ======================================================================= def MakeExtrudeEditable(doc,op) : doc.SetActiveObject(op) c4d.CallCommand(12236) # Make Editable c4d.CallCommand(100004768) # Select Children c4d.CallCommand(16768) # Connect+Delete ob = doc.GetActiveObject() if ob == None: return bc = c4d.BaseContainer() if bc == None: return bc.SetData(c4d.MDATA_OPTIMIZE_POLYGONS, True) # remove 1 or 2 point polygons bc.SetData(c4d.MDATA_OPTIMIZE_UNUSEDPOINTS, True) # remove unused points bc.SetData(c4d.MDATA_OPTIMIZE_POINTS, True) # remove double points bc.SetData(c4d.MDATA_OPTIMIZE_TOLERANCE, 0.5) # tolerance doc.AddUndo( c4d.UNDOTYPE_CHANGE_NOCHILDREN, ob ) c4d.utils.SendModelingCommand(c4d.MCOMMAND_OPTIMIZE, [ob],c4d.MODIFY_ALL, bc, doc) return # ======================================================================= if __name__ == "__main__": doc = c4d.documents.GetActiveDocument() selected = doc.GetActiveObjects(0) if len(selected) > 0 : c4d.StopAllThreads() doc.StartUndo() for ob in selected: if( ob.GetTypeName() == "Extrude NURBS" ) : MakeExtrudeEditable(doc,ob) doc.EndUndo() c4d.DrawViews(c4d.DRAWFLAGS_NO_ANIMATION)
As you can see we need to understand a few principles of Python and the internal workings of Cinema 4D to write a script. On the other hand it is not really hard once we overcome that hurdle. In the future it should be straight forward to replace our MakeExtrudeEditable() function with a function that performs different steps. The main part of getting all selected objects and traversing them to perform the desired steps remains the same. Taking a couple of minutes to think about a script and writing it certainly beats doing the same tedious task over and over again throughout a work day or even over weeks and months.
Putting it all together writing this simple script required to learn few principles:
- how to interact with the scene (the document) through BaseDocument class
- how to get the selected objects in a scene using GetActiveObjects
- using the for statement to go through that list
- writing our own function to capsulate the steps we want to perform on the objects
- using CallCommand() to use menu commands in our script
- using SendModelingCommand() to use more complex commands that require parameters
- a brief glimpse on how to use Undo()
Do you have any question? Drop me a note on Twitter @martinweber