Units in OpenUSD#

What Are Units?#

When assembling 3D content from various sources—whether created in Blender at meter scale, Houdini in centimeters, or Revit in feet—ensuring consistent interpretation of units is critical for coherent scene composition. USD provides several stage metadata fields to encode unit information:

  • metersPerUnit (MPU) - Defines the linear scale of geometry in the file relative to meters. For example, 0.01 means content is encoded in centimeters (0.01 meters per unit), while 1.0 means content is in meters.

  • upAxis - Specifies the coordinate system orientation, typically "Y" or "Z".

  • kilogramsPerUnit (KGPU) - Defines the mass/density scale for physics simulations, representing kilograms per unit cubed.

  • timeCodesPerSecond - Defines the temporal scale for animation, mapping time codes to real-world seconds.

These metadata fields help USD understand the intent of content creators and enable correct interpretation when multiple layers are composed together.

Note

If metersPerUnit is not explicitly authored in a USD file, the fallback value is 0.01 (centimeters). For upAxis, the fallback is "Y".

Why Units Matter#

Consider a pipeline where:

  • An environment asset is built in meters (metersPerUnit = 1.0)

  • A character asset is built in centimeters (metersPerUnit = 0.01)

  • Both assets are referenced into the same scene

Without proper unit handling, a 2-meter tall character would appear as 2 centimeters tall when composed into a meter-based scene—100 times too small. Understanding how USD handles unit metadata during composition is essential for building robust pipelines.

How Does It Work?#

Automatic vs. Manual Unit Reconciliation#

A critical aspect of USD’s unit system is that not all unit metadata is handled the same way during composition. This distinction is important for pipeline developers to understand:

Automatically Handled#

  • timeCodesPerSecond

When layers with different timeCodesPerSecond values are composed (via sublayers, references, or payloads), USD automatically scales time samples so animations play back correctly relative to the root layer’s timeCodesPerSecond.

For example, if your root layer has timeCodesPerSecond = 24 and you reference a layer with timeCodesPerSecond = 60, USD automatically adjusts the time samples from the referenced layer so the animation timing remains correct. This automatic scaling happens transparently during value resolution.

Manually Handled#

  • upAxis

  • metersPerUnit

  • kilogramsPerUnit

In contrast, USD does not automatically reconcile geometric and physics unit metadata. According to the OpenUSD documentation:

“As with encoding stage upAxis, we restrict the encoding of linear units to be stage-wide; if assembling assets of different metrics, it is the assembler’s responsibility to apply suitable correctives to the referenced data to bring it into the referencing stage’s metric.”

This means:

  • A 6-meter tall tree (metersPerUnit = 1.0) referenced into a centimeter-based stage (metersPerUnit = 0.01) will compose as 6 centimeters tall by default—100 times too small

  • Different upAxis values across composed layers require manual coordinate transformations

  • The pipeline must apply scale corrections or ensure unit consistency upstream

Common metersPerUnit Values#

Here are typical MPU values for common units:

Unit

metersPerUnit

Kilometers

1000

Meters

1.0

Centimeters

0.01

Millimeters

0.001

Inches

0.0254

Feet

0.3048

Miles

1609.34

Pipeline Strategies#

Production pipelines typically handle unit mismatches through one of these approaches:

  1. Organization-wide standards: Establish conventions (e.g., “all assets use centimeters and Z-up”) and enforce them during asset creation

  2. Explicit scale correction: Apply corrective transforms when referencing assets with different units

  3. Conversion tools: Build importers/exporters that normalize units during asset ingestion

  4. Metadata validation: Use asset validation tools to flag unit mismatches before composition

Working With Python#

USD provides APIs for querying and setting stage unit metadata:

 1from pxr import Usd, UsdGeom, UsdPhysics
 2
 3# Create or open a stage
 4stage = Usd.Stage.CreateNew("example.usda")
 5
 6# Get and set metersPerUnit
 7current_mpu = UsdGeom.GetStageMetersPerUnit(stage)
 8print(f"Current metersPerUnit: {current_mpu}")
 9
10UsdGeom.SetStageMetersPerUnit(stage, 0.0254)  # Set to inches
11print(f"Updated to: {UsdGeom.GetStageMetersPerUnit(stage)}")
12
13# Get and set upAxis
14current_up = UsdGeom.GetStageUpAxis(stage)
15print(f"Current upAxis: {current_up}")
16
17UsdGeom.SetStageUpAxis(stage, UsdGeom.Tokens.z)  # Set to Z-up
18print(f"Updated to: {UsdGeom.GetStageUpAxis(stage)}")
19
20# Get and set timeCodesPerSecond (stage method, not UsdGeom)
21current_tcps = stage.GetTimeCodesPerSecond()
22print(f"Current timeCodesPerSecond: {current_tcps}")
23
24stage.SetTimeCodesPerSecond(48)
25print(f"Updated to: {stage.GetTimeCodesPerSecond()}")
26
27# Get and set kilogramsPerUnit using UsdPhysics schema
28current_kgpu = UsdPhysics.GetStageKilogramsPerUnit(stage)
29print(f"Current kilogramsPerUnit: {current_kgpu}")
30
31UsdPhysics.SetStageKilogramsPerUnit(stage, 0.001)  # Set to grams
32print(f"Updated to: {UsdPhysics.GetStageKilogramsPerUnit(stage)}")

