Working with Scene Objects in pymxs

This topic is an introduction to creating and manipulating scene objects in 3ds Max using pymxs. This is an overview, and contains some examples. For more comprehensive information about all objects you can create and manipulate, see the Node: MAXWrapper group of topics (under 3ds Max Objects and Interfaces) in the MAXScript Help.

Objects in a 3ds Max scene include geometry, shapes, lights, cameras, helpers, spacewarps, and systems. They are organized in a tree structure called the scene graph, with the help of a node object, with a root node at the top. Each node can have one parent and multiple children. The Scene Explorer reflects this structure. Each scene graph node stores information about the object at that location in the scene graph, such as its name, position rotation and scale, shading, modifiers, visibility, layers, and so on. For more information, see Working with the Scene Graph.

Note:

Most examples in this topic are run from the MAXScript Listener window, which also has a Python commandline mode. This allows us to see results immediately in 3ds Max, and to query objects. Examples run in the listener are indicated with the Listener Python prompt >>>.

Creating a Scene Object

Objects are created with constructors. All of the properties of an object can optionally be set when it is constructed, and if they are omitted the object's defaults are used. For example, to create a teapot:

>>> my_teapot = pymxs.runtime.teapot()
>>> my_teapot
<Teapot<$Teapot:Teapot001 @ [0.000000,0.000000,0.000000]>>

This creates a new teapot object at position [0,0,0], with a radius of 25 and 4 segments. To change these parameters at creation time, we can specify them in the constructor:

>>> teapot_position=pymxs.runtime.point3(100,20,10)
>>> my_teapot = pymxs.runtime.teapot(radius=50, pos=teapot_position, segments=2)

We can now manipulate any of the object's properties. For example, to turn our teapot into a simple pot by removing its handle and spout:

>>> my_teapot.spout = False
>>> my_teapot.handle = False

All of the available properties for a teapot are documented in the Teapot: GeometryClass topic in the MAXScript Help.

We can also use any of the common properties, operators or methods exposed by the Node class. For example, we can query whether the object is a Shape (with isShapeObject()) and get the number of face and vertices (with getPolygonCount()):

>>> pymxs.runtime.isShapeObject(my_teapot)
False
>>> pymxs.runtime.getPolygonCount(my_teapot)
<Array<#(192, 98)>>

Modifying the Object

A modifier transforms an object. Most modifiers are "object modifiers" that are applied to objects and transform them in their own local space, and perform deformations such as bend, taper and twist.

A modifier takes an object and either modifies it directly, or creates a new object from the input object. Many modifiers only apply to certain characteristics (that is, channels) of an object such as the vertices or the faces.

Modifiers can be specific to certain types of object, for example, the Volume Select Modifier only works on triangle mesh objects (TriObject).

To modify our object from the previous example, we first create a new modifier, then set it's properties, and then add it to the object (though you can also add it to the object, and then set the properties).

>>> my_modifier = pymxs.runtime.taper()
>>> my_modifier.amount = 2.0
>>> my_modifier.curve = 1.5
>>> pymxs.runtime.addmodifier(my_teapot, my_modifier)

Editing a Mesh

There are two ways to work with a geometry object's mesh in pymxs/MAXScript: convert the object to a mesh object (such as Editable_Poly) using pymxs.runtime.convertToMesh() or convertToPoly(), or apply one of the mesh modifiers to the object (such as Edit_Poly). Collapsing an object with modifers applied to it also converts it to a mesh object, an Editable_Mesh (a tri-mesh) by default, or an Editable_Poly if Edit_Poly is in the modifier stack.

It is also possible to construct a mesh from scratch in pymxs/MAXScript. See the mesh_and_cpv.py sample for an example of how you could do this.

Transforming: Scale, Rotation and Position

There are three transforms related to scene nodes: the Node Transform Matrix (held in the object's .transform property), the Object-Offset Transform Matrix (held in the object's .objectoffsetpos, .objectoffsetrot, and .objectoffsetscale properties), and the Object Transform Matrix, which is the combination of the Node Transform and Object-Offset matrices. This is held in the object's .objecttransform property.

For a detailed discussion of how to access and manipulate these transforms, as well as the "shortcut" properties such as .pos and .rotation, see the "Using Node Transform Properties" topic in the MAXScript Help.

This example script is an adapation of the sample script in that topic adapted to use pymxs:

from pymxs import runtime as rt

# define function for coercing a rotation to a quat
rt.execute("fn r2q r = (return r as quat)")

def DumpXForms (obj):
    # output node transform properties
    print "{}:\t{}".format("transform", obj.transform)
    print "{}:\t{}".format("position ", obj.pos)
    print "{}:\t{}".format("rotation ", obj.rotation)
    # output node's pivot point location
    print "{}:\t{}".format("pivot ", obj.pivot)
    # output object offsets
    print "{}:\t{}".format("objectoffsetpos", obj.objectoffsetpos)
    print "{}:\t{}".format("objectoffsetrot", obj.objectoffsetrot)
    print "{}:\t{}".format("objectoffsetscale", obj.objectoffsetscale)
    # output object transform
    print "{}:\t{}".format("objecttransform ", obj.objecttransform)
    # output vertex position in local and world coordinates
    # we do this because 'in coordsys' is not available in pymxs
    rt.setRefCoordSys(rt.Name('local'))
    print "{}:\t{}".format("vert 1 (local) ", (rt.getvert( obj, 1)))
    rt.setRefCoordSys(rt.Name('world'))
    print "{}:\t{}".format("vert 1 (world1) ", (rt.getvert( obj, 1)))
    # calculate and output vertex position in world coordinates
    v_pos = rt.getvert( obj, 1) * obj.objecttransform
    print"{}:\t{}".format("vert 1 (world2) ", v_pos)



# define function for rotating only the pivot point
def RotatePivotOnly( obj, rotation):
    rotValInv=rt.inverse (rt.r2q(rotation))
    with pymxs.animate(False):
        obj.rotation*=rotValInv
        obj.objectoffsetpos*=rotValInv
        obj.objectoffsetrot*=rotValInv


b=rt.box(pos=rt.Point3(75,75,0)) # create a 25x25x25 box, vertex 1 at [62.5,62.5,0] (world)
rt.convertToMesh (b) # convert box to mesh so we can access the vertex location
DumpXForms(b) # print transforms
b.pivot= rt.Point3(50,50,0)# move pivot only to [50,50,0]
DumpXForms (b) # print transforms
rotation = rt.EulerAngles( 0, 0, 35)
RotatePivotOnly (b, rotation) # rotate pivot only 35 degrees about local Z
DumpXForms (b)# print transforms

Getting Custom Attributes

You can access custom attributes on an object by using pymxs.runtime.custattributes:

from pymxs import runtime as rt
print rt.custattributes.getpblockdefs(rt.custattributes.getdef(rt.getnodebyname("Sphere001"), 1))

Assuming $Sphere001 has a custom float attribute, the result would be:

#(#(#main, 0, 0, #(#rollout, #params), #(#myattr, #(#type, #float, #ui, #(#myattr), #default, 0.0))))