Skip to content

Frustum culling in LOPS

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