OOohh a new file format... Hello USD - Part 1

The cross platform open source file format OpenUSD (previously Pixar Universal Scene Description) backs the immersive experiences in Apple’s VisionOS. For most people and teams it will be largely hidden from view by RealityComposer and now its successor RealityComposerPro. That said even without installing any 2023 beta software, Apple’s Preview already displays USD files. Apple has been working with the film industry to accommodate existing software and pipelines for years, apparently.

Part one is the structure of a basic USD file describing a collection of spheres. It assumes you know python and have done some graphics programming before. Later sections will assume comfort around C, C++ and Swift.

References

What’s a USD file?

OpenUSD files aren’t simply 3D object models. USD provides a language to describe “scenes”: all the objects, positioning, animations, etc. There are multiple extensions for working with OpenUSD files, one of which is explicitly a package.

Getting Started

Pixar has made available C++ and Python code that enables folks to work with USD files, but compiling it isn’t necessary for a Hello World because of the “human readable” ASCII version of the spec. Put the below in a text file with the extension .usd and Preview will open it. I’ve called mine the super imaginative sphere.usd. I am using the usd extension instead of the usda extension so Preview will open it.

#usda 1.0
# https://openusd.org/release/tut_helloworld_redux.html

def Sphere "sphere"
{
}

A low poly image of a gray sphere in a computer window.

Sphere is a built in geometry primitive on many but not all implementations of OpenUSD. Primitives or “prims” refer to scene elements on a stage. A more complete .usd file for a sphere would look something like:

#usda 1.0
# https://openusd.org/release/tut_inspect_and_author_props.html

def Xform "hello"
{
    def Sphere "world"
    {
        float3[] extent = [(-2, -2, -2), (2, 2, 2)]
        color3f[] primvars:displayColor = [(0, 0, 1)]
        double radius = 2
    }
}

A low poly image of a bright blue sphere in a computer window.

Two to Tango

What if you want two spheres? The original geometry primitive can be reused in an entirely different file with what is referred to as “layers”, although that word appears nowhere in the file itself. Layers don’t alter the underlying file, they override it. There seems to be some analogies with CSS in terms of which files/options get rendering priority in case of conflict. Our scene isn’t that complicated yet.

The base file

First, create a base_sphere.usd file which takes our sphere file and adds a new default position to it, an offset from the center. There are a handful of tokens one can set on a geometric primitive easily it appears.

A much larger number of tokens is listed if you look at the header file, but translate, pivot and rotate aren’t on that list?

TODO: What’s the deal with tokens vs primvars.

It looks like the format is such that one sets the value, and then passes the value name as a string to an array that determines what order the various possible alterations are applied. Fascinating. I’m going to have to look at the parser. No wonder they don’t want people hand scripting this. FWIW, The python API to write this couplet is UsdGeom.XformCommonAPI(hello).SetTranslate((4, 5, 6)) so it isn’t a string under the hood? To be continued after we install the package…

#usda 1.0
# https://openusd.org/release/tut_referencing_layers.html
(
    defaultPrim = "hello"
)

def Xform "hello"
{
    double3 xformOp:translate = (4, 5, 6) 
    uniform token[] xformOpOrder = ["xformOp:translate"]

    def Sphere "world"
    {
        float3[] extent = [(-2, -2, -2), (2, 2, 2)]
        color3f[] primvars:displayColor = [(0, 0, 1)]
        double radius = 2
    }
}

The layers file

Next, write a file that uses our Sphere definition and overwrites the properties on it as desired.

In the below file for the layers (I’ll refer to it as sphere_layers.usd)

#usda 1.0
# https://openusd.org/release/tut_referencing_layers.html

over "blueSphere" (
    prepend references = @./base_sphere.usd@
)
{
    uniform token[] xformOpOrder = []
}

over "redSphere" (
    prepend references = @./base_sphere.usd@
)
{
    over "world"
    {
        color3f[] primvars:displayColor = [(1, 0, 0)]
        double radius = 3
    }
}

A low poly image of a bright blue sphere and a bright red sphere somewhat separated, in a computer window.

All the spheres!!!

What if you want a lot of spheres? The below script generates a usd file with multiple spheres, with random colors.

A bunch of spheres glommed together. Each is a slightly different size and color.

