Hello USD - Part 20: Hello usdrecord

This article is part of a series.

I’ve mostly been looking at my usda files as screen grabs from QuickLook. It came up on the ASFWG slack that the utility usdrecord, included in the default build, will make png files. Several other types, too!

usage: usdrecord [-h] [--mask PRIMPATH[,PRIMPATH...]] [--camera CAMERA]
                 [--defaultTime | --frames FRAMESPEC[,FRAMESPEC...]]
                 [--complexity {low,medium,high,veryhigh}]
                 [--colorCorrectionMode {disabled,sRGB,openColorIO}]
                 [--renderer {GL,Embree,Prman}]
                 [--imageWidth IMAGEWIDTH]
                 usdFilePath outputImagePath

What it looks like

The veryhigh complexity setting for usdrecord makes positively luscious output. The default output width is 960 pixels. The documentation says it will then pick the width based on the camera’s aspect ratio. I don’t have a camera in my scene so I was curious to find out that the height would be. Turns out it was 700 pixels, TIL the Academy Ratio is a thing! Also, because I don’t have any animation stored in this USD file, I did not need to add the ###’s to the name for the frame numbers. Like with usdcat, the output filename chooses the file type.

usdrecord --complexity veryhigh input.usda output.png
usdrecord QuickLook

I then made a set of bigger images (5700 pixels across), one at each complexity, and sliced them up with imagemagick to compare them.

usdrecord --complexity veryhigh --imageWidth 5700 input.usda output.png
convert finalring_forT_vh_5700.png -crop 250x250  +repage  +adjoin veryhigh/finalring_vh_%02d.png
convert finalring_vh_133.png -crop "+170+170" -resize 240x240 closeup_vh.png

The table below shows one of the 250x250 sections cut out of each of the complexity examples in the top row. In the second row a close up of the edge of the corresponding sphere in the top row.

Low Med High Very High

The png files usdrecord makes are a bit chonky. They’re well served by a slim down. I used ImageOptim, for convenience. What is nice? That chonkness comes completely from the pixel data’s lack of compression. usdrecord does not load the files up with meta data or anything like that.

usdrecord Under The Hood

I haven’t touched the rendering aspects of OpenUSD at all, and have very little understanding of what Hydra is and how it works. PNG files, on the other hand, I feel like I have a small handle on those. So I went looking for what was under the hood because that may have been my way into the Renderer/RenderDelegate aspect of OpenUSD. I may use the term renderer imprecisely in the following section, per what it means in OpenUSD specifically. I’m still trying to get what role a capital “R” renderer does and what it doesn’t.

The trail starts with usdrecord a shebanged python script that lives in the bin folder. I can’t otool check the dependencies, because it’s not actually a binary.

I did take special note of line 190: frameRecorder = UsdAppUtils.FrameRecorder(rendererPluginId, args.gpuEnabled). This frameRecorder receives all the CLI’s arguments, and actually appears to do the writing to disk with frameRecorder.Record on line 203.

I found its definition at https://github.com/PixarAnimationStudios/OpenUSD/blob/10b62439e9242a55101cf8b200f2c7e02420e1b0/pxr/usdImaging/usdAppUtils/frameRecorder.cpp#L49

Note that this is a C++ file, so the compiled C++ has to live somewhere. This looked promising:

cd $BUILD_DESTINATION/build/USD/pxr/usd/usdUtils/
otool -L ./libusd_usdUtils.dylib

Got me this information:

