Pscale by the camera Link Icon

I've described how we can set a unified pscale by the camera in SOPs here which uses this simple algebraic approach: $$ \text{Pixel Size} = \left(\frac{\text{Aperture}}{\text{Focal Length}}\right) \times \left(\frac{\text{Distance}}{\text{Resolution}}\right) $$ However, if we want to set up the same behavior in LOPs, you will notice that getting distance (Pz) by NDC or UV might not be straightforward, as it would require jumping back into the SOP/OBJ context.

As an alternative, we can calculate it by projecting the vector from the camera to the point onto the camera Z. You can also read more about vector projections here

VEXpression
string campath = chs("camera");
float apertH = usd_attrib(0, campath, "horizontalAperture");
float apertV = usd_attrib(0, campath, "verticalAperture");
float focal = usd_attrib(0, campath, "focalLength");
float resx = chf("render_resolution_x");
float pixsize = apertH / focal ;

// get camera position
matrix camM = usd_worldtransform(0, campath);
vector camPos = set(camM.wx, camM.wy, camM.wz);

// vector from cam to point
vector camPtoP = v@points-camPos;
// camera direction vector
vector camDir = -set(camM.zx, camM.zy, camM.zz)/100;
// projection distance and direction
float dotC = max(0, dot(camPtoP, normalize(camDir)));
// projected vector
vector ProjZ = dotC*normalize(camDir);
float Pz = length(ProjZ);

// pscale size of pixel (based on camera RESOLUTION)
f@widths = pixsize * (Pz/resx);

// adjust unified pscale by user input
f@widths *= ch("pscale_multi");

lop-camera-pscale-1

Frustum culling in LOPS Link Icon

Frustum culling in LOPs has been a topic since the beginning of the Solaris context. As always, there are many ways to skin a cat, especially when it comes to Houdini. Ideally, we want to keep everything within the LOP context without creating SOP context nodes for intermediate computations, as that could drastically slow our workflows down.

SideFX gave us amazing primitive matching patterns like bound, where we can specify elements that are within the camera frustum:

VEXpression
%bound(/cameras/camera1, fovscale = 1.1)

lop-frustum-cull-1
But what about the geometry points or the instance points ?. toNDC() function works great with OBJ cameras but it won't accept LOP cameras. We can, of course, import camera into OBJ level and reference it that way:
lop-frustum-cull-2

If we want to keep everything within the LOP context, one way around is to calculate NDC ourselves.

VEXpression
vector toLopNDC(string primpath; string camera; vector position; float frame){
    float focalLength = usd_attrib(0, camera, "focalLength", frame);
    float horizontalAperture = usd_attrib(0, camera, "horizontalAperture",frame);
    float verticalAperture = usd_attrib(0, camera, "verticalAperture", frame);
    float screenWindowWidth = tan(horizontalAperture / (2 * focalLength)) * 2;
    float screenWindowHeight = screenWindowWidth * verticalAperture / horizontalAperture;
    vector pos_world    = position * usd_worldtransform(0, primpath, frame);
    vector pos_cam      = pos_world * invert(usd_worldtransform(0, camera, frame));
    vector posCamNorm = set(pos_cam.x/pos_cam.z, pos_cam.y/pos_cam.z, -pos_cam.z);
    vector posNDC = set(
        0.5 - (posCamNorm.x / screenWindowWidth),
        0.5 - (posCamNorm.y / screenWindowHeight),
        posCamNorm.z
    );
    return posNDC;
}

vector ndc = toLopNDC(@primpath, chs("camera"), v@positions, @Frame);
float overscan = 0.1;
if(ndc.x>=0-overscan && ndc.x <=1+overscan)
    v@scales *= 0.3;

lop-frustum-cull-3

Decimate curves Link Icon

You've got lots of curves in the LOP context and want to decimate without going back to SOPs? You are in luck!, USD stores 'curveVertexCounts' array with number of points per each primitive. We can modify this array by removing unwanted primitives

lop-decimate-curve

VEXpression
int nums = usd_attriblen(0, @primpath, "curveVertexCounts");
int curve_vtx_num[] = usd_attrib(0, @primpath, "curveVertexCounts");
i[]@curveVertexCounts = {};
for (int i=0; i<nums; ++i){
    if (rand(i+ch("seed")) < ch("prune_ammount"))
        push(i[]@curveVertexCounts, curve_vtx_num[i]);
}

Assigning materials from string attribute Link Icon

The gist of it:

To get a material assigned automatically to a lop primitive, the string attribute that needs to be on the sop primitives is called usdmaterialpath. It must point to the material in the scene graph, not the material node itself.

Here there are 2 materials. In order to be visible in the scene graph, we need to add them in the material library parameter tab.


Linking the material to the object

To get the usdmaterialpath attribute to actually be used in solaris, we need to change the bind material parameter of the sop import to Create and Bind Material Based on Imported Attributes". This will create an empty lop primitive and we can see the "blue" material is assigned to the object. BUT the material is currently only an empty primitive. Not a problem, when checking the material library node where the actual material lives, since the path is the same, i.e /materials/blue, the empty primitive gets populated with the material and the object is now blue.


Other workflows:

I personally prefer the workflow above, as I like having my sop imports in different branches, merged together and then have a material library, but if we place the material library first and then add the sop import, we don't need to set the bind material parameter to Create and Bind Material Based on Imported Attributes". Since the materials already exist, we don't need to create an empty one. The sop import will find it and assign the object to it with the "Bind Material Based on Imported Attribute" option of the bind material parameter.