Example usage:

python3 multiball.py 30 myspheres

Where 30 is the number of spheres and myspheres is the name of the output file. The arguments are optional.

#!/usr/bin/env python3
import sys
from datetime import datetime
import random
    
minX = -4.0
maxX = 4.0
minY = minX
maxY = maxX
minZ = minX
maxZ = maxX
minRadius = 0.8
maxRadius = 2.0

def file_header(): 
    return '#usda 1.0\n'

def offset_string(xoffset, yoffset, zoffset):
    #"My name is {fname}, I'm {age}".format(fname = "John", age = 36)
    return '\n\tdouble3 xformOp:translate = ({x}, {y}, {z})\n\tuniform token[] xformOpOrder = ["xformOp:translate"]\n'.format(x=xoffset, y=yoffset, z=zoffset)
    
def color_string(red, green, blue):
    return '\n\t\tcolor3f[] primvars:displayColor = [({r}, {g}, {b})]'.format(r=red, g=green, b=blue)
    
def radius_string(radius):
    return '\n\t\tdouble radius = {rad}'.format(rad = radius)

def build_item(id, reference_file, geometry_name, xoffset, yoffset, zoffset, radius, red, green, blue):
    tmp_string = '\nover "' + id + '" (\n\tprepend references = @./' + reference_file + '.usd@\n)\n{'
    
    if any([xoffset != 0, yoffset != 0, zoffset != 0]):
        tmp_string = tmp_string + offset_string(xoffset, yoffset, zoffset)        
    
    tmp_string = tmp_string + '\n\tover "' + geometry_name + '"\n\t{'

    tmp_string = tmp_string + color_string(red, green, blue)
    tmp_string = tmp_string + radius_string(radius)

    tmp_string = tmp_string + "\n\t}"
    
    tmp_string = tmp_string + "\n}"
    return tmp_string

def write_file(string, path):
    with open(path, 'bw+') as f:
        f.write(string.encode('utf-8'))
        f.close()

if __name__ == "__main__":    
    if len(sys.argv) == 3:
        count = int(sys.argv[1])
        destination_file_name = sys.argv[2]
    elif len(sys.argv) == 2:
        count = int(sys.argv[1])
        destination_file_name = "multiball_" + datetime.now().strftime("%Y%m%dT%H%M%S")
    else:
        count = 5
        destination_file_name = "multiball_" + datetime.now().strftime("%Y%m%dT%H%M%S")

    origin_marker = build_item("blueSphere", "sphere_base", "sphere", 0, 0, 0, 1, 0, 0, 1)

    file_contents = file_header() + origin_marker

    for x in range(count):
        file_contents = file_contents + build_item("sphere_" + "{id}".format(id=x), "sphere_base", "sphere", random.uniform(minX, maxX), random.uniform(minY, maxY), random.uniform(minZ, maxZ), random.uniform(minRadius, maxRadius), random.random(), random.random(), random.random())
    print(file_contents)
    write_file(file_contents, destination_file_name + ".usd")

Using the following sphere_base.usd file:

#usda 1.0
(
    defaultPrim = "my_shape"
)

def Xform "my_shape"
{
    double3 xformOp:translate = (0, 0, 0)
    uniform token[] xformOpOrder = ["xformOp:translate"]

    def Sphere "sphere"
    {
        float3[] extent = [(-2, -2, -2), (2, 2, 2)]
        color3f[] primvars:displayColor = [(0, 0, 1)]
        double radius = 2
    }
}

I’d say this is a fairly ill-advised approach to working with USD files, but it worked! No installs required.

Sharing the work.

There is currently no good online viewer for USD geometries. I consider that a pretty significant weakness.

I did experiment with importing into Blender and then exporting to .x3d, however the mesh color data does not arrive into Blender with the current importer? It was a tick box… did I miss it? I was pretty excited that the model came though! The color isn’t a material in this case. Perhaps if it was it would? As a note for future work, the color information can be added into the .x3d looking something like:

<Appearance>
    <Material diffuseColor='1 0 0'/>
</Appearance>

There is an opportunity to create a what is referred to as a rendering delegate here, and some people have started that work, but it seems to have stalled.

Here is the x3d as I’ve played around with it a bit. see raw

Next… adding a Material!