EP.009 — ← back to log

Procedural Modeling and Animation

Three Houdini systems from VFX 721: a Chinese stilt house, a randomized warehouse, and an animated flat-six engine.

VFX 721
Prof. Deborah Fowler
Spring 26
Houdini 21 · VEX · Copernicus · Karma XPU
Oct 7, 2026

VFX 721 with Prof. Deborah Fowler is about building systems that generate 3D content instead of modeling each piece by hand. The class runs like a studio: hard deadlines and work-in-progress shown dailies-style. Everything is built in Houdini, mostly with VEX, across three projects: a procedural building, a complex scene, and a procedural animation.

Project 1: Procedural Building

The brief: a procedural system that generates a building's exterior. I chose the diaojiaolou (吊脚楼), the wooden stilt houses of southwest China built out over rivers and hillsides. Stacked roofs, lattice windows, and dozens of stilts make it a good test of repetition with variation.

Reference photo of a riverside stilt house beside the procedural Houdini render
Reference (left, via Xiaohongshu) vs. render (right)

Controls

One global seed drives every random choice, with controls grouped into four tabs:

Final render of the stilt house from a low angle
Final render
Close-up of lattice windows, railings, and roof tiles
Window and railing patterns

Roof

The roof profile is built from three points referenced relative to each other, so it follows the roof-height control. A for-each loop copies the sloped edge to form uneven rows of tiles, and each tile is oriented along the slope with a quaternion:

float opposite = ch("roof_height");
float adjacent = getbbox_max("op:../../WALL_SYSTEM/BND_ADJACENT").z
               - getbbox_min("op:../../WALL_SYSTEM/BND_ADJACENT").z;
vector slope_dir = normalize(set(0, adjacent, opposite));

matrix3 m = dihedral(set(0,-1,0), slope_dir);
p@orient = quaternion(m);

Randomly removing points along the curves gives broken, missing tiles. Offsetting tiles along the roof normal creates the overhang. Tile color and the curvature of each ridge are also randomized from the global seed.

Close-up of randomized roof tiles
Randomized tiles, color, and gaps
Windows opened at random angles
Window openness per building

Stilts

A Point Wrangle lays out the stilt grid with a little jitter, and the stilts are projected down onto the terrain, so their length adapts to whatever ground they stand on.

One catch: the orient attribute rotates around the pivot, which sits at the origin. So I moved the terrain and stilts down, oriented the stilts, then moved everything back up. The horizontal beams reuse the stilt points: a curve through them, then PolyWire.

Randomization

Almost everything varies: window pattern and openness, the vertical log in front of each window, wall height, railing pattern, and the wood texture on the logs. Most of it runs on for-each loops, since each iteration number makes an easy, unique seed. I find them more versatile than Copy and Transform or Copy to Points. For example, this nudges each roof up or down by up to 0.03, combining the global seed with the floor, column, and tile iterations:

fit(rand(ch("../../global_seed")*636
       + detail("../../floor/", "iteration", 0)*3754
       + detail("../../row", "iteration", 0)*13.86
       + detail("../meta_tile", "iteration", 0)), 0, 1, -0.03, 0.03)

The stone wall behind the stilts is a procedural texture built in Copernicus.

A taller configuration of the stilt house with many floors
Another configuration from the same system

Render: Karma XPU, 1080×1920, 128 samples, about 36 seconds per frame. Every piece is packed, so the default building is only 87 packed primitives.

Project 2: Complex Scene

The brief: a full environment that combines several procedural systems. I built a warehouse that generates shipping skids stacked with cardboard boxes. Each skid gets its own seed from its for-each iteration, so no two shelves look the same.

Final render of warehouse shelves stacked with randomized boxes
Final render
Reference photo of a warehouse beside the procedural render
Reference (left, iStock) vs. render (right)

Two stacking patterns

Each skid randomly gets one of two patterns: moved boxes, loose stacks that have been handled, or full boxes, a tight, untouched block. Both are point-generating VEX wrangles, written with AI assistance.

Moved boxes treats each grid point as the base of one stack:

Full boxes builds a 2D grid instead. Each row gets a random number of cells, each cell a random width and depth. A running cursor packs cells tightly along X, and rows are spaced by their deepest cell, so nothing overlaps.

Labels and tape

The box labels come from three sources: a receipt image found online, a care-label texture built with Copernicus nodes, and a barcode written in Copernicus code. For the barcode, low-frequency noise warps the X coordinate before sampling a higher-frequency noise, and smooth() sharpens the result into crisp stripes:

float warp = noise(@P.x * 3) * 2.0;
@C = smooth(0.5, 0.5, noise((@P.x + warp) * 20));

Inside a for-each loop over the top faces, a Switch picks the receipt or the label, Match Size fits it to the face, and two Transforms add random offset and rotation. Some labels are randomly removed so they don't repeat.

The tape uses a Ray SOP with minimum distance, which only projects in one direction. To wrap tape around a box, I projected half a strip and mirrored it to the other side, then mirrored again for a second strip.

Details

Shelf numbers are generated by an expression that counts rows from the top down and zero-pads them ("01", "02"…). Full stacks randomly get plastic wrap, and loose stacks get small objects scattered on top. Column guards and nails finish the racks.

Close-up of loose box stacks with labels, receipts, and a numbered shelf beam
Labels, receipts, and shelf numbers
Plastic-wrapped full box stack on a pallet
Plastic wrap on a full stack

Render: Karma XPU on an RTX 3090, 1920×1440, 128 samples, 5 lights, about 11,000 packed primitives, about 1 min 42 s per frame.

Project 3: Procedural Animation

The brief: animate a mechanical system. The suggested examples include kinetic sculptures, carnival rides, automatons, and wind-up toys, with CHOPs used to keep motion in sync. I animated a flat-six engine, based on a Porsche display model. Pistons, valves, gears, and timing chains are all driven from a single crankshaft input. Nothing is hand-keyed: every part takes its speed and phase from real mechanical relationships, and one speed control re-times the whole engine while keeping every ratio correct.

Render of the animated flat-six engine: pistons, crankshaft, camshafts, and timing chains
Final render
Reference display engine beside the Houdini render
Reference (left, via YouTube) vs. render (right)

Pistons

Each piston is solved as a fixed-length linkage. Points at the pin ends of the crankshaft, rod, and piston define the joints. The crank pin rotates with the crankshaft and supplies the driving position. Because the connecting rod has a fixed length R, the piston's position follows from it: measure the vertical distance between crank pin and piston (changeY) each frame, then recover the rod's horizontal reach with Pythagoras, √(R² − changeY²). The piston slides in its bore while the rod swings, and all three parts stay connected.

R = ch("../lengthofRod");
y0 = point("../crankshaft_trans/", 5595, "P", 1);
y1 = ch("../heightofRod");
changeY = y1 - y0;
changeX = sqrt(R * R - changeY * changeY);
x1 = point("../crankshaft_trans/", 5595, "P", 0) + changeX - 0.5;
return x1;

Valves

Valve lift is calculated from bounding boxes, so it stays in sync with the camshafts.

Gears and chain

The chain is built with the Chain node, which arrays one link along a curve. A single control drives it: the chain's position offset advances by $T * speed, sliding every link along the curve.

The gears aren't animated against time on their own. They read their rotation from the chain's position offset, which keeps every tooth in the gaps between links:

The divisor sets how much chain travel equals one full turn, tuned to each gear's tooth count. Because the cam divisor is twice the crank's, the camshaft turns once for every two crank revolutions: the real 2:1 timing reduction. The extra timing belt on the front of the engine is matched to the same speed.

What I learned


← back to the log