v2.2.1
🔍
✓ Verified — v2.2.1

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.

Key distinction
The Houdini Bridge is a live connection. Every bridge method call is a synchronous RPC round-trip to a running Houdini process. This is different from the Maya and Blender integrations, which accumulate an action list and then spawn a batch DCC session only when the executor node fires.

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"
}
VariableRequiredDescription
VIBRANTE_NODE_APPYesAbsolute path to the Vibrante-Node app root (where src/main.py lives)
VIBRANTE_PYTHON_EXEOptionalPath 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:

  1. User opens Houdini — pythonrc.py runs automatically and validates VIBRANTE_NODE_APP and VIBRANTE_PYTHON_EXE in the Python console.
  2. User clicks Vibrante-Node → Launch Vibrante-Node in the Houdini menu bar (or the shelf tool).
  3. launch() calls setup_env() exactly once — this sets all required environment variables for the subprocess.
  4. The Vibrante-Node subprocess starts; NodeRegistry.load_all_with_extras() picks up v_nodes_houdini/ nodes alongside the bundled node library.
  5. HouBridge.connect() establishes the TCP socket to port 18811 (or VIBRANTE_HOU_PORT if overridden).
  6. Graph execution can now use any bridge.* method — each call is a live RPC round-trip into Houdini.
Single setup_env call
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

VariableSet byConsumed by
VIBRANTE_NODE_APPvibrante_node.jsonvibrante_node_houdini.get_app_root()
VIBRANTE_PYTHON_EXEvibrante_node.jsonvibrante_node_houdini._find_system_python()
VIBRANTE_HOUDINI_MODEsetup_env()"subprocess"src/utils/qt_compat.py — selects PyQt5 mode
VIBRANTE_HOU_PORTsetup_env() after server startssrc/utils/hou_bridge.py — default 18811
VIBRANTE_HIP_FILEsetup_env() with current .hip pathAvailable in node python_code via os.environ
v_nodes_dirsetup_env() + EnvManager configNodeRegistry.load_all_with_extras() in window.py
v_scripts_pathsetup_env() + EnvManager configMainWindow._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
Never import hou directly
Never write 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

MethodReturnsDescription
bridge.ping(){"status": "ok", "version": "…"}Check server reachability and Houdini version

Node Creation & Deletion

MethodReturnsNotes
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

MethodReturnsNotes
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 parmsDump 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

MethodReturnsNotes
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
Use run_code sparingly
Prefer the typed bridge methods (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

MethodReturns
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

MethodReturnsNotes
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

MethodReturnsNotes
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 IDDescription
hou_create_geoCreate an Object-level geo container at /obj
hou_create_nodeCreate any node type at any network path
hou_delete_nodeDelete a node by path
hou_set_parmSet a single parameter value
hou_get_parmRead a single parameter value
hou_set_parmsSet multiple parameters in one call
hou_get_parmsDump all parameters of a node
hou_connect_nodesWire one node's output to another's input
hou_cook_nodeForce-cook a node
hou_run_codeExecute arbitrary Python inside Houdini
hou_scene_infoRead current .hip file, fps, frame, frame range
hou_node_infoQuery node type, category, children, connections
hou_node_existsCheck whether a node path exists
hou_set_display_flagToggle the blue display flag on a SOP
hou_set_render_flagToggle the purple render flag on a SOP
hou_layout_childrenAuto-layout nodes inside a network
hou_save_hipSave the current .hip file
hou_set_expressionSet an HScript or Python expression on a parameter
hou_set_keyframeSet a keyframe at a specific frame and value

Production Use Cases


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:

You can also trigger these diagnostics on demand via Vibrante-Node → About Vibrante-Node Integration in the Houdini menu bar.

Common issues

SymptomCauseFix
Houdini nodes missing from LibraryVIBRANTE_NODE_APP not set, or v_nodes_houdini/ not foundCheck package JSON path; verify startup diagnostics show OK
Port already in use on launchAnother Houdini session is using port 18811Add { "VIBRANTE_HOU_PORT": "18812" } to package JSON env block
30-second timeout / ConnectionErrorHoudini is blocked cooking a heavy sceneThe bridge reconnects automatically on the next call. Reduce cook complexity or increase timeout in hou_bridge.py.
AttributeError: hou.playbar in hbatchHeadless Houdini has no playbarHandled automatically by the server — returns fallback range [1, 240].
hou.OperationFailed on set_display_flagNode 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 onceEnsure launch_with_context() calls launch() — never calls setup_env() itself. See §10.4.

Mistakes to avoid

WrongCorrect
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__ manuallyThey 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.