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.
Create a Python file called
obj2usd.pyin thedata_exchangefolder.
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.
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.usdafile 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.
In the section
ADD CODE BELOW HEREwrite 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.
Let’s start with defining
extract(). Abovemain(),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.
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.objand convert it to.usd.transform()- Where we apply export options and any transformations to the content structure that are not part of the original.objfile.
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.")