Transforms
This section still needs some more love, we'll likely expand it more in the near future.
Table of Contents
TL;DR - Transforms In-A-Nutshell
Transforms are encoded via the following naming scheme and attributes:
- xformOpOrder: This (non-animatable) attribute controls what xformOp: namespaced attributes affect the prims local space transform.
- xfromOp:: Xform ops are namespaced with this namespace and can be considered by the "xformOpOrder" attribute. We can add any number of xform ops for xformable prims, the final world transform is then computed based on a prims local transform and that of all its ancestors.
What should I use it for?
We rarely write the initial transforms ourselves, this is something our DCCs excel at. We do query transforms though for different scenarios:
- We can bake down the transform to a single prim. This can then be referenced or inherited and used as a parent constraint like mechanism.
- When merging hierarchies, we often want to preserve the world transform of leaf prims. Let's say we have two stages: We can simply get the parent xform of stage A and then apply it in inverse to our leaf prim in stage B. That way the leaf prim in stage B is now in local space and merging the stages returns the expected result. We show an example of this below.
Resources
Overview
Creating xforms is usually handled by our DCCS. Let's go over the basics how USD encodes them to understand what we are working with.
All shown examples can be found in the xforms .hip file in our GitHub repo.
Creating and animating transforms
USD evaluates xform attributes on all sub types of the xformable schema.
Transforms are encoded via the following naming scheme and attributes:
- xformOpOrder: This (non-animatable) attribute controls what xformOp: namespaced attributes affect the prims local space transform.
- xfromOp:: Xform ops are namespaced with this namespace and can be considered by the "xformOpOrder" attribute.
- We can add any number of xform ops to xformable prims.
- Any xform op can be suffixed with a custom name, e.g. xformOp:translate:myCoolTranslate
- Available xform Ops are:
- xformOp:translate
- xformOp:orient
- xformOp:rotateXYZ, xformOp:rotateXZY, xformOp:rotateYXZ, xformOp:rotateYZX, xformOp:rotateZXY, xformOp:rotateZYX
- xformOp:rotateX, xformOp:rotateY, xformOp:rotateZ
- xformOp:scale
- xformOp:transform
The final world transform is computed based on a prims local transform and that of all its ancestors.
## Xformable Class
# This is class is a wrapper around creating attributes that start with "xformOp".
# When we run one of its "Add<XformOpName>Op" methods, it automatically adds
# it to the "xformOpOrder" attribute. This attribute controls, what attributes
# contribute to the xform of a prim.
# Has: 'TransformMightBeTimeVarying',
# Get: 'GetOrderedXformOps', 'GetXformOpOrderAttr', 'GetResetXformStack',
# Add: 'AddTranslateOp', 'AddOrientOp', 'AddRotate<XYZ>op', 'AddScaleOp', 'AddTransformOp', 'AddXformOp',
# Set: 'CreateXformOpOrderAttr', 'SetXformOpOrder', 'SetResetXformStack', 'MakeMatrixXform',
# Clear: 'ClearXformOpOrder',
## For querying we can use the following. For large queries we should resort to UsdGeom.XformCache/UsdGeom.BBoxCache
# Get Xform: 'GetLocalTransformation', 'ComputeLocalToWorldTransform', 'ComputeParentToWorldTransform',
# Get Bounds: 'ComputeLocalBound', 'ComputeUntransformedBound', 'ComputeWorldBound',
import math
from pxr import Gf, Sdf, Usd, UsdGeom
stage = Usd.Stage.CreateInMemory()
root_prim_path = Sdf.Path("/root")
root_prim = stage.DefinePrim(root_prim_path, "Xform")
cone_prim_path = Sdf.Path("/root/cone")
cone_prim = stage.DefinePrim(cone_prim_path, "Cone")
# Set local transform of leaf prim
cone_xformable = UsdGeom.Xformable(cone_prim)
cone_translate_op = cone_xformable.AddTranslateOp(opSuffix="upAndDown")
cone_rotate_op = cone_xformable.AddRotateXYZOp(opSuffix= "spinMeRound")
for frame in range(1, 100):
cone_translate_op.Set(Gf.Vec3h([5, math.sin(frame * 0.1) * 3, 0]), frame)
#cone_rotate_op.Set(Gf.Vec3h([0, frame * 5, 0]), frame)
# By clearing the xformOpOrder attribute, we keep the transforms, but don't apply it.
cone_xformOpOrder_attr = cone_xformable.GetXformOpOrderAttr()
cone_xformOpOrder_value = cone_xformOpOrder_attr.Get()
#cone_xformable.ClearXformOpOrder()
# Reverse the transform order
#cone_xformOpOrder_attr.Set(cone_xformOpOrder_value[::-1])
# A transform is combined with its parent prims' transforms
root_xformable = UsdGeom.Xformable(root_prim)
root_translate_op = root_xformable.AddTranslateOp(opSuffix="upAndDown")
root_rotate_op = root_xformable.AddRotateZOp(opSuffix= "spinMeRound")
for frame in range(1, 100):
# root_translate_op.Set(Gf.Vec3h([5, math.sin(frame * 0.5), 0]), frame)
root_rotate_op.Set(frame * 15, frame)
Here is the snippet in action:
Ignoring parent transforms by resetting the xform stack
We can also set the special '!resetXformStack!' value in our "xformOpOrder" attribute to reset the transform stack. This means all parent transforms will be ignored, as well as any attribute before the '!resetXformStack!' in the xformOp order list.
Resetting the xform stack is often not the right way to go, as we loose any parent hierarchy updates. We also have to make sure that we write our reset-ed xform with the correct sub-frame time samples, so that motion blur works correctly.
This should only be used as a last resort to enforce a prim to have a specific transform. We should rather re-write leaf prim xform in local space, see Merging hierarchy transforms for more info.
import math
from pxr import Gf, Sdf, Usd, UsdGeom
stage = Usd.Stage.CreateInMemory()
root_prim_path = Sdf.Path("/root")
root_prim = stage.DefinePrim(root_prim_path, "Xform")
cone_prim_path = Sdf.Path("/root/cone")
cone_prim = stage.DefinePrim(cone_prim_path, "Cone")
# Set local transform of leaf prim
cone_xformable = UsdGeom.Xformable(cone_prim)
cone_translate_op = cone_xformable.AddTranslateOp(opSuffix="upAndDown")
for frame in range(1, 100):
cone_translate_op.Set(Gf.Vec3h([5, math.sin(frame * 0.1) * 3, 0]), frame)
# A transform is combined with its parent prims' transforms
root_xformable = UsdGeom.Xformable(root_prim)
root_rotate_op = root_xformable.AddRotateZOp(opSuffix= "spinMeRound")
for frame in range(1, 100):
root_rotate_op.Set(frame * 15, frame)
# If we only want the local stack transform, we can add the special
# '!resetXformStack!' attribute to our xformOpOrder attribute.
# We can add it anywhere in the list, any xformOps before it and on ancestor prims
# will be ignored.
cone_xformable.SetResetXformStack(True)
Querying transforms
We can query xforms via UsdGeom.Xformable
API or via the UsdGeom.XformCache
cache.
The preferred way should always be the xform cache, as it re-uses ancestor xforms in its cache, when querying nested xforms. Only when querying a single leaf transform, we should go with the Xformable API.
import math
from pxr import Gf, Sdf, Usd, UsdGeom
stage = Usd.Stage.CreateInMemory()
root_prim_path = Sdf.Path("/root")
root_prim = stage.DefinePrim(root_prim_path, "Xform")
cone_prim_path = Sdf.Path("/root/cone")
cone_prim = stage.DefinePrim(cone_prim_path, "Cone")
# Set local transform of leaf prim
cone_xformable = UsdGeom.Xformable(cone_prim)
cone_translate_op = cone_xformable.AddTranslateOp(opSuffix="upAndDown")
for frame in range(1, 100):
cone_translate_op.Set(Gf.Vec3h([5, math.sin(frame * 0.1) * 3, 0]), frame)
# A transform is combined with its parent prims' transforms
root_xformable = UsdGeom.Xformable(root_prim)
root_rotate_op = root_xformable.AddRotateZOp(opSuffix= "spinMeRound")
for frame in range(1, 100):
root_rotate_op.Set(frame * 15, frame)
# For single queries we can use the xformable API
print(cone_xformable.ComputeLocalToWorldTransform(Usd.TimeCode(15)))
## Xform Cache
# Get: 'GetTime', 'ComputeRelativeTransform', 'GetLocalToWorldTransform', 'GetLocalTransformation', 'GetParentToWorldTransform'
# Set: 'SetTime'
# Clear: 'Clear'
xform_cache = UsdGeom.XformCache(Usd.TimeCode(1))
for prim in stage.Traverse():
print(xform_cache.GetLocalToWorldTransform(prim))
"""Returns:
( (0.9659258262890683, 0.25881904510252074, 0, 0), (-0.25881904510252074, 0.9659258262890683, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )
( (0.9659258262890683, 0.25881904510252074, 0, 0), (-0.25881904510252074, 0.9659258262890683, 0, 0), (0, 0, 1, 0), (4.7520971567527654, 1.5834484942764433, 0, 1) )
"""
xform_cache = UsdGeom.XformCache(Usd.TimeCode(1))
for prim in stage.Traverse():
print(xform_cache.GetLocalTransformation(prim))
"""Returns:
(Gf.Matrix4d(0.9659258262890683, 0.25881904510252074, 0.0, 0.0,
-0.25881904510252074, 0.9659258262890683, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0), False)
(Gf.Matrix4d(1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
5.0, 0.299560546875, 0.0, 1.0), False)
"""
Transforms in production
Let's have a look at some production related xform setups.
Merging hierarchy transforms
In production we often need to merge different layers with different transforms at different hierarchy levels. For example when we have a cache in world space and we want to merge it into an existing hierarchy.
Here's how we can achieve that (This example is a bit abstract, we'll add something more visual in the near future).
import math
from pxr import Gf, Sdf, Usd, UsdGeom, UsdUtils
# Stage A: A car animated in world space
stage_a = Usd.Stage.CreateInMemory()
#stage_a = stage
car_prim_path = Sdf.Path("/set/stret/car")
car_prim = stage_a.DefinePrim(car_prim_path, "Xform")
car_body_prim_path = Sdf.Path("/set/stret/car/body/hull")
car_body_prim = stage_a.DefinePrim(car_body_prim_path, "Cube")
car_xformable = UsdGeom.Xformable(car_prim)
car_translate_op = car_xformable.AddTranslateOp(opSuffix="carDrivingDownStreet")
for frame in range(1, 100):
car_translate_op.Set(Gf.Vec3h([frame, 0, 0]), frame)
# Stage A: A person animated in world space
stage_b = Usd.Stage.CreateInMemory()
#stage_b = stage
mike_prim_path = Sdf.Path("/set/stret/car/person/mike")
mike_prim = stage_b.DefinePrim(mike_prim_path, "Sphere")
mike_xformable = UsdGeom.Xformable(mike_prim)
mike_translate_op = mike_xformable.AddTranslateOp(opSuffix="mikeInWorldSpace")
mike_xform_op = mike_xformable.AddTransformOp(opSuffix="mikeInLocalSpace")
# Let's disable the transform op for now
mike_xformable.GetXformOpOrderAttr().Set([mike_translate_op.GetOpName()])
for frame in range(1, 100):
mike_translate_op.Set(Gf.Vec3h([frame, 1, 0]), frame)
# How do we merge these?
stage_a_xform_cache = UsdGeom.XformCache(0)
stage_b_xform_cache = UsdGeom.XformCache(0)
for frame in range(1, 100):
stage_a_xform_cache.SetTime(frame)
car_xform = stage_a_xform_cache.GetLocalToWorldTransform(car_prim)
stage_b_xform_cache.SetTime(frame)
mike_xform = stage_b_xform_cache.GetLocalToWorldTransform(mike_prim)
mike_xform = mike_xform * car_xform.GetInverse()
mike_xform_op.Set(mike_xform, frame)
# Let's enable the transform op now and disable the translate op
mike_xformable.GetXformOpOrderAttr().Set([mike_xform_op.GetOpName()])
stage_c = Usd.Stage.CreateInMemory()
# Combine stages
stage_c = Usd.Stage.CreateInMemory()
layer_a = stage_a.GetRootLayer()
layer_b = stage_b.GetRootLayer()
UsdUtils.StitchLayers(layer_a, layer_b)
stage_c.GetEditTarget().GetLayer().TransferContent(layer_a)
Baking transforms for constraint like behavior
If we want a parent constraint like behavior, we have to bake down the transform to a single prim. We can then inherit/internal reference/specialize this xform to "parent constrain" something.
from pxr import Gf, Sdf, Usd, UsdGeom
stage = Usd.Stage.CreateInMemory()
# Scene
car_prim_path = Sdf.Path("/set/stret/car")
car_prim = stage.DefinePrim(car_prim_path, "Xform")
car_body_prim_path = Sdf.Path("/set/stret/car/body/hull")
car_body_prim = stage.DefinePrim(car_body_prim_path, "Cube")
car_xformable = UsdGeom.Xformable(car_prim)
car_translate_op = car_xformable.AddTranslateOp(opSuffix="carDrivingDownStreet")
for frame in range(1, 100):
car_translate_op.Set(Gf.Vec3h([frame, 0, 0]), frame)
# Constraint Targets
constraint_prim_path = Sdf.Path("/constraints/car")
constraint_prim = stage.DefinePrim(constraint_prim_path)
constraint_xformable = UsdGeom.Xformable(constraint_prim)
constraint_xformable.SetResetXformStack(True)
constraint_translate_op = constraint_xformable.AddTranslateOp(opSuffix="moveUp")
constraint_translate_op.Set(Gf.Vec3h([0,5,0]))
constraint_transform_op = constraint_xformable.AddTransformOp(opSuffix="constraint")
xform_cache = UsdGeom.XformCache(Usd.TimeCode(0))
for frame in range(1, 100):
xform_cache.SetTime(Usd.TimeCode(frame))
xform = xform_cache.GetLocalToWorldTransform(car_body_prim)
constraint_transform_op.Set(xform, frame)
# Constrain
balloon_prim_path = Sdf.Path("/objects/balloon")
balloon_prim = stage.DefinePrim(balloon_prim_path, "Sphere")
balloon_prim.GetAttribute("radius").Set(2)
balloon_prim.GetReferences().AddInternalReference(constraint_prim_path)
Reading Xforms in Shaders
To read composed xforms in shaders, USD ships with the Coordinate Systems mechanism.
It allows us to add a relationship on prims, that targets to an xform prim. This xform can then be queried in shaders e.g. for projections or other transform related needs.
See these links for more information: UsdShade.CoordSys Houdini CoordSys in Shaders