Examples#

Example 1: Demonstrating metersPerUnit Composition Behavior#

This example shows what happens when assets with different metersPerUnit values are composed together without manual correction. We’ll create two cube assets that both represent 1-meter cubes, but authored at different unit scales:

  1. A 1-meter cube authored in centimeters (metersPerUnit = 0.01), with size = 100.0

  2. A 1-meter cube authored in millimeters (metersPerUnit = 0.001), with size = 1000.0

Both cubes represent the same real-world size—1 meter. If USD automatically converted units during composition, both cubes would appear identical when referenced into any scene regardless of that scene’s metersPerUnit. Let’s see what actually happens when we reference both into a millimeter-scale scene.

 1from pxr import Usd, UsdGeom, Sdf, Gf
 2
 3# Create a cube asset in CENTIMETERS (metersPerUnit = 0.01)
 4cm_asset_path = "_assets/1m_cube_centimeters.usda"
 5cm_stage = Usd.Stage.CreateNew(cm_asset_path)
 6UsdGeom.SetStageUpAxis(cm_stage, UsdGeom.Tokens.y)
 7UsdGeom.SetStageMetersPerUnit(cm_stage, 0.01)  # Centimeters
 8
 9# Create a 1 meter cube in centimeter coordinate system
10cube_in_cm = UsdGeom.Cube.Define(cm_stage, "/Cube")
11cube_in_cm.GetSizeAttr().Set(100.0)
12cube_in_cm.GetDisplayColorAttr().Set([(0.2, 0.6, 0.9)])
13
14# Position and save
15cm_stage.SetDefaultPrim(cube_in_cm.GetPrim())
16cm_stage.Save()
17
18print(f"Centimeter asset: size = { cube_in_cm.GetSizeAttr().Get()} at metersPerUnit = {UsdGeom.GetStageMetersPerUnit(cm_stage)}")
19print(f"  -> Represents a {cube_in_cm.GetSizeAttr().Get() * UsdGeom.GetStageMetersPerUnit(cm_stage):.1f} meter cube\n")
20
21# Create a cube asset in MILLIMETERS (metersPerUnit = 0.001)
22mm_asset_path = "_assets/cube_in_millimeters.usda"
23mm_stage = Usd.Stage.CreateNew(mm_asset_path)
24UsdGeom.SetStageUpAxis(mm_stage, UsdGeom.Tokens.y)
25UsdGeom.SetStageMetersPerUnit(mm_stage, 0.001)  # Millimeters
26
27# Create a 1 meter cube in millimeter coordinate system
28cube_in_mm = UsdGeom.Cube.Define(mm_stage, "/Cube")
29cube_in_mm.GetSizeAttr().Set(1000.0)
30cube_in_mm.GetDisplayColorAttr().Set([(0.9, 0.5, 0.2)])
31
32# Position and save
33mm_stage.SetDefaultPrim(cube_in_mm.GetPrim())
34mm_stage.Save()
35
36print(f"Millimeter asset: size = { cube_in_mm.GetSizeAttr().Get()} at metersPerUnit = {UsdGeom.GetStageMetersPerUnit(mm_stage)}")
37print(f"  -> Represents a {cube_in_mm.GetSizeAttr().Get() * UsdGeom.GetStageMetersPerUnit(mm_stage):.1f} meter cube\n")
38
39# Create a scene that references both cubes (using millimeter scale)
40scene_path = "_assets/units_mismatch_scene.usda"
41scene_stage = Usd.Stage.CreateNew(scene_path)
42UsdGeom.SetStageUpAxis(scene_stage, UsdGeom.Tokens.y)
43UsdGeom.SetStageMetersPerUnit(scene_stage, 0.001)  # Scene is in millimeters
44
45# Add both cube references
46world = UsdGeom.Xform.Define(scene_stage, "/World")
47scene_stage.SetDefaultPrim(world.GetPrim())
48
49meter_ref = scene_stage.DefinePrim("/World/Cube_1m_In_Centimeters")
50meter_ref.GetReferences().AddReference(cm_asset_path)
51UsdGeom.XformCommonAPI(meter_ref).SetTranslate(Gf.Vec3d(-1000, 0, 0))
52
53cm_ref = scene_stage.DefinePrim("/World/Cube_1m_In_Millimeters")
54cm_ref.GetReferences().AddReference(mm_asset_path)
55UsdGeom.XformCommonAPI(cm_ref).SetTranslate(Gf.Vec3d(1000, 0, 0)) 
56
57scene_stage.Save()
58
59print("Scene composition (metersPerUnit = 0.01):")
60print("  - Referenced 1m cube in centimeter-scale appears 10x too small")
61print("  - Referenced 1m cube in millimeter-scale appear correctly")
Centimeter asset: size = 100.0 at metersPerUnit = 0.01
  -> Represents a 1.0 meter cube

