Solaris USD Asset Turntable

Posted: 2023-05-02 | Updated: 2023-05-09

We have been watching the development and growing support from DCCs for USD over the last couple of years. Now after wrapping up on Star Wars Visions 2 and Kizazi Moto, we have a small gap between projects and have decided to take the leap. Arnold (our primary renderer) along with houdini's support for USD feel like they have matured enough now for us to take the plunge.

The first task that I focussed on was Asset Build, and considering our need for rapid development the component builder ticked most of our boxes so I spent a couple of days wrapping it into an HDA that made it a bit more artist friendly (we have since deprecated this hda for an asset build workflow I built from scratch that fits our requirements & pipeline better). The very next requirement though is for the modelling & lookdev teams to take thier assets and have them reviewed. Thus my focus shifted to a solaris based turntable that could take the usd file being published and get it up on shotgun.


The initial requirements were pretty simple, import the asset, determine its size, create a backdrop that can be scaled to the fit the asset size, place the bottom of the asset on the origin (if not already modelled above 0,0,0), rotate it by 360° degrees whilst also placing the camera at the correct distance to frame the asset correctly. We decided to take it a bit futher adding a tumble, and shader overide allowing us to render a wireframe too.

Creating the Turntable

Asset Prep

First and foremost we import the asset using an Asset Reference node, we then set any specific variants (mtl or geo) required. Our modelling team use either Blender or Maya, so we kept our model exports simple. We make sure to place them in a USD friend hirerachy so the models get placed in /reveiw_asset/geo/render. This ultimately get set as a scope and becomes the render purpose so as a catch, incase the file being reviewed is not from the component builder, but from Maya or Blender I drop down a Configure Primitive and set Type: Scope and Purpose: Render.

Object to 0,0,0

To place the object on the origin correctly I needed to get the bounding box of the primitive and move it upwards accordingly. Houdini has introduced a bunch of new vex functions specifically for USD that can be used, so I drop down an Attribute Wrangle retrieve the minimum bounding box value which is a vector x,y,z and so min[1] represents the y value. I then create a new vector that is the inverse of this using only the y axis and translate the object based on this value. Here is the code snippet for this:

// move object up to sit on Origin
vector min = usd_getbbox_min(0, @primpath, “render”);
vector asset_bottom = set(0, -min[1], 0);
usd_addtranslate(0, @primpath, "translate", asset_bottom);

Backdrop & Rotation

Very simply the next node is a Transform targeting /review_asset with the y axis animated with rotating 360°. Starting at 30° and ending at 390°.

The backdrop is created using a SOP Create by placing a grid, polyextrude, etc to create a base & backdrop for the asset to be place onto. Using the Match Size node selecting the SOP Create prim and target prim set to %type:Mesh /review_asset I scale X, Y, Z to fit the bounds of the review asset. This is done with some python code inside of the Scale Paramater by pressing ctl+e. The code uses the pxr module to ComputeWorldBounds via UsdGeom.Imageable I then use math.hypot to calculate the 'length' of the object which I then use to set the scale

Edit: This python code became increasingly complex and although working seemed simpler to use the vex functions I learned later. I decided to instead use the usd_addscale function to make sure the backdrop was big enough. Scaling the backdrop by the value of the objects size

// get object diameter
vector bbox_size = usd_getbbox_size(0,"/review_asset",“render”);
float object_diameter = length(bbox_size);

if (object_diameter <= 1){
    object_diameter = 1;

// create scale attrib
vector xform = set(object_diameter, object_diameter/2, object_diameter/2);

// Set Sphere & MacBeth Positions
usd_addscale(0, "/backdrop", "my_scale", xform);

Positioning the Camera

Next step was to drop down a Camera, 50mm works nicely for asset reviews so no changes needed on the camera node. I set the prim path to /cameras/$OS with the node name being review_cam.

Now I jumped back to some vex in another wrangle node, the plan is to set the position of the camera based on the object size. This is where it got exciting having to learn a bunch of new usd specific vex functions. First I need to get the size of the review_asset:

// get object diameter
vector bbox_size = usd_getbbox_size(0,"/review_asset",“render”);
float object_diameter = length(bbox_size);
I knew that I would later want to translate the camera up slightly, so I decided to calculate this based on weather height or width was greater:

Edit: After lookdev begun publishing assets I realised that if width was greater than height it could translate the camera upwards too far and so have adjusted this code to only work based on height.

float height = bbox_size[1];
float width = bbox_size[0];

float cam_height = hieght/2;
// if (width > height){
//        cam_height = width/2;
// }
then I need to get the focal length and aperture of the camera:

// get the focal length and aperture width of the camera
float focal = usd_attrib("opinput:0", @primpath, "focalLength");
float aperture = usd_attrib("opinput:0", @primpath, "horizontalAperture");
next it was time to calculate the horizontal field of view:

// calculate the horizontal field of view (fovx)
float apx = (aperture / 2) * (3.14159265359 / 180);
float fovx = degrees(2 * atan(apx / focal));
float fov = fovx * 100.0;

some of you might have noticed I multiply the fovx by 100 this is because I noticed that even though I set the focal length on the camera to 50mm the USD was storing that as 0.5 and so I needed to bring it back to the values I was expecting:

usd turntable focal length

next I calculated the distance that camera needs to be place away from the object, I also create a channel to allow me to promote a padding value that can be used as a multiplier:

// calculate distance with padding
float camera_distance = (object_diameter / tan(radians(fov / 2.0)));
float padding = chf("padding_value");
float camera_pad_distance = camera_distance * padding;
its pretty simple from here, we just set the position of the camera by creating our own vector and using the usd_addtranslate function

// set camera position
vector xform = set(0, cam_height, camera_pad_distance);
usd_addtranslate(0, @primpath, "my_translation", xform);
you can use this same xform to position other objects that you want to follow the camera and stay in view, such as chrome/matte spheres or a macbeth grid for example.

Finally I set a fixed -15° rotation on the X axis to the camera using a transform node.

Wrapping Up

Some finishing touches might need to be added such as using the Render Geometry Settings node to setup any asset subdivisions, shadow or ray visibility settings as required.

Once we have rotated the object 360° we spin the lights by 360° too, but that is done separately by an asset review light rig created by our lighting supervisor. We then render these turntables on the farm, converting them to mp4 ready for upload to shotgun.