Working with nested properties, such as controllers, in pymxs requires a different approach than in MAXScript. MAXScript "knows" about nested object properties, and does some extra parsing when it encounters them. Python and MAXScript treat properties indicated with dot notation differently: Python always returns a reference to the indicated property, while MAXScript returns a copy of the value. Attempting to assign a value may not work as intended in pymxs.
box.controller.position
will work, because controller is a reference.For example, in MAXScript you can do this:
b = box()
b.pos.x = 20
The equivalent in pymxs does not work as intended, because b.pos.x
is returning a copy of the pos object, which is no longer connected to the box's position.
There are three solutions to this problem:
getProperty()
and setProperty()
functionsgetmxsprop()
and setmxsprop()
functionsFor example:
from pymxs import runtime as mxs
b = mxs.box()
b.pos.x = 9 # trying to set the x value of the box position, but we got a copy of the position
print(b.pos.x) # see, we are still at pos.x == 0
# solution: use MXSWrapper.setmxsprop()
b.setmxsprop("pos.x", 9.0) # properly set the nested property value
print(b.pos.x) # the x value of the box position is now at 9
# OR use MAXScript setProperty()
mxs.setProperty(b, "pos.x", 10.0)
print(mxs.getProperty(b, "pos.x"))
# you can also do this:
b_pos = b.pos
b_pos.x = 10 # set the copied value from the box property
b.pos = b_pos # properly apply back the changed pos value to the box
print(b.pos.x) # the x value of the box position is now at 10
# getting the position:
print(b.pos.x) # 10.0 - fetches the proper value, but it is a copy of the position
print(b.getmxsprop("pos.x")) # 10.0 - fetches the proper value
print(mxs.getProperty(b, "pos.x")) # 10.0 - also works
Here is an example of some MAXScript code:
myTeapot = teapot()
myTeapot.pos.controller = noise_position()
In MAXScript, as in Python, myTeapot.pos
is a Point3, which doesn't have an associated controller property. Attempting to get myTeapot.pos.controller
in Python results in an error such as: AttributeError: 'pymxs.MXSWrapperBase' object has no attribute 'controller'
.
But in MAXScript myTeapot.pos.controller
is perfectly valid, as it evaluates to an object structure hierarchy. Therefore, to achieve the same thing using pymxs, we must use a different approach and use getPropertyController()
/ setPropertyController()
.
Here's a complete example where we set the Position XYZ controller of a box, and the X component of the position for a sphere:
from pymxs import runtime as rt
# set the transform -> position controller
t = rt.teapot()
rt.setPropertyController( t.controller, 'Position', rt.noise_position())
print(rt.getPropertyController(t.controller, 'Position'))
# set the transform -> position -> x_position controller
s = rt.sphere()
sphr_pos_ctrl = rt.getPropertyController(s.controller, 'Position')
rt.setPropertyController(sphr_pos_ctrl, 'X Position', rt.noise_float())
print(rt.getPropertyController(sphr_pos_ctrl, 'X Position'))
Output:
Controller:Noise_Position
Controller:Noise_Float
Expression controllers are created and set in the same way, though they require the extra steps of adding an expression and (if required) scalar and vector targets. In this example, we add an expression controller to the X Position sub-controller of an object's position controller, so that its X position always matches a target object.
from pymxs import runtime as rt
# target:
s = rt.sphere()
t = rt.teapot(pos=rt.point3(20,20,0))
s_pos_ctl = rt.getPropertyController(s.controller, 'Position')
expr_pos_ctl = rt.position_expression()
# first add the scalar variable targeting the sphere's X position
expr_pos_ctl.addScalarTarget('sphere_x', rt.getPropertyController(s_pos_ctl, 'X Position'))
# then add the expression that uses that variable
expr_pos_ctl.setExpression('[sphere_x, 20,0]')
# finally, set the controller
rt.setPropertyController(t.controller, "Position", expr_pos_ctl)
The transform properties of an object has default controllers, and can be accessed and modified as explained above. Adding a controller to a property that does not currently have one with pymxs is not as straightforward, because there is no name associated with the property's controller.
3ds Max stores all the controllers for properties for an object in a structure called a subanim (sub-animatable). A subanim can contain nested subanims. To assign a controller to a property that does not have a controller, we need to find the property's location in the subanim tree structure by index, and assign the controller to the result. We can do this using the MAXScript function getSubanim()
.
For example, to assign a float controller to an object's width property:
from pymxs import runtime as mxs
b = mxs.box()
bw= mxs.getSubAnim(mxs.getSubAnim(b, 4), 2)
mxs.setProperty(bw, 'controller', mxs.bezier_float())
This gets the box node's fourth subAnim (the box object), and then gets the second subAnim (the width property).
Note that the getSubAnim()
function gets the subAnim, while getPropertyController()
(illustrated above) gets the controller for the subAnim. So to expand the previous example:
from pymxs import runtime as mxs
b = mxs.box()
# get the subAnim for X_Rotation:
brx = mxs.getSubAnim(mxs.getSubAnim(mxs.getSubAnim(b, 3), 2), 1)
# get the controller for X_Rotation:
brxc = mxs.getPropertyController(mxs.getPropertyController(b.controller, 'Rotation'), 'X Rotation')
brx.controller == brxc # this is True
This example illustrates visiting all the subAnims in an object's subAnim tree, and prints out the corresponding names and indices. It can be used to determine the correct indices to use to assign new controllers:
from pymxs import runtime as mxs
b = mxs.box()
for sub_i in range(b.numsubs):
sub = mxs.getSubAnim(b, sub_i+1)
print(f'{sub.name} [{sub_i+1}]')
if sub.controller == None:
print(' No controller assigned to it')
else:
print(f' current controller: {(sub.controller)}')
if sub.keys == None:
print(' No keys assigned to it')
for secsub_i in range(sub.numsubs):
secsub = mxs.getSubAnim(sub, secsub_i+1)
print(f'\t{secsub.name} [{secsub_i+1}]')
if secsub.controller == None:
print('\t No controller assigned to it')
else:
print(f'\t current controller: {(secsub.controller)}')
if secsub.keys == None:
print('\t No keys assigned to it')
for thirdsub_i in range(secsub.numsubs):
thirdsub = mxs.getSubAnim(secsub, thirdsub_i+1)
print(f'\t\t{thirdsub.name} [{thirdsub_i+1}]')
if secsub.controller == None:
print('\t No controller assigned to it')
else:
print(f'\t\t current controller: {(thirdsub.controller)}')
This displays the following output:
Visibility [1]
No controller assigned to it
No keys assigned to it
Space Warps [2]
No controller assigned to it
No keys assigned to it
Transform [3]
current controller: Controller:Position_Rotation_Scale
Position [1]
current controller: Controller:Position_XYZ
X Position [1]
current controller: Controller:Bezier_Float
Y Position [2]
current controller: Controller:Bezier_Float
Z Position [3]
current controller: Controller:Bezier_Float
Rotation [2]
current controller: Controller:Euler_XYZ
X Rotation [1]
current controller: Controller:Bezier_Float
Y Rotation [2]
current controller: Controller:Bezier_Float
Z Rotation [3]
current controller: Controller:Bezier_Float
Scale [3]
current controller: Controller:Bezier_Scale
Object (Box) [4]
No controller assigned to it
No keys assigned to it
Length [1]
No controller assigned to it
No keys assigned to it
Width [2]
No controller assigned to it
No keys assigned to it
Height [3]
No controller assigned to it
No keys assigned to it
Width Segments [4]
No controller assigned to it
No keys assigned to it
Length Segments [5]
No controller assigned to it
No keys assigned to it
Height Segments [6]
No controller assigned to it
No keys assigned to it
Material [5]
No controller assigned to it
No keys assigned to it
Image Motion Blur Multiplier [6]
No controller assigned to it
No keys assigned to it
Object Motion Blur On Off [7]
No controller assigned to it
No keys assigned to it