Exercise: Transforming the Prim Hierarchy#
In this exercise, we will create our first transformation step to turn our converted output into an asset that is easy to reference and to fix the defaultPrim
validation error. Let’s look at the tree view of a USD layer produced by obj2usd
.
Notice that there are multiple prims under the stage pseudo-root (or root). While our converter output faithfully represents the flat hierarchy from OBJ, it creates two usability issues in OpenUSD:
There is no
defaultPrim
metadata to reference this stage without specifying a target prim.Even with
defaultPrim
set, there’s no easy way to reference the entire asset since all the prims that make up the asset don’t share a common ancestor prim.
This is why we should add a transformation step to make converted OBJs easier to use out-of-box in OpenUSD. Let’s choose to make this a transformation that always runs as part of our converter because it’s critical to provide good UX for end users.
First, open
obj2usd.py
Let’s add a new function to handle this transformation. Copy and paste this code between
extract()
andtransform()
:
1def set_default_prim(stage: Usd.Stage):
2 """Set a default prim to make this stage referenceable
3
4 OBJ has no notion of a scene graph hierarchy or a scene root.
5 This is a mandatory chaser to move all prims under a default prim
6 to make this asset referenceable.
7 Args:
8 stage (Usd.Stage): The stage to modify
9 """
10
11 # Get the prim in the root namespace that we want to reparent under the default prim.
12 root_prims = stage.GetPseudoRoot().GetChildren()
13 world_prim = UsdGeom.Xform.Define(stage, "/World").GetPrim()
14 stage.SetDefaultPrim(world_prim)
15 editor = Usd.NamespaceEditor(stage)
16 for prim in root_prims:
17 editor.ReparentPrim(prim, world_prim)
18 editor.ApplyEdits()
This function creates a new UsdGeom.Xform
prim called “/World” and sets it as the defaultPrim
. It then parents all of the other prims in the root namespace to “/World” using Usd.NamespaceEditor
so that they all share a common ancestor and can be referenced together.
This won’t do anything until we call the new function in transform()
.
Copy and paste this code into
transform()
:
1set_default_prim(stage)
Click to reveal our Python code up to this point.
1import argparse
2import logging
3import math
4from enum import Enum
5from pathlib import Path
6
7import assimp_py
8from pxr import Gf, Sdf, Tf, Usd, UsdGeom, UsdShade
9
10logger = logging.getLogger("obj2usd")
11
12
13class UpAxis(Enum):
14 Y = UsdGeom.Tokens.y
15 Z = UsdGeom.Tokens.z
16
17 def __str__(self):
18 return self.value
19
20# ADD CODE BELOW HERE
21# vvvvvvvvvvvvvvvvvvv
22
23def extract(input_file: Path, output_file: Path) -> Usd.Stage:
24 logger.info("Executing extraction phase...")
25 process_flags = 0
26 # Load the obj using Assimp
27 scene = assimp_py.ImportFile(str(input_file), process_flags)
28 # Define the stage where the output will go
29 stage: Usd.Stage = Usd.Stage.CreateNew(str(output_file))
30 UsdGeom.SetStageUpAxis(stage, UsdGeom.Tokens.y)
31 # Assume linear units as meters.
32 UsdGeom.SetStageMetersPerUnit(stage, UsdGeom.LinearUnits.meters)
33
34 for mesh in scene.meshes:
35 # Replace any invalid characters with underscores.
36 sanitized_mesh_name = Tf.MakeValidIdentifier(mesh.name)
37 usd_mesh = UsdGeom.Mesh.Define(stage, f"/{sanitized_mesh_name}")
38 # You can use the Vt APIs here instead of Python lists.
39 # Especially keep this in mind for C++ implementations.
40 face_vertex_counts = []
41 face_vertex_indices = []
42 for indices in mesh.indices:
43 # Convert the indices to a flat list
44 face_vertex_indices.extend(indices)
45 # Append the number of vertices for each face
46 face_vertex_counts.append(len(indices))
47
48 usd_mesh.CreatePointsAttr(mesh.vertices)
49 usd_mesh.CreateFaceVertexCountsAttr().Set(face_vertex_counts)
50 usd_mesh.CreateFaceVertexIndicesAttr().Set(face_vertex_indices)
51 # Treat the mesh as a polygonal mesh and not a subdivision surface.
52 # Respect the normals or lack of normals from OBJ.
53 usd_mesh.CreateSubdivisionSchemeAttr(UsdGeom.Tokens.none)
54 if mesh.normals:
55 usd_mesh.CreateNormalsAttr(mesh.normals)
56
57 # Get the mesh's material by index
58 # scene.materials is a dictionary consisting of assimp material properties
59 mtl = scene.materials[mesh.material_index]
60 if not mtl:
61 continue
62 sanitized_mat_name = Tf.MakeValidIdentifier(mtl["NAME"])
63 material_path = Sdf.Path(f"/{sanitized_mat_name}")
64 # Create the material prim
65 material: UsdShade.Material = UsdShade.Material.Define(stage, material_path)
66 # Create a UsdPreviewSurface Shader prim.
67 shader: UsdShade.Shader = UsdShade.Shader.Define(stage, material_path.AppendChild("Shader"))
68 shader.CreateIdAttr("UsdPreviewSurface")
69 # Connect shader surface output as an output for the material graph.
70 material.CreateSurfaceOutput().ConnectToSource(shader.ConnectableAPI(), UsdShade.Tokens.surface)
71 # Get colors
72 diffuse_color = mtl["COLOR_DIFFUSE"]
73 emissive_color = mtl["COLOR_EMISSIVE"]
74 specular_color = mtl["COLOR_SPECULAR"]
75 # Convert specular shininess to roughness.
76 roughness = 1 - math.sqrt(mtl["SHININESS"] / 1000.0)
77
78 shader.CreateInput("useSpecularWorkflow", Sdf.ValueTypeNames.Int).Set(1)
79 shader.CreateInput("diffuseColor", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(diffuse_color))
80 shader.CreateInput("emissiveColor", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(emissive_color))
81 shader.CreateInput("specularColor", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(specular_color))
82 shader.CreateInput("roughness", Sdf.ValueTypeNames.Float).Set(roughness)
83 binding_api = UsdShade.MaterialBindingAPI.Apply(usd_mesh.GetPrim())
84 binding_api.Bind(material)
85
86 return stage
87
88
89def set_default_prim(stage: Usd.Stage):
90 """Set a default prim to make this stage referenceable
91
92 OBJ has no notion of a scene graph hierarchy or a scene root.
93 This is a mandatory chaser to move all prims under a default prim
94 to make this asset referenceable.
95 Args:
96 stage (Usd.Stage): The stage to modify
97 """
98
99 # Get the prim in the root namespace that we want to reparent under the default prim.
100 root_prims = stage.GetPseudoRoot().GetChildren()
101 world_prim = UsdGeom.Xform.Define(stage, "/World").GetPrim()
102 stage.SetDefaultPrim(world_prim)
103 editor = Usd.NamespaceEditor(stage)
104 for prim in root_prims:
105 editor.ReparentPrim(prim, world_prim)
106 editor.ApplyEdits()
107
108
109def transform(stage: Usd.Stage, args: argparse.Namespace):
110 logger.info("Executing transformation phase...")
111 set_default_prim(stage)
112
113
114def main(args: argparse.Namespace):
115 # Extract the .obj
116 stage: Usd.Stage = extract(args.input, args.output)
117 # Transformations to be applied to the scene hierarchy
118 transform(stage, args)
119 # Save the Stage after editing
120 stage.Save()
121
122# ^^^^^^^^^^^^^^^^^^^^
123# ADD CODE ABOVE HERE
124
125
126if __name__ == "__main__":
127 logging.basicConfig(level=logging.DEBUG)
128 parser = argparse.ArgumentParser(
129 "obj2usd", description="An OBJ to USD converter script."
130 )
131 parser.add_argument("input", help="Input OBJ file", type=Path)
132 parser.add_argument("-o", "--output", help="Specify an output USD file", type=Path)
133 export_opts = parser.add_argument_group("Export Options")
134 export_opts.add_argument(
135 "-u",
136 "--up-axis",
137 help="Specify the up axis for the exported USD stage.",
138 type=UpAxis,
139 choices=list(UpAxis),
140 default=UpAxis.Y,
141 )
142
143 args = parser.parse_args()
144 if args.output is None:
145 args.output = args.input.parent / f"{args.input.stem}.usda"
146
147 logger.info(f"Converting {args.input}...")
148 main(args)
149 logger.info(f"Converted results output as: {args.output}.")
150 logger.info(f"Done.")
Save the file and execute the script by running the following in the terminal:
Windows:
python .\data_exchange\obj2usd.py .\data_exchange\shapes.obj
Linux:
python ./data_exchange/obj2usd.py ./data_exchange/shapes.obj
Open the output USD stage with usdview to see the result:
Windows:
.\scripts\usdview.bat .\data_exchange\shapes.usda
Linux:
./scripts/usdview.sh ./data_exchange/shapes.usda
You should now see in the usdview tree view that only “World” is parented under root (pseudo-root) and all of the mesh and material prims are parented under “World”.
Run usdchecker to validate that
defaultPrim
metadata is now set:
Windows:
.\scripts\usdchecker.bat .\data_exchange\shapes.usda
Linux:
./scripts/usdchecker.sh ./data_exchange/shapes.usda
Congratulations! No more errors reported. You’ve now created a fully compliant OpenUSD asset.