Millimeter asset: size = 1000.0 at metersPerUnit = 0.001
  -> Represents a 1.0 meter cube

Scene composition (metersPerUnit = 0.01):
  - Referenced 1m cube in centimeter-scale appears 10x too small
  - Referenced 1m cube in millimeter-scale appear correctly
Loading your model...
_assets/units_mismatch_scene.usda
        
    

Notice the size discrepancy: even though both cubes represent 1 meter in their respective source files, they appear at different sizes in the composed scene. Here’s what happened:

The centimeter cube (blue) was authored with size = 100.0 at metersPerUnit = 0.01. When referenced into the millimeter scene (metersPerUnit = 0.001), USD does not perform any unit conversion—it simply brings in the raw value of 100.0 units. In the millimeter coordinate system, 100 units equals 0.1 meters (100 × 0.001), making it appear 10 times too small.

The millimeter cube (orange) was authored with size = 1000.0 at metersPerUnit = 0.001. Since the scene also uses millimeters, the value of 1000 units equals 1 meter (1000 × 0.001), so it appears at the correct size.

This demonstrates USD’s fundamental behavior: geometric values are copied literally without unit conversion. To properly handle the centimeter cube in this millimeter scene, the pipeline would need to apply a 10× scale transform to compensate for the unit mismatch.

Flattening the stage reveals exactly what USD composed—the raw attribute values without any unit-based scaling:

#usda 1.0
(
    defaultPrim = "World"
    metersPerUnit = 0.001
    upAxis = "Y"
)

def Xform "World"
{
    def Cube "Cube_1m_In_Centimeters"
    {
        color3f[] primvars:displayColor = [(0.2, 0.6, 0.9)]
        double size = 100
        double3 xformOp:translate = (-1000, 0, 0)
        uniform token[] xformOpOrder = ["xformOp:translate"]
    }

    def Cube "Cube_1m_In_Millimeters"
    {
        color3f[] primvars:displayColor = [(0.9, 0.5, 0.2)]
        double size = 1000
        double3 xformOp:translate = (1000, 0, 0)
        uniform token[] xformOpOrder = ["xformOp:translate"]
    }
}

Example 2: Automatic timeCodesPerSecond Scaling#

This example demonstrates that USD does automatically handle timeCodesPerSecond differences during composition. We’ll create an animated asset at 60 fps and reference it into a 24 fps scene.

 1from pxr import Usd, UsdGeom, Gf
 2
 3# Create animated asset at 60 fps
 4anim_asset_path = "_assets/animated_60fps.usda"
 5anim_stage = Usd.Stage.CreateNew(anim_asset_path)
 6UsdGeom.SetStageUpAxis(anim_stage, UsdGeom.Tokens.y)
 7UsdGeom.SetStageMetersPerUnit(anim_stage, 1.0)
 8anim_stage.SetTimeCodesPerSecond(60)  # 60 fps
 9anim_stage.SetStartTimeCode(0)
