Exercise: Extracting Materials#
Similar to how we extracted the geometry from our .obj
file, we will now extract the materials. This is the last part of the code that we will be adding to extract()
.
At the top of our file, add
Gf
,Sdf
, andUsdShade
imports frompxr
.
1from pxr import Gf, Sdf, Tf, Usd, UsdGeom, UsdShade
Here we’ll sanitize the material identifiers as we did with the meshes. Then we’ll set up the initial shaders.
Let’s get each material from the mesh. Add the following code to get the material and check if it exists before creating the identifier. This will slot underneath where we defined our
if mesh.normals
but not inside the if statement.
1# Get the mesh's material by index
2# scene.materials is a dictionary consisting of assimp material properties
3mtl = scene.materials[mesh.material_index]
4if not mtl:
5 continue
6sanitized_mat_name = Tf.MakeValidIdentifier(mtl["NAME"])
7material_path = Sdf.Path(f"/{sanitized_mat_name}")
Next, we will create the material prim in our stage that has a shader surface for the material graph. Add the following code underneath
material_path
.
1# Create the material prim
2material: UsdShade.Material = UsdShade.Material.Define(stage, material_path)
3# Create a UsdPreviewSurface Shader prim.
4shader: UsdShade.Shader = UsdShade.Shader.Define(stage, material_path.AppendChild("Shader"))
5shader.CreateIdAttr("UsdPreviewSurface")
6# Connect shader surface output as an output for the material graph.
7material.CreateSurfaceOutput().ConnectToSource(shader.ConnectableAPI(), UsdShade.Tokens.surface)
First, we get the material info from Assimp using the material index stored with the mesh. You’ll notice that we ensure we’re using a valid identifier for the material name. With the valid material name, we create the UsdShade.Material
and UsdShade.Shader
.
The material prim serves as a container for a material graph. In this case, we have just one node in our material graph, a UsdPreviewSurface
shader. Lastly, we loft the material graph output by connecting the outputs:surface
from the shader prim to the material prim.
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
With usdview open, click on the
Material_001
prim in the stage tree and expandoutputs:surface
in the Properties window.
Attention
If you don’t see the following in usdview, make sure Default Dome Light is enabled. You can enable it by going to Lights > Enable Default Dome Light.
We’ve defined the material graph and primary shader for each material, but at this point, you shouldn’t see any visual difference in the viewport.
One point of interest is that the material names were fixed in this case with Tf.MakeValidIdentifier()
since this OBJ was authored in Blender with material names in the format: “Material.001”. The invalid character, the period, was replaced with an underscore. You can open the OBJ file in VS Code to see the original material names. Additionally, the expanded outputs:surface
property on the material prim shows the connection to the shader prim.
Next, let’s map the material properties from OBJ to the
UsdPreviewSurface
shader. Copy and paste this code below where we created our shaders.
1# Get colors
2diffuse_color = mtl["COLOR_DIFFUSE"]
3emissive_color = mtl["COLOR_EMISSIVE"]
4specular_color = mtl["COLOR_SPECULAR"]
5# Convert specular shininess to roughness.
6roughness = 1 - math.sqrt(mtl["SHININESS"] / 1000.0)
7
8shader.CreateInput("useSpecularWorkflow", Sdf.ValueTypeNames.Int).Set(1)
9shader.CreateInput("diffuseColor", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(diffuse_color))
10shader.CreateInput("emissiveColor", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(emissive_color))
11shader.CreateInput("specularColor", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(specular_color))
12shader.CreateInput("roughness", Sdf.ValueTypeNames.Float).Set(roughness)
Here we are setting the UsdPreviewSurface
shader to use a specular workflow instead of PBR, as it maps most directly to the data coming from OBJ. After that, we create the relevant inputs and set their values from OBJ. This will complete the material definitions, but we won’t see anything in the viewport until we bind the material to the mesh.
Let’s bind the material to the mesh. Add the following code below the shader connections,
shader.CreateInput()
.
1binding_api = UsdShade.MaterialBindingAPI.Apply(usd_mesh.GetPrim())
2binding_api.Bind(material)
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
31 for mesh in scene.meshes:
32 # Replace any invalid characters with underscores.
33 sanitized_mesh_name = Tf.MakeValidIdentifier(mesh.name)
34 usd_mesh = UsdGeom.Mesh.Define(stage, f"/{sanitized_mesh_name}")
35 # You can use the Vt APIs here instead of Python lists.
36 # Especially keep this in mind for C++ implementations.
37 face_vertex_counts = []
38 face_vertex_indices = []
39 for indices in mesh.indices:
40 # Convert the indices to a flat list
41 face_vertex_indices.extend(indices)
42 # Append the number of vertices for each face
43 face_vertex_counts.append(len(indices))
44
45 usd_mesh.CreatePointsAttr(mesh.vertices)
46 usd_mesh.CreateFaceVertexCountsAttr().Set(face_vertex_counts)
47 usd_mesh.CreateFaceVertexIndicesAttr().Set(face_vertex_indices)
48 # Treat the mesh as a polygonal mesh and not a subdivision surface.
49 # Respect the normals or lack of normals from OBJ.
50 usd_mesh.CreateSubdivisionSchemeAttr(UsdGeom.Tokens.none)
51 if mesh.normals:
52 usd_mesh.CreateNormalsAttr(mesh.normals)
53
54 # Get the mesh's material by index
55 # scene.materials is a dictionary consisting of assimp material properties
56 mtl = scene.materials[mesh.material_index]
57 if not mtl:
58 continue
59 sanitized_mat_name = Tf.MakeValidIdentifier(mtl["NAME"])
60 material_path = Sdf.Path(f"/{sanitized_mat_name}")
61 # Create the material prim
62 material: UsdShade.Material = UsdShade.Material.Define(stage, material_path)
63 # Create a UsdPreviewSurface Shader prim.
64 shader: UsdShade.Shader = UsdShade.Shader.Define(stage, material_path.AppendChild("Shader"))
65 shader.CreateIdAttr("UsdPreviewSurface")
66 # Connect shader surface output as an output for the material graph.
67 material.CreateSurfaceOutput().ConnectToSource(shader.ConnectableAPI(), UsdShade.Tokens.surface)
68 # Get colors
69 diffuse_color = mtl["COLOR_DIFFUSE"]
70 emissive_color = mtl["COLOR_EMISSIVE"]
71 specular_color = mtl["COLOR_SPECULAR"]
72 # Convert specular shininess to roughness.
73 roughness = 1 - math.sqrt(mtl["SHININESS"] / 1000.0)
74
75 shader.CreateInput("useSpecularWorkflow", Sdf.ValueTypeNames.Int).Set(1)
76 shader.CreateInput("diffuseColor", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(diffuse_color))
77 shader.CreateInput("emissiveColor", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(emissive_color))
78 shader.CreateInput("specularColor", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(specular_color))
79 shader.CreateInput("roughness", Sdf.ValueTypeNames.Float).Set(roughness)
80 binding_api = UsdShade.MaterialBindingAPI.Apply(usd_mesh.GetPrim())
81 binding_api.Bind(material)
82
83 return stage
84
85
86def transform(stage: Usd.Stage, args: argparse.Namespace):
87 logger.info("Executing transformation phase...")
88
89
90def main(args: argparse.Namespace):
91 # Extract the .obj
92 stage: Usd.Stage = extract(args.input, args.output)
93 # Transformations to be applied to the scene hierarchy
94 transform(stage, args)
95 # Save the Stage after editing
96 stage.Save()
97
98# ^^^^^^^^^^^^^^^^^^^^
99# ADD CODE ABOVE HERE
100
101
102if __name__ == "__main__":
103 logging.basicConfig(level=logging.DEBUG)
104 parser = argparse.ArgumentParser(
105 "obj2usd", description="An OBJ to USD converter script."
106 )
107 parser.add_argument("input", help="Input OBJ file", type=Path)
108 parser.add_argument("-o", "--output", help="Specify an output USD file", type=Path)
109 export_opts = parser.add_argument_group("Export Options")
110 export_opts.add_argument(
111 "-u",
112 "--up-axis",
113 help="Specify the up axis for the exported USD stage.",
114 type=UpAxis,
115 choices=list(UpAxis),
116 default=UpAxis.Y,
117 )
118
119 args = parser.parse_args()
120 if args.output is None:
121 args.output = args.input.parent / f"{args.input.stem}.usda"
122
123 logger.info(f"Converting {args.input}...")
124 main(args)
125 logger.info(f"Converted results output as: {args.output}.")
126 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
Now, we should see unique materials on the meshes.
Notice the diffuse and specular differences between the materials. Take some time to explore the values on the different shaders and their visual results in the viewport.