Thinking Procedurally
The biggest shift when coming to Houdini from other DCCs is the mindset change. In Maya or Blender, you sculpt a specific result. In Houdini, you build a system that generates results. Every node is a step in an instruction set - change the input, change a parameter, and the entire output updates.
Key principles of procedural thinking:
- Parameterize everything - Hard-coded values become parameters. Magic numbers become channel references (
ch("my_param")). - Think in data, not shapes - Geometry is just points with attributes. A wall is a set of points with
@height,@thickness, and@material. - Design for variation - If your setup only works for one specific input, it's not procedural. Test with different inputs early.
- Separate concerns - One node does one thing. Chain them together to build complex behavior from simple steps.
Before building a network, write out the steps in plain language: "For each building footprint -> extrude to random height -> add windows based on floor count -> assign material by zone." Then translate each step into a node or group of nodes.
For-Each Loops and Feedback Loops
Loops are one of Houdini's most powerful SOP-level features. They let you apply operations to individual pieces of geometry or iterate a process multiple times.
For-Each Connected Piece
Processes each connected piece of geometry independently. The Block Begin (set to "Fetch Piece") and Block End (set to "Merge Each Piece") define the loop boundary.
Common setup:
- Use a Connectivity SOP to generate a
classattribute - Add a For-Each Named Primitive block (or use
@class) - Inside the loop, each iteration sees only one piece
- Transform, modify, or process each piece independently
// Pre-loop: assign unique piece IDs and random values
// Run this Wrangle BEFORE the for-each loop
// Give each piece a random scale and rotation
int piece = i@class;
float seed = float(piece) * 13.37;
f@rand_scale = fit01(rand(seed), 0.6, 1.4);
f@rand_rot = fit01(rand(seed + 1), -30, 30);
v@centroid = getbbox_center(0);
// Inside for-each loop: transform each piece independently
// This runs on each piece in isolation
vector center = getbbox_center(0);
float scale = f@rand_scale;
float rot_deg = f@rand_rot;
// Move to origin, scale, rotate, move back
@P -= center;
@P *= scale;
float angle = radians(rot_deg);
matrix3 m = ident();
rotate(m, angle, {0, 1, 0});
@P *= m;
@P += center;
Feedback Loops (Iterative Solving)
Feedback loops feed the output of one iteration back as the input to the next. Perfect for growth algorithms, relaxation, and iterative refinement.
// Feedback loop: iterative point relaxation
// Block Begin set to "Fetch Feedback", iterations = 20
// Average each point's position with its neighbors
int nbs[] = neighbours(0, @ptnum);
if(len(nbs) == 0) return;
vector avg = {0, 0, 0};
foreach(int nb; nbs) {
avg += point(0, "P", nb);
}
avg /= float(len(nbs));
// Blend toward average (relaxation factor)
float blend = chf("relax_strength"); // 0.1-0.5 works well
@P = lerp(@P, avg, blend);
Feedback loops cook every iteration sequentially and cannot be parallelized across iterations. Keep the geometry count low inside loops, or use VEX solver approaches in DOPs for heavy simulations.
Building Reusable HDAs
Houdini Digital Assets (HDAs) package a node network into a single reusable operator with a custom interface. They're how TAs deliver tools to artists.
HDA Design Guidelines
- Define clear inputs and outputs - Label them (e.g., "Base Geometry", "Guide Curves"). Use Null nodes named
OUTfor the final output. - Promote key parameters - Right-click any parameter -> "Promote to Type Properties" to add it to the HDA's interface. Group related params into folders.
- Add help and tooltips - Fill in the Help tab in Type Properties. Add tooltips to every promoted parameter.
- Version your HDAs - Use namespaced operator names like
studio::wall_generator::2.0. Bump the version when the interface changes. - Handle edge cases - Add error checking with a Python SOP or VEX that validates input geometry.
// Input validation wrangle (run over Detail, inside HDA)
int npts = npoints(0);
int nprims = nprimitives(0);
if(npts == 0) {
error("No input geometry. Connect geometry to input 1.");
}
if(!hasattrib(0, "point", "N")) {
warning("No normals found. Adding default normals.");
}
// Store metadata for downstream nodes
setdetailattrib(0, "_input_ptcount", npts);
setdetailattrib(0, "_input_primcount", nprims);
Store your HDAs in a shared $HOUDINI_PATH location (e.g., a studio OTL library). Use Embedded HDAs only for project-specific one-offs. For team-wide tools, always use file-based HDAs under version control.
TOPs/PDG for Batch Processing
Task Operator Network (TOPs) powered by the Procedural Dependency Graph (PDG) lets you run work items in parallel - across frames, files, or variations. It's Houdini's answer to batch rendering, data processing, and wedging.
Common TOP Patterns
- Wedging - Generate parameter variations automatically and cook them in parallel
- File processing - Read a directory of files, process each one, and write results
- Frame rendering - Distribute frame ranges across local cores or farm machines
- Data pipelines - Chain CSV ingestion -> geometry generation -> export
# Python Processor TOP - generate work items from file list
# Place this in a Python Processor node's "Generate" callback
import os
asset_dir = self.parm("asset_directory").eval()
extensions = (".bgeo.sc", ".obj", ".fbx")
for filename in os.listdir(asset_dir):
if filename.endswith(extensions):
item = item_holder.addWorkItem()
filepath = os.path.join(asset_dir, filename)
item.setStringAttrib("filepath", filepath)
item.setStringAttrib("asset_name", os.path.splitext(filename)[0])
item.setIntAttrib("index", item_holder.numWorkItems() - 1)
# Wedge setup with Python - generate parameter combinations
# In a Python Processor TOP
import itertools
noise_freqs = [0.5, 1.0, 2.0, 4.0]
seed_values = [0, 42, 99, 256]
subdivisions = [2, 3, 4]
for freq, seed, subdiv in itertools.product(noise_freqs, seed_values, subdivisions):
item = item_holder.addWorkItem()
item.setFloatAttrib("noise_freq", freq)
item.setIntAttrib("seed", seed)
item.setIntAttrib("subdivisions", subdiv)
label = f"freq{freq}_seed{seed}_sub{subdiv}"
item.setStringAttrib("wedge_label", label)
Use the built-in Wedge TOP for simple parameter sweeps - no Python needed. Reserve Python Processor TOPs for complex logic like reading databases, filtering file lists, or generating work items from API responses.
Organizing Complex Networks
Large Houdini projects can become unwieldy fast. These conventions keep networks readable for you and your team.
Network Hygiene Rules
- Name every node -
scatter_treesnotscatter2. Use lowercase with underscores. - Use Null nodes as bookmarks - Name them
OUT_terrain,CTRL_settings,REF_guide_curves. Common prefixes:OUT_- Final output of a sectionCTRL_- Parameter controller node (artists interact with this)REF_- Reference geometry (not processed further)
- Use Subnet or HDA for logical grouping - Each major stage (terrain -> scatter -> buildings -> export) should be its own subnet or HDA.
- Color-code nodes - Pick consistent colors: green for output, blue for input references, yellow for debug/visualization.
- Add Sticky Notes - Document why, not what. "Offset needed to compensate for pivot mismatch in FBX export" is useful. "This is a transform" is not.
Use Network Boxes (select nodes -> press Shift+O) to visually group related nodes without creating subnets. They're lighter than subnets and don't create new scoping contexts. Label them clearly.
Working with External Data
Real production pipelines feed data into Houdini from external sources - CSV files, JSON configs, databases. Here's how to ingest and use that data.
Reading CSV Data
# Python SOP - read CSV and create points from coordinates
import csv
node = hou.pwd()
geo = node.geometry()
geo.clear()
csv_path = node.parm("csv_file").eval()
with open(csv_path, "r") as f:
reader = csv.DictReader(f)
for row in reader:
pt = geo.createPoint()
pt.setPosition(hou.Vector3(
float(row["x"]),
float(row["y"]),
float(row["z"])
))
# Transfer any extra columns as attributes
if "name" in row:
if geo.findPointAttrib("name") is None:
geo.addAttrib(hou.attribType.Point, "name", "")
pt.setAttribValue("name", row["name"])
if "scale" in row:
if geo.findPointAttrib("pscale") is None:
geo.addAttrib(hou.attribType.Point, "pscale", 1.0)
pt.setAttribValue("pscale", float(row["scale"]))
Reading JSON Configuration
# Python SOP - read JSON config and set detail attributes
import json
node = hou.pwd()
geo = node.geometry()
json_path = node.parm("config_path").eval()
with open(json_path, "r") as f:
config = json.load(f)
# Store each config value as a detail attribute
for key, value in config.items():
if isinstance(value, (int, float)):
geo.addAttrib(hou.attribType.Global, key, value)
geo.setGlobalAttribValue(key, value)
elif isinstance(value, str):
geo.addAttrib(hou.attribType.Global, key, "")
geo.setGlobalAttribValue(key, value)
elif isinstance(value, list) and len(value) == 3:
geo.addAttrib(hou.attribType.Global, key, hou.Vector3())
geo.setGlobalAttribValue(key, hou.Vector3(value))
// Read the JSON-loaded detail attributes downstream in VEX
float grid_size = detail(0, "grid_size");
string biome = detail(0, "biome_type");
int seed = detail(0, "random_seed");
// Use config values to drive generation
float freq = grid_size * 0.1;
@P.y = snoise(@P * freq + seed) * detail(0, "height_scale");
Best Practices for Production
These patterns separate hobby projects from production-ready Houdini setups:
- Lock critical file paths with
$HIP- Always reference files relative to$HIP(the scene file location) or$JOB(the project root). Never use absolute paths. - Use Take system for variations - Avoid duplicating networks for different versions. Use Takes to override specific parameters while sharing the rest of the setup.
- Cache aggressively - Insert File Cache nodes at key stages. Name them descriptively:
$HIP/cache/terrain_base.v001.bgeo.sc. Version your caches. - Pre-compute expensive operations - If VDB meshing, boolean operations, or simulations don't need to re-cook, cache them and load from disk.
- Test at low resolution first - Use a switch node driven by a
lowrestoggle parameter to swap between fast preview geometry and final quality. - Document your graph - Future you (in three weeks) won't remember why that Transform SOP has those specific values. Add sticky notes.
// Quality switch pattern - use in a Switch SOP expression
// Switch input 0 = low-res, input 1 = high-res
// Expression on Switch: ch("../CTRL_settings/high_quality")
// Low-res wrangle: reduce density for fast iteration
f@density *= 0.1;
i@subdivisions = 1;
// High-res wrangle: full quality settings
f@density *= 1.0;
i@subdivisions = chi("../CTRL_settings/subdiv_level");
Create a CTRL_settings null node at the top of your network with all global parameters (quality toggle, random seed, output path, etc.). Reference these from anywhere with relative channel references. This gives artists a single place to control the entire setup.
Now that you understand Houdini's workflow patterns, dive into VEX Programming to master the code that drives these systems, or explore Python in Houdini for tool building and pipeline automation.