./libusd_usdUtils.dylib:
	@rpath/libusd_usdUtils.dylib (compatibility version 0.0.0, current version 0.0.0)
	@rpath/libusd_usdShade.dylib (compatibility version 0.0.0, current version 0.0.0)
	@rpath/libboost_python39.dylib (compatibility version 0.0.0, current version 0.0.0)
	@rpath/libusd_usdGeom.dylib (compatibility version 0.0.0, current version 0.0.0)
	@rpath/libusd_usd.dylib (compatibility version 0.0.0, current version 0.0.0)
	@rpath/libusd_kind.dylib (compatibility version 0.0.0, current version 0.0.0)
	@rpath/libusd_pcp.dylib (compatibility version 0.0.0, current version 0.0.0)
	@rpath/libusd_sdr.dylib (compatibility version 0.0.0, current version 0.0.0)
	@rpath/libusd_ndr.dylib (compatibility version 0.0.0, current version 0.0.0)
	@rpath/libusd_sdf.dylib (compatibility version 0.0.0, current version 0.0.0)
	@rpath/libusd_ar.dylib (compatibility version 0.0.0, current version 0.0.0)
	@rpath/libusd_vt.dylib (compatibility version 0.0.0, current version 0.0.0)
	@rpath/libusd_gf.dylib (compatibility version 0.0.0, current version 0.0.0)
	@rpath/libusd_plug.dylib (compatibility version 0.0.0, current version 0.0.0)
	@rpath/libusd_work.dylib (compatibility version 0.0.0, current version 0.0.0)
	@rpath/libusd_trace.dylib (compatibility version 0.0.0, current version 0.0.0)
	@rpath/libusd_js.dylib (compatibility version 0.0.0, current version 0.0.0)
	@rpath/libusd_tf.dylib (compatibility version 0.0.0, current version 0.0.0)
	@rpath/libusd_arch.dylib (compatibility version 0.0.0, current version 0.0.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.100.3)
	/Users/labtanza/.pyenv/versions/3.9.17/lib/libpython3.9.dylib (compatibility version 3.9.0, current version 3.9.0)
	@rpath/libtbb.dylib (compatibility version 0.0.0, current version 0.0.0)
	/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 1500.65.0)

Hunh. No libpng, which I was expecting to see.

frameRecorder.cpp does have functions that use the parameters passed into usdrecord along with a the timestamp to move the camera and resolve to scene graph to provide the renderer with the information it needs to actually do the rendering. That renderer appears to be an parameter in the initializer: _imagingEngine(HdDriver(), rendererPluginId, gpuEnabled).

Also in framerRecorder.cpp is the definition for a TextureBufferWriter. Its initializer takes in the FrameRecorder’s _imagingEngine, so on line 154 we can see its of type UsdImagingGLEngine. Additionally we can learn that the TextureBufferWriter write function uses HioImage::OpenForWriting.

HIOImage appears to have a subclass, Hio_StbImage that uses the stb library to do its reading and writing.

AND stb does not appear to use our friend libpng, it compresses the data itself.

   The PNG output is not optimal; it is 20-50% larger than the file
   written by a decent optimizing implementation; though providing a custom
   zlib compress function (see STBIW_ZLIB_COMPRESS) can mitigate that.
   This library is designed for source code compactness and simplicity,
   not optimal image file size or run-time performance.

Totally reasonable trade off as the stb library means usdrecord could potentially save JPEG, PNG, TGA, BMP, PSD, GIF, HDR, PIC and PNM with very very little extra code. What usdrecord will output: [“bmp”, “jpg”, “jpeg”, “png”, “tga”, “hdr”]

I had been looking for something that depended on libpng because the build_usd.py script manages a libpng install, although… none of the code in the Pixar/OpenUSD repo directly includes a png.h. The optional install of OpenImageIO, which gets used for the “loading and saving of texture files” does use it. (build_usd.py line 2300) I am unsure at the moment which parts of OpenUSD will call OpenImageIO, if installed.

Like with stb there also seems to be a subclass of HIOImage that uses OpenImageIO? But only for the image types not handled by stb? These types are NOT made available to usdrecord from what I can tell trying to use them on a default build.

Fin

I don’t have a totally firm grasp on how OpenUSD picks which subclass to use for its HioImage::OpenForWriting. I’m intrigued that there seems to be a plugin architecture. I do feel good about tracking the journey of a little ol’ usda all the way from ascii to png.

Issue to keep an eye on: https://github.com/PixarAnimationStudios/OpenUSD/issues/2556

This article is part of a series.