Exercise: Anatomy of a Converter#

Let’s create a Python script to house all our code. We’ll be developing a standalone converter to help understand how a converter works.

  1. Create a Python file called obj2usd.py in the data_exchange folder.

Next, we’ll add some boilerplate code to help you get started. The following code is a starting point for coding the different parts of the converter. When you write your own converter, this specific structure isn’t necessary.

  1. Let’s start with some boilerplate code:

 1
 2import argparse
 3import logging
 4import math
 5from enum import Enum
 6from pathlib import Path
 7
 8import assimp_py
 9from pxr import Usd, UsdGeom
10
11logger = logging.getLogger("obj2usd")
12
13
14class UpAxis(Enum):
15    Y = UsdGeom.Tokens.y
16    Z = UsdGeom.Tokens.z
17
18    def __str__(self):
19        return self.value
20
21# ADD CODE BELOW HERE
22# vvvvvvvvvvvvvvvvvvv
23
24# [...]
25
26# ^^^^^^^^^^^^^^^^^^^^
27# ADD CODE ABOVE HERE
28
29
30if __name__ == "__main__":
31    logging.basicConfig(level=logging.DEBUG)
32    parser = argparse.ArgumentParser(
33        "obj2usd", description="An OBJ to USD converter script."
34    )
35    parser.add_argument("input", help="Input OBJ file", type=Path)
36    parser.add_argument("-o", "--output", help="Specify an output USD file", type=Path)
37    export_opts = parser.add_argument_group("Export Options")
38    export_opts.add_argument(
39        "-u",
40        "--up-axis",
41        help="Specify the up axis for the exported USD stage.",
42        type=UpAxis,
43        choices=list(UpAxis),
44        default=UpAxis.Y,
45    )
46
47    args = parser.parse_args()
48    if args.output is None:
49        args.output = args.input.parent / f"{args.input.stem}.usda"
50
51    logger.info(f"Converting {args.input}...")
52    main(args)
53    logger.info(f"Converted results output as: {args.output}.")
54    logger.info(f"Done.")
55

At the top of the file, the import statements provide some interesting information. We’ll use Assimp (Open Asset Import Library), an open source file format conversion library, to parse OBJ files for us. We’re also importing the USD Python API for USD authoring.

The if __name__ == "__main__": section towards the bottom of the file is the entry point for the obj2usd script. Note that the script processes a few command line arguments:

  • input: Takes the input OBJ file path to convert.

  • --output: An optional output path for the exported USD file. If not supplied, it will use the OBJ file path with a .usda file extension instead.

  • --up-axis: An export option to control what the up-axis for the export USD stage should be.

Once the arguments are parsed, we call main(). However, main() does not exist yet.

  1. In the section ADD CODE BELOW HERE write the following function:

1def main(args: argparse.Namespace):
2    # Extract the .obj
3    stage: Usd.Stage = extract(args.input, args.output)
4    # Transformations to be applied to the scene hierarchy
5    transform(stage, args)
6    # Save the Stage after editing
7    stage.Save()

Here we’re calling two functions, extract() and transform(), which we will define in the next steps. Referring back to the anatomy of a converter, we start with the extract phase to map the data to OpenUSD as directly as possible to maintain fidelity to the source format. Then we will be performing a transformation phase which consists of one or more optional steps that are added to better meet end-client and user needs. For this module, we’ll focus on the following:

  • Applying user exporting options.

  • Making changes to the content structure that deviates from the source format.

  1. Let’s start with defining extract(). Above main(), add the following code:

1def extract(input_file: Path, output_file: Path) -> Usd.Stage:
2    logger.info("Executing extraction phase...")
3    process_flags = 0
4    # Load the obj using Assimp 
5    scene = assimp_py.ImportFile(str(input_file), process_flags)
6    # Define the stage where the output will go 
7    stage: Usd.Stage = Usd.Stage.CreateNew(str(output_file))
8
9    return stage

You’ll notice that we are not parsing through the scene data we got from Assimp. In the next module, we’ll go over how we can extract the geometry from our .obj file.

The last piece of our two-phase approach is defining the transformation.

  1. Above main(), add the following code:

1def transform(stage: Usd.Stage, args: argparse.Namespace):
2    logger.info("Executing transformation phase...")

At this point, we’ve outlined our two-phase process. The hierarchy is currently defined as:

  • main() - The main entry point of our script.

    • extract() - Where we extract the data from .obj and convert it to .usd.

    • transform() - Where we apply export options and any transformations to the content structure that are not part of the original .obj file.

Note

We will not execute the script yet, but we will continue to build upon it throughout the rest of the exercises in this module to fill out the extract() and transform() functions.

Below is what your python script should look like.

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 Usd, UsdGeom
 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    return stage
32
33
34def transform(stage: Usd.Stage, args: argparse.Namespace):
35    logger.info("Executing transformation phase...")
36
37
38def main(args: argparse.Namespace):
39    # Extract the .obj
40    stage: Usd.Stage = extract(args.input, args.output)
41    # Transformations to be applied to the scene hierarchy
42    transform(stage, args)
43    # Save the Stage after editing
44    stage.Save()
45
46# ^^^^^^^^^^^^^^^^^^^^
47# ADD CODE ABOVE HERE
48
49
50if __name__ == "__main__":
51    logging.basicConfig(level=logging.DEBUG)
52    parser = argparse.ArgumentParser(
53        "obj2usd", description="An OBJ to USD converter script."
54    )
55    parser.add_argument("input", help="Input OBJ file", type=Path)
56    parser.add_argument("-o", "--output", help="Specify an output USD file", type=Path)
57    export_opts = parser.add_argument_group("Export Options")
58    export_opts.add_argument(
59        "-u",
60        "--up-axis",
61        help="Specify the up axis for the exported USD stage.",
62        type=UpAxis,
63        choices=list(UpAxis),
64        default=UpAxis.Y,
65    )
66
67    args = parser.parse_args()
68    if args.output is None:
69        args.output = args.input.parent / f"{args.input.stem}.usda"
70
71    logger.info(f"Converting {args.input}...")
72    main(args)
73    logger.info(f"Converted results output as: {args.output}.")
74    logger.info(f"Done.")