Houdini Bridge Integration
The Houdini Bridge is a live, bidirectional integration between Vibrante-Node and a running Houdini session. It operates over a local TCP JSON-RPC connection managed by vibrante_hou_server.py (running inside Houdini) and src/utils/hou_bridge.py (the client singleton in Vibrante-Node). Unlike the headless Maya/Blender patterns, the Houdini bridge is always-live — nodes interact with the live Houdini session in real time as the graph executes.
Architecture
The integration spans two OS processes connected by a local TCP socket. The server runs inside Houdini and has full access to the hou Python API. The client is a singleton in the Vibrante-Node subprocess.
Houdini Process Vibrante-Node Subprocess
───────────────── ──────────────────────────
vibrante_hou_server.py src/utils/hou_bridge.py
JSON-RPC server HouBridge singleton
port 18811 (default) <─TCP─> _send() / _recv()
hou.* API available threading.Lock per instance
runs on Houdini thread TCP_NODELAY, 30 s timeout
auto-reconnect on pipe error
Plugin file layout
plugins/houdini/
├── vibrante_node.json ← Houdini package file (user installs this)
├── v_nodes_houdini/ ← Houdini node .json definitions
│ ├── hou_create_geo.json
│ ├── hou_set_parm.json
│ └── …
├── v_scripts_houdini/ ← Houdini-specific .py scripts (Scripts menu)
│ └── …
└── houdini/ ← Added to HOUDINI_PATH by package JSON
├── MainMenuCommon.xml ← Adds "Vibrante-Node" menu to Houdini bar
├── toolbar/vibrante_node.shelf ← Shelf tool
└── scripts/python/
├── pythonrc.py ← Startup validation
├── vibrante_node_houdini.py ← launch(), setup_env(), show_about()
└── vibrante_hou_server.py ← JSON-RPC server running inside Houdini
Installation & Configuration
vibrante_node.json — the Houdini package file
Copy plugins/houdini/vibrante_node.json to your Houdini packages directory (e.g. ~/houdini20.5/packages/). Open it and set the two required variables:
{
"env": [
{ "VIBRANTE_NODE_APP": "/path/to/vibrante-node" },
{ "VIBRANTE_PYTHON_EXE": "C:/Python311/python.exe" }
],
"path": "$VIBRANTE_NODE_APP/plugins/houdini/houdini"
}
| Variable | Required | Description |
|---|---|---|
VIBRANTE_NODE_APP | Yes | Absolute path to the Vibrante-Node app root (where src/main.py lives) |
VIBRANTE_PYTHON_EXE | Optional | Path to Python 3.11 executable with PyQt5. Auto-detected if absent, but explicit is faster. |
The path entry adds plugins/houdini/houdini/ to HOUDINI_PATH so Houdini finds the menu XML, shelf, and pythonrc.py automatically on next launch.
Launch Sequence
Once the package is installed, the full launch flow is:
- User opens Houdini —
pythonrc.pyruns automatically and validatesVIBRANTE_NODE_APPandVIBRANTE_PYTHON_EXEin the Python console. - User clicks Vibrante-Node → Launch Vibrante-Node in the Houdini menu bar (or the shelf tool).
launch()callssetup_env()exactly once — this sets all required environment variables for the subprocess.- The Vibrante-Node subprocess starts;
NodeRegistry.load_all_with_extras()picks upv_nodes_houdini/nodes alongside the bundled node library. HouBridge.connect()establishes the TCP socket to port 18811 (orVIBRANTE_HOU_PORTif overridden).- Graph execution can now use any
bridge.*method — each call is a live RPC round-trip into Houdini.
setup_env() is called exactly once inside launch(). It must never be called a second time in the same session — doing so would append duplicate path entries to v_nodes_dir and v_scripts_path. See CLAUDE.md §10.4 for the full bug history.
Environment Variables
| Variable | Set by | Consumed by |
|---|---|---|
VIBRANTE_NODE_APP | vibrante_node.json | vibrante_node_houdini.get_app_root() |
VIBRANTE_PYTHON_EXE | vibrante_node.json | vibrante_node_houdini._find_system_python() |
VIBRANTE_HOUDINI_MODE | setup_env() → "subprocess" | src/utils/qt_compat.py — selects PyQt5 mode |
VIBRANTE_HOU_PORT | setup_env() after server starts | src/utils/hou_bridge.py — default 18811 |
VIBRANTE_HIP_FILE | setup_env() with current .hip path | Available in node python_code via os.environ |
v_nodes_dir | setup_env() + EnvManager config | NodeRegistry.load_all_with_extras() in window.py |
v_scripts_path | setup_env() + EnvManager config | MainWindow._populate_scripts_menu() in window.py |
v_nodes_dir and v_scripts_path are general-purpose multi-directory variables (os.pathsep-separated). Studio TDs can configure additional paths in Settings → Application Paths without overwriting what Houdini's setup_env() already set.
Bridge API Reference
Import and obtain the bridge singleton at the top of any node's python_code:
from src.utils.hou_bridge import get_bridge
bridge = get_bridge() # returns the HouBridge singleton — never import hou directly
import hou in node code. The hou module is only available inside the Houdini process. All Houdini operations must go through the bridge client.
Connectivity
| Method | Returns | Description |
|---|---|---|
bridge.ping() | {"status": "ok", "version": "…"} | Check server reachability and Houdini version |
Node Creation & Deletion
| Method | Returns | Notes |
|---|---|---|
create_node(parent, node_type, name="") | {"path": "…", "name": "…", "type": "…"} | Creates a node inside parent. Returns result["path"] for chaining. |
delete_node(path) | {"deleted": "…"} | Deletes the node at path. |
result = bridge.create_node("/obj", "geo", "my_geo")
geo_path = result["path"] # e.g. "/obj/my_geo"
Parameters
| Method | Returns | Notes |
|---|---|---|
set_parm(node, parm, value) | {"set": True} | Set a single parameter by name |
get_parm(node, parm) | {"value": …} | Read a single parameter value |
set_parms(node, parms_dict) | {"set": True, "count": N} | Set multiple parameters in one RPC call |
get_parms(node) | flat dict of all parms | Dump all parameter name→value pairs |
bridge.set_parm("/obj/my_geo/alembic1", "fileName", "/path/to/file.abc")
value = bridge.get_parm("/obj/my_geo/alembic1", "fileName")["value"]
bridge.set_parms("/obj/my_geo/null1", {"tx": 1.0, "ty": 2.0, "tz": 0.5})
Connections & Cooking
| Method | Returns | Notes |
|---|---|---|
connect_nodes(from_node, to_node, output=0, input_idx=0) | {"connected": True} | Wire from_node's output into to_node's input |
cook_node(path, force=False) | {"cooked": True} | Force-cook a node. force=True bypasses Houdini's cook cache. |
Arbitrary Python Execution
bridge.run_code(code) executes a string of Python inside Houdini. The hou module is available. Assign to the local variable result to receive a value back.
run_result = bridge.run_code(
"n = hou.node('/obj/my_geo'); result = n.displayNode().path() if n and n.displayNode() else None"
)
display_path = run_result.get("result") # e.g. "/obj/my_geo/convert1" or None
set_parm, create_node, etc.) over run_code wherever possible. run_code is powerful but its results are untyped and harder to debug. Use it when a Houdini operation has no corresponding bridge method.
Scene & Node Inspection
| Method | Returns |
|---|---|
scene_info() | {"hip_file": …, "fps": …, "frame": …, "frame_range": [start, end]} |
node_info(path) | {"path": …, "name": …, "type": …, "category": …, "inputs": […], "outputs": […], "children": […]} |
children(path="/obj") | List of {"name": …, "type": …, "path": …} dicts |
node_exists(path) | {"exists": True} or {"exists": False} |
The node_info category field returns "Object", "Sop", "Shop", etc. — use this to distinguish geo containers from SOP nodes when resolving ambiguous path inputs.
Flags, Layout & Save
| Method | Returns | Notes |
|---|---|---|
set_display_flag(path, on=True) | {"set": True} | Toggle the blue display flag |
set_render_flag(path, on=True) | {"set": True} | Toggle the purple render flag |
layout_children(path="/obj") | {"done": True} | Auto-layout child nodes |
save_hip(path="") | {"saved": "…"} | Save the .hip file; empty path saves in-place |
Animation & Expressions
| Method | Returns | Notes |
|---|---|---|
set_expression(node, parm, expression, language="hscript") | {"set": True} | language = "hscript" or "python" |
set_keyframe(node, parm, frame, value) | {"set": True} | Set a single keyframe on a parameter |
set_frame(frame) | {"frame": …} | Move Houdini's current frame |
set_playback_range(start, end) | {"start": …, "end": …} | Set the timeline range |
bridge.set_expression("/obj/my_geo/null1", "tx", "sin($F * 0.1)", language="hscript")
bridge.set_expression("/obj/my_geo/null1", "ty", "hou.frame() * 0.05", language="python")
bridge.set_keyframe("/obj/my_cam", "tx", frame=1, value=0.0)
bridge.set_keyframe("/obj/my_cam", "tx", frame=240, value=100.0)
Common Node Patterns
Creating a geometry container with SOPs
from src.nodes.base import BaseNode
from src.utils.hou_bridge import get_bridge
class Hou_Create_Geo(BaseNode):
name = "hou_create_geo"
def __init__(self):
super().__init__()
# [AUTO-GENERATED-PORTS-START]
self.add_input("geo_name", "string", widget_type="text", default="my_geo")
self.add_output("geo_path", "string")
# [AUTO-GENERATED-PORTS-END]
async def execute(self, inputs):
geo_name = inputs.get("geo_name", "my_geo")
if not geo_name:
self.log_error("geo_name is required.")
return {"geo_path": "", "exec_out": True}
try:
bridge = get_bridge()
# Create /obj-level geo container
result = bridge.create_node("/obj", "geo", geo_name)
geo_path = result["path"]
# Clear the default file SOP Houdini adds automatically
for child in bridge.children(geo_path):
bridge.delete_node(child["path"])
bridge.layout_children("/obj")
return {"geo_path": geo_path, "exec_out": True}
except Exception as e:
self.log_error(f"Houdini create_geo failed: {e}")
return {"geo_path": "", "exec_out": True}
def register_node():
return Hou_Create_Geo
Resolving Object vs SOP input paths
Many nodes accept a geo_path that may be either an Object-level geo container or a SOP node. Use node_info to distinguish them:
node_info = bridge.node_info(geo_path)
category = node_info.get("category", "")
if category == "Object":
# Descend into the geo container to find the display SOP
run_result = bridge.run_code(
f"n = hou.node('{geo_path}'); result = n.displayNode().path() if n and n.displayNode() else None"
)
input_sop = run_result.get("result")
if not input_sop:
raise Exception(f"No display SOP found inside: {geo_path}")
sop_context = geo_path
elif category == "Sop":
# Already a SOP — parent is the geo container
sop_context = "/".join(geo_path.rstrip("/").split("/")[:-1])
input_sop = geo_path
else:
raise Exception(f"Unsupported node category '{category}': {geo_path}")
VEX attribwrangle
vex_code = (
'vector p0 = point(0, "P", primpoint(0, @primnum, 0));\n'
'vector p1 = point(0, "P", primpoint(0, @primnum, 1));\n'
'vector dir = normalize(p1 - p0);\n'
'if (abs(dot(dir, set(1,0,0))) < 0.9) { removeprim(0, @primnum, 1); }'
)
wrangle_result = bridge.create_node(sop_context, "attribwrangle", "edge_filter")
wrangle_path = wrangle_result["path"]
bridge.connect_nodes(input_sop, wrangle_path, output=0, input_idx=0)
bridge.set_parm(wrangle_path, "class", 1) # 0=detail, 1=primitive, 2=point, 3=vertex
bridge.set_parm(wrangle_path, "snippet", vex_code)
bridge.set_display_flag(wrangle_path, True)
bridge.set_render_flag(wrangle_path, True)
bridge.layout_children(sop_context)
Standard execute() pattern
All Houdini nodes follow this guard pattern — validate input, get bridge, wrap in try/except, always return exec_out:
async def execute(self, inputs):
geo_path = inputs.get("geo_path", "")
if not geo_path:
self.log_error("No geo path provided.")
return {"result_path": "", "exec_out": True}
try:
bridge = get_bridge()
# … create / modify nodes …
return {"result_path": result_path, "exec_out": True}
except Exception as e:
self.log_error(f"Houdini operation failed: {str(e)}")
return {"result_path": "", "exec_out": True}
Node Library
Houdini nodes live in plugins/houdini/v_nodes_houdini/ and appear under the Houdini category in the Library panel. They are loaded via v_nodes_dir and are only present when launching from Houdini.
| Node ID | Description |
|---|---|
hou_create_geo | Create an Object-level geo container at /obj |
hou_create_node | Create any node type at any network path |
hou_delete_node | Delete a node by path |
hou_set_parm | Set a single parameter value |
hou_get_parm | Read a single parameter value |
hou_set_parms | Set multiple parameters in one call |
hou_get_parms | Dump all parameters of a node |
hou_connect_nodes | Wire one node's output to another's input |
hou_cook_node | Force-cook a node |
hou_run_code | Execute arbitrary Python inside Houdini |
hou_scene_info | Read current .hip file, fps, frame, frame range |
hou_node_info | Query node type, category, children, connections |
hou_node_exists | Check whether a node path exists |
hou_set_display_flag | Toggle the blue display flag on a SOP |
hou_set_render_flag | Toggle the purple render flag on a SOP |
hou_layout_children | Auto-layout nodes inside a network |
hou_save_hip | Save the current .hip file |
hou_set_expression | Set an HScript or Python expression on a parameter |
hou_set_keyframe | Set a keyframe at a specific frame and value |
Production Use Cases
- Procedural geometry from asset databases — query a SQL/JSON asset registry, then drive
create_node,set_parm, andcook_nodeto build the geometry network automatically. - Camera rig setup from shot metadata — read shot data (focal length, film back, clipping planes) from a spreadsheet or Prism and push to a camera node's parameters in one execution run.
- Parameter sweeps — loop over a list of values,
set_parm,cook_node, and export each variant to disk — useful for wedge renders and look-dev iterations. - Live scene validation — after an artist sets up a scene, run a validation workflow that queries node state, checks parameter values, verifies expected topology, and logs issues — all without leaving Houdini.
- Keyframe injection from external data — parse mocap BVH or simulation CSV files and call
set_keyframein a loop to bake the curves onto Houdini nodes. - HDA parameter exposure and locking — use
run_codeto callhou.HDADefinitionAPIs for bulk HDA management workflows. - Automated USD export — chain
hou_cook_node→hou_run_code(callropNode.render()) → a file copy node for fully automated USD/Alembic output.
Troubleshooting
Startup diagnostics
After Houdini loads the package, check the Python Shell for startup messages from pythonrc.py. Each variable is printed with an OK or ERROR/MISSING status:
VIBRANTE_NODE_APP— OK / ERROR (not set or path doesn't exist)VIBRANTE_PYTHON_EXE— OK / WARNING (missing or path not found)v_nodes_houdini/— OK / MISSINGv_scripts_houdini/— OK / MISSING
You can also trigger these diagnostics on demand via Vibrante-Node → About Vibrante-Node Integration in the Houdini menu bar.
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Houdini nodes missing from Library | VIBRANTE_NODE_APP not set, or v_nodes_houdini/ not found | Check package JSON path; verify startup diagnostics show OK |
| Port already in use on launch | Another Houdini session is using port 18811 | Add { "VIBRANTE_HOU_PORT": "18812" } to package JSON env block |
30-second timeout / ConnectionError | Houdini is blocked cooking a heavy scene | The bridge reconnects automatically on the next call. Reduce cook complexity or increase timeout in hou_bridge.py. |
AttributeError: hou.playbar in hbatch | Headless Houdini has no playbar | Handled automatically by the server — returns fallback range [1, 240]. |
hou.OperationFailed on set_display_flag | Node type doesn't support display flags (e.g. Object-level null) | The server catches this and raises a clear ValueError — check your node category before calling the flag methods. |
| Nodes or scripts load twice (duplicates in Library) | setup_env() called more than once | Ensure launch_with_context() calls launch() — never calls setup_env() itself. See §10.4. |
Mistakes to avoid
| Wrong | Correct |
|---|---|
import hou; hou.node("/obj").createNode("geo") | bridge.create_node("/obj", "geo") |
hou_bridge.get_hou() | from src.utils.hou_bridge import get_bridge; bridge = get_bridge() |
result = bridge.create_node(…); result.path() | result = bridge.create_node(…); path = result["path"] |
for c in bridge.children(p): c.destroy() | for c in bridge.children(p): bridge.delete_node(c["path"]) |
Adding exec_in/exec_out in __init__ manually | They are added by super().__init__() automatically |
Technical Notes
Thread safety
Each HouBridge instance has its own threading.Lock. The _send() / _recv() cycle acquires the lock, so concurrent node calls from the AsyncRuntime thread pool serialize safely through the socket without corrupting the response stream.
TCP_NODELAY
socket.TCP_NODELAY is set on every new connection. Without it, Nagle's algorithm on Windows introduces ~40 ms of latency per RPC call. With it, small JSON-RPC payloads are sent immediately.
Auto-reconnect
On BrokenPipeError or ConnectionResetError (e.g. Houdini was restarted), the client reconnects once automatically and retries the send. If the reconnect also fails, a clear ConnectionError is raised and the socket is closed so the next call starts fresh.