Houdini Workflows

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.
Tip

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:

  1. Use a Connectivity SOP to generate a class attribute
  2. Add a For-Each Named Primitive block (or use @class)
  3. Inside the loop, each iteration sees only one piece
  4. Transform, modify, or process each piece independently
VEX
// 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);
VEX
// 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.

VEX
// 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);
Warning

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 OUT for 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.
VEX
// 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);
HDA Best Practice

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
# 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)
PYTHON
# 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)
Tip

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_trees not scatter2. 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 section
    • CTRL_ - 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.
Network Boxes

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
# 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
# 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))
VEX
// 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 lowres toggle 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.
VEX
// 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");
Tip

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.

Next Steps

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.