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.
Controls
One global seed drives every random choice, with controls grouped into four tabs:
- Basic: floors, columns, floor height, roof height.
- Stilts: rows, columns, and spacing in each direction.
- Rail: railing height and spacing.
- Window: maximum window opening angle.


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.


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.
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.
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:
- @ptnum as the seed: every point builds an independent stack, with no loop needed and parallel by default.
- Decorrelated seeds: different multipliers and offsets for count, height, jitter, and rotation, so changing the seed doesn't move them all in lockstep.
- v@scale instead of @pscale: boxes can be tall or short while keeping their footprint.
- @orient: built from X, Y, and Z quaternions, so Copy to Points picks it up automatically.
- Snap rotation: small yaw jitter plus an occasional 90° turn, like a person loading a pallet.
- Top-box flag: marks the last box in each stack, so labels and scattered objects go only on top.
- Running height: each box starts where the last one ended, so stacks stay flush at any size.
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.


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.
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:
- Crank gear:
-(posoffset / 10) * 360 - Cam gears:
-(posoffset / 20) * 360
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
- Build the system, not the model. One seed and a few controls can produce a whole street of stilt houses or a whole warehouse. Time spent on the system pays off in every variation.
- For-each loops are my default tool. Every iteration gives a free, unique seed, which makes per-object randomness easy to control.
- Separate your random seeds. If count, scale, and rotation share a seed, they change together and the result looks artificial. Separate seeds keep each one independent.
- Randomness needs rules. Snapping a box to 90°, or dropping a few roof tiles, reads as real only because it follows how people load pallets and how roofs age.
- Drive animation from real mechanics. Deriving the pistons, gears, and chain from one crankshaft input kept everything in sync. Changing the speed never broke the timing.
- Know where your pivot is. Orient attributes rotate around the origin, so moving geometry to the origin, rotating, and moving it back saved me from rebuilding the stilts.
- Reference first. Each project started from a real photo or model and ended with a side-by-side comparison, which made it obvious what still looked wrong.