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().

  1. At the top of our file, add Gf, Sdf, and UsdShade imports from pxr.

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.

  1. 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}")
  1. 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.

  1. 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
  1. 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
  1. With usdview open, click on the Material_001 prim in the stage tree and expand outputs: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.

  1. 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.

  1. 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.")
  1. 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
  1. 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.