10anim_stage.SetEndTimeCode(120)  # 2 seconds of animation at 60fps
11
12# Create animated sphere
13anim_sphere = UsdGeom.Sphere.Define(anim_stage, "/AnimatedSphere")
14anim_sphere.GetRadiusAttr().Set(1.0)
15anim_sphere.GetDisplayColorAttr().Set([(0.3, 0.9, 0.3)])
16
17# Animate from left to right over 2 seconds (120 frames at 60fps)
18xform_api = UsdGeom.XformCommonAPI(anim_sphere)
19xform_api.SetTranslate(Gf.Vec3d(-5, 0, 0), Usd.TimeCode(0))
20xform_api.SetTranslate(Gf.Vec3d(5, 0, 0), Usd.TimeCode(120))
21
22anim_stage.SetDefaultPrim(anim_sphere.GetPrim())
23anim_stage.Save()
24
25print(f"Animation asset: {anim_stage.GetStartTimeCode()}-{anim_stage.GetEndTimeCode()} @ {anim_stage.GetTimeCodesPerSecond()} fps")
26print(f"  -> Duration: {(anim_stage.GetEndTimeCode() - anim_stage.GetStartTimeCode() + 1) / anim_stage.GetTimeCodesPerSecond():.1f} seconds") 
27print(f"  -> Animates from x=-5 to x=5\n")
28
29# Create scene at 24 fps that references the 60fps animation
30scene_24fps_path = "_assets/units_timecode_scene.usda"
31scene_stage = Usd.Stage.CreateNew(scene_24fps_path)
32UsdGeom.SetStageUpAxis(scene_stage, UsdGeom.Tokens.y)
33UsdGeom.SetStageMetersPerUnit(scene_stage, 1.0)
34scene_stage.SetTimeCodesPerSecond(24)  # Scene is 24 fps
35scene_stage.SetStartTimeCode(0)
36scene_stage.SetEndTimeCode(48)  # 2 seconds at 24fps
37
38world = UsdGeom.Xform.Define(scene_stage, "/World")
39scene_stage.SetDefaultPrim(world.GetPrim())
40
41# Create a floor
42floor = UsdGeom.Cube.Define(scene_stage, "/World/Plane")
43floor_xform_api = UsdGeom.XformCommonAPI(floor)
44floor_xform_api.SetTranslate(Gf.Vec3d(0, -1, 0))
45floor_xform_api.SetScale(Gf.Vec3f(10.0, 0.1, 4.0))
46
47# Author the same animation in 24fps for comparison
48local_anim = UsdGeom.Sphere.Define(scene_stage, "/World/LocalAnimation")
49local_anim.GetRadiusAttr().Set(1.0)
50local_anim.GetDisplayColorAttr().Set([(0.9, 0.3, 0.3)])
51
52# Animate from left to right over 2 seconds (48 frames at 24fps)
53xform_api = UsdGeom.XformCommonAPI(local_anim)
54xform_api.SetTranslate(Gf.Vec3d(-5, 2, 0), Usd.TimeCode(0))
55xform_api.SetTranslate(Gf.Vec3d(5, 2, 0), Usd.TimeCode(48))
56
57# Reference thte 60fps animated sphere
58ref_prim = scene_stage.DefinePrim("/World/ReferencedAnimation")
59ref_prim.GetReferences().AddReference(anim_asset_path)
60
61scene_stage.Save()
62
63print(f"Scene: {scene_stage.GetStartTimeCode()}-{scene_stage.GetEndTimeCode()} @ {scene_stage.GetTimeCodesPerSecond()} fps")
64print(f"  -> Duration: {(scene_stage.GetEndTimeCode() - scene_stage.GetStartTimeCode() + 1) / scene_stage.GetTimeCodesPerSecond():.1f} seconds")
65print(f"  -> Local sphere animates from x=-5 to x=5 like the referenced sphere")
66print(f"  -> Referenced animation automatically scaled from 60fps to 24fps")
67print(f"  -> Same 2-second duration and motion maintained")
Animation asset: 0.0-120.0 @ 60.0 fps
  -> Duration: 2.0 seconds
  -> Animates from x=-5 to x=5

Scene: 0.0-48.0 @ 24.0 fps
  -> Duration: 2.0 seconds
  -> Local sphere animates from x=-5 to x=5 like the referenced sphere
  -> Referenced animation automatically scaled from 60fps to 24fps
  -> Same 2-second duration and motion maintained
Loading your model...
_assets/units_timecode_scene.usda
        
    

The referenced animation plays back correctly in the 24 fps scene even though it was authored at 60 fps. USD automatically scales the time samples via value resolution. Both spheres take 2 seconds to complete and traverse the same distance, demonstrating that temporal scaling is handled automatically—unlike geometric units which require manual correction.

Key Takeaways#

Understanding USD’s unit system is critical for building robust production pipelines:

  1. Different metadata, different rules: timeCodesPerSecond is automatically reconciled during composition, while metersPerUnit, kilogramsPerUnit, and upAxis are not. Pipelines must handle geometric and physics unit mismatches explicitly.

  2. Standards and conventions are essential: The most effective approach is establishing and enforcing organization-wide unit conventions during asset creation. This minimizes the need for runtime conversions and reduces the chance of errors.

  3. Pipeline responsibility: When different units are unavoidable (e.g., ingesting external vendor assets), pipelines must apply corrective transforms or conversions to ensure consistent interpretation across the composed scene.

  4. Validation is key: Implement validation tools that check for unit consistency across referenced assets and flag potential issues before they cause problems in production.

By understanding these distinctions and establishing clear unit handling strategies, you can build reliable USD pipelines that seamlessly integrate content from diverse sources while maintaining spatial and temporal accuracy.