Initial commit: Hermes-Houdini Bridge
- Shelf tool to open/close command port in Houdini - hhb client script (eval, script, get-selected, set-param, etc.) - Hermes skill with standard hou module workflows - Example: create pyro setup - hou module quick reference
This commit is contained in:
@@ -0,0 +1,82 @@
|
|||||||
|
# Hermes-Houdini Bridge
|
||||||
|
|
||||||
|
> Hermes Agent ↔ Houdini Python Bridge — remote node control, parameter editing, VEX injection, and scene querying via Houdini's built-in `hcommand` protocol.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Hermes Agent (orange-desktop) Houdini 21.0 (fedora.fritz.box)
|
||||||
|
┌─────────────────────┐ ┌──────────────────────────────┐
|
||||||
|
│ hhb eval "code" │──SSH──────────→│ hcommand PORT "python code" │
|
||||||
|
│ hhb script file.py │ │ ↓ │
|
||||||
|
│ hhb get-selected │ │ hou module (running session) │
|
||||||
|
└─────────────────────┘ └──────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
No custom server needed — uses Houdini's built-in `openport` + `hcommand` protocol. The bridge wraps SSH and command quoting so Hermes can send Python code to your live Houdini session.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Install the Shelf Tool (once)
|
||||||
|
|
||||||
|
Copy `houdini/shelf/hermes_bridge.shelf` to your Houdini toolbar, or run the shelf script directly in Houdini's Python Shell.
|
||||||
|
|
||||||
|
### 2. Open the Bridge (every session)
|
||||||
|
|
||||||
|
Click the **Hermes Bridge** shelf button in Houdini. It opens port 12345 and shows a status dialog.
|
||||||
|
|
||||||
|
### 3. Use from Hermes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get selected node info
|
||||||
|
./hermes/bin/hhb get-selected
|
||||||
|
|
||||||
|
# Set a parameter
|
||||||
|
./hermes/bin/hhb eval "hou.selectedNodes()[0].parm('file').set('/path/to/file.abc')"
|
||||||
|
|
||||||
|
# Query connections
|
||||||
|
./hermes/bin/hhb eval "print(hou.selectedNodes()[0].inputConnections())"
|
||||||
|
|
||||||
|
# Execute a complex Python script
|
||||||
|
./hermes/bin/hhb script ./examples/create_pyro_setup.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Houdini 20+ (tested with 21.0)
|
||||||
|
- SSH access from Hermes host to Houdini workstation
|
||||||
|
- Network connectivity (port 12345, default)
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
hermes-houdini-bridge/
|
||||||
|
├── houdini/
|
||||||
|
│ └── shelf/
|
||||||
|
│ └── hermes_bridge.shelf # Shelf tool: open/close port
|
||||||
|
├── hermes/
|
||||||
|
│ ├── bin/
|
||||||
|
│ │ └── hhb # Client: send commands to Houdini
|
||||||
|
│ └── skill/
|
||||||
|
│ └── SKILL.md # Hermes skill for standard workflows
|
||||||
|
├── examples/
|
||||||
|
│ └── create_pyro_setup.py # Example: build a pyro sim
|
||||||
|
├── docs/
|
||||||
|
│ └── REFERENCE.md # hou module cheat sheet
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- Port 12345 opens on `0.0.0.0` — only use on trusted networks
|
||||||
|
- To restrict to localhost: modify the shelf tool's `openport` call to `hou.hscript('openport 12345 +.+.+.+')`
|
||||||
|
- The bridge uses Hermes' existing SSH key — no additional credentials needed
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Fix |
|
||||||
|
|---------|-----|
|
||||||
|
| `hcommand: connect failed` | Port not open. Click shelf button in Houdini. |
|
||||||
|
| `hcommand: connection refused` | Houdini not running or port blocked by firewall. |
|
||||||
|
| Python error in output | Code has syntax issues. Test in Houdini Python Shell first. |
|
||||||
|
| `ModuleNotFoundError: No module named 'hou'` | Running hython directly, not through hcommand. Use `hhb`. |
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# hou Module Quick Reference
|
||||||
|
|
||||||
|
Common patterns for the Hermes Bridge.
|
||||||
|
|
||||||
|
## Selection & Navigation
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Selected nodes
|
||||||
|
hou.selectedNodes() # → list[hou.Node]
|
||||||
|
hou.selectedNodes()[0].path() # → '/obj/geo1/box1'
|
||||||
|
hou.selectedNodes()[0].type().name() # → 'box'
|
||||||
|
|
||||||
|
# Find nodes
|
||||||
|
hou.node('/obj/geo1') # Get by path
|
||||||
|
hou.node('/obj').children() # All children
|
||||||
|
hou.node('/obj').recursiveGlob('*box*')# Glob search
|
||||||
|
|
||||||
|
# Parent/child
|
||||||
|
node.parent() # Parent node
|
||||||
|
node.node('childname') # Named child
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
```python
|
||||||
|
n = hou.selectedNodes()[0]
|
||||||
|
|
||||||
|
# Get/Set
|
||||||
|
n.parm('tx').eval() # Evaluate value
|
||||||
|
n.parm('tx').set(5.0) # Set value
|
||||||
|
n.parm('file').set('/path/to/file') # Set string param
|
||||||
|
|
||||||
|
# All non-default parms
|
||||||
|
[(p.name(), p.eval()) for p in n.parms() if not p.isAtDefault()]
|
||||||
|
|
||||||
|
# Existence check
|
||||||
|
if n.parm('snippet') is not None:
|
||||||
|
n.parm('snippet').set('@P.y += 1;')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Connections
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Inputs
|
||||||
|
n.inputConnections() # → tuple of (input_node, output_idx, input_idx)
|
||||||
|
n.inputConnectors() # Input connector names
|
||||||
|
n.setInput(0, other_node) # Connect
|
||||||
|
|
||||||
|
# Outputs
|
||||||
|
n.outputConnections() # What connects FROM this node
|
||||||
|
|
||||||
|
# Disconnect all inputs
|
||||||
|
for i in range(len(n.inputConnectors())):
|
||||||
|
n.setInput(i, None)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Node Creation
|
||||||
|
|
||||||
|
```python
|
||||||
|
parent = hou.node('/obj/geo1')
|
||||||
|
|
||||||
|
# Single nodes
|
||||||
|
box = parent.createNode('box')
|
||||||
|
null = parent.createNode('null', 'OUT')
|
||||||
|
|
||||||
|
# Common types
|
||||||
|
'geo' # Geometry container
|
||||||
|
'null' # Null
|
||||||
|
'attribwrangle' # Attribute Wrangle
|
||||||
|
'attribvop' # Attribute VOP
|
||||||
|
'file' # File read (SOP)
|
||||||
|
'rop_geometry' # Geometry ROP
|
||||||
|
'switch' # Switch
|
||||||
|
'merge' # Merge
|
||||||
|
'transform' # Transform (xform)
|
||||||
|
'sphere' # Sphere
|
||||||
|
'grid' # Grid
|
||||||
|
'scatter' # Scatter
|
||||||
|
'boolean' # Boolean
|
||||||
|
'remesh' # Remesh
|
||||||
|
'polyextrude' # Poly Extrude
|
||||||
|
'principledshader::2.0' # Material
|
||||||
|
```
|
||||||
|
|
||||||
|
## VEX
|
||||||
|
|
||||||
|
```python
|
||||||
|
wrangle = hou.selectedNodes()[0]
|
||||||
|
wrangle.parm('snippet').set('''
|
||||||
|
// Your VEX code here
|
||||||
|
@P.y += sin(@Frame * 0.1) * 0.5;
|
||||||
|
@Cd = rand(@ptnum);
|
||||||
|
''')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```python
|
||||||
|
node.moveToGoodPosition() # Auto-layout one node
|
||||||
|
for n in parent.children():
|
||||||
|
n.moveToGoodPosition() # Layout all children
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scene Info
|
||||||
|
|
||||||
|
```python
|
||||||
|
hou.hipFile.name() # Current .hip path
|
||||||
|
hou.hipFile.basename() # Just filename
|
||||||
|
hou.expandString('$HIP') # Scene directory
|
||||||
|
hou.hscript('ls /obj') # Raw hscript
|
||||||
|
```
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""
|
||||||
|
Example: Create a simple Pyro simulation setup in Houdini.
|
||||||
|
Run via: hhb script examples/create_pyro_setup.py
|
||||||
|
"""
|
||||||
|
import hou
|
||||||
|
|
||||||
|
# Create geometry container
|
||||||
|
obj = hou.node('/obj')
|
||||||
|
geo = obj.createNode('geo', 'pyro_sim')
|
||||||
|
|
||||||
|
# Source geometry (sphere)
|
||||||
|
sphere = geo.createNode('sphere', 'pyro_source')
|
||||||
|
sphere.parm('radx').set(0.5)
|
||||||
|
sphere.parm('rady').set(0.5)
|
||||||
|
sphere.parm('radz').set(0.5)
|
||||||
|
|
||||||
|
# Transform to position it
|
||||||
|
xform = geo.createNode('xform', 'position')
|
||||||
|
xform.setInput(0, sphere)
|
||||||
|
xform.parm('ty').set(2)
|
||||||
|
|
||||||
|
# Null for output
|
||||||
|
null_geo = geo.createNode('null', 'OUT_GEO')
|
||||||
|
null_geo.setInput(0, xform)
|
||||||
|
|
||||||
|
# Create DOP network for the sim
|
||||||
|
dopnet = geo.createNode('dopnet', 'pyro_sim')
|
||||||
|
dopnet.setInput(0, null_geo)
|
||||||
|
|
||||||
|
# Inside the DOP: we'd need to set up pyro solver, smoke object, etc.
|
||||||
|
# For now, just create the structure
|
||||||
|
dopnet.moveToGoodPosition()
|
||||||
|
|
||||||
|
# Layout
|
||||||
|
sphere.moveToGoodPosition()
|
||||||
|
xform.moveToGoodPosition()
|
||||||
|
null_geo.moveToGoodPosition()
|
||||||
|
|
||||||
|
print("Pyro setup skeleton created at:", geo.path())
|
||||||
|
print("Nodes:", [n.name() for n in geo.children()])
|
||||||
Executable
+188
@@ -0,0 +1,188 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
# hhb — Hermes-Houdini Bridge Client
|
||||||
|
# Sends Python/hou commands to a running Houdini session
|
||||||
|
# via SSH + hcommand.
|
||||||
|
# ──────────────────────────────────────────────────────────
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HOST="${HHB_HOST:-niklas@192.168.178.39}"
|
||||||
|
PORT="${HHB_PORT:-12345}"
|
||||||
|
HCOMMAND="${HHB_HCOMMAND:-/opt/hfs21.0/bin/hcommand}"
|
||||||
|
WORKSPACE="${HHB_WORKSPACE:-/opt/data/projects/hermes-houdini-bridge}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: hhb <command> [args]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
eval <python_code> Execute Python code in the running Houdini session.
|
||||||
|
Example: hhb eval "print(hou.selectedNodes())"
|
||||||
|
|
||||||
|
script <file.py> Send a Python script file to Houdini and execute it.
|
||||||
|
Example: hhb script ./examples/create_pyro_setup.py
|
||||||
|
|
||||||
|
get-selected Print paths and types of selected nodes.
|
||||||
|
get-param <parm> Get value of a parameter on the first selected node.
|
||||||
|
set-param <parm> <val> Set a parameter on the first selected node.
|
||||||
|
get-inputs Print input connections of the first selected node.
|
||||||
|
status Check if the bridge port is open.
|
||||||
|
help Show this help.
|
||||||
|
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Helpers ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
run_remote_python() {
|
||||||
|
# Write Python code to a temp file, execute via hcommand.
|
||||||
|
# This avoids quoting hell with complex Python snippets.
|
||||||
|
local python_code="$1"
|
||||||
|
local tmpfile="/tmp/hhb_$$.py"
|
||||||
|
|
||||||
|
# Upload script
|
||||||
|
printf '%s' "$python_code" | ssh "$HOST" "cat > $tmpfile"
|
||||||
|
|
||||||
|
# Execute via hscript's python command — reads file and execs
|
||||||
|
local output
|
||||||
|
output=$(ssh "$HOST" \
|
||||||
|
"$HCOMMAND $PORT \"python exec(open('$tmpfile').read())\"" 2>&1) || {
|
||||||
|
local rc=$?
|
||||||
|
ssh "$HOST" "rm -f $tmpfile" 2>/dev/null
|
||||||
|
echo "hhb: hcommand failed (exit $rc)" >&2
|
||||||
|
echo "$output" >&2
|
||||||
|
return $rc
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
ssh "$HOST" "rm -f $tmpfile" 2>/dev/null
|
||||||
|
|
||||||
|
# hcommand output has a prefix line like "-> foobar", strip it
|
||||||
|
echo "$output" | sed 's/^->\s*//'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Commands ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
cmd_status() {
|
||||||
|
local output
|
||||||
|
output=$(ssh "$HOST" "$HCOMMAND $PORT openport" 2>&1) || {
|
||||||
|
echo "Bridge INACTIVE — Houdini not reachable or port $PORT closed."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
echo "$output" | sed 's/^->\s*//'
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_eval() {
|
||||||
|
[[ $# -ge 1 ]] || { echo "Usage: hhb eval <python_code>"; exit 1; }
|
||||||
|
run_remote_python "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_script() {
|
||||||
|
[[ $# -ge 1 ]] || { echo "Usage: hhb script <file.py>"; exit 1; }
|
||||||
|
local script_path="$1"
|
||||||
|
[[ -f "$script_path" ]] || { echo "File not found: $script_path"; exit 1; }
|
||||||
|
|
||||||
|
scp -q "$script_path" "$HOST:/tmp/hhb_script_$$.py"
|
||||||
|
local output
|
||||||
|
output=$(ssh "$HOST" \
|
||||||
|
"$HCOMMAND $PORT \"python exec(open('/tmp/hhb_script_$$.py').read())\"" 2>&1) || {
|
||||||
|
local rc=$?
|
||||||
|
ssh "$HOST" "rm -f /tmp/hhb_script_$$.py" 2>/dev/null
|
||||||
|
echo "hhb: hcommand failed (exit $rc)" >&2
|
||||||
|
echo "$output" >&2
|
||||||
|
return $rc
|
||||||
|
}
|
||||||
|
ssh "$HOST" "rm -f /tmp/hhb_script_$$.py" 2>/dev/null
|
||||||
|
echo "$output" | sed 's/^->\s*//'
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_get_selected() {
|
||||||
|
run_remote_python '
|
||||||
|
import hou
|
||||||
|
nodes = hou.selectedNodes()
|
||||||
|
if not nodes:
|
||||||
|
print("No nodes selected.")
|
||||||
|
else:
|
||||||
|
for n in nodes:
|
||||||
|
print(f"{n.path():50s} [{n.type().name()}]")
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_get_param() {
|
||||||
|
[[ $# -ge 1 ]] || { echo "Usage: hhb get-param <parm_name>"; exit 1; }
|
||||||
|
local parm="$1"
|
||||||
|
run_remote_python "
|
||||||
|
import hou
|
||||||
|
nodes = hou.selectedNodes()
|
||||||
|
if not nodes:
|
||||||
|
print('No nodes selected.')
|
||||||
|
else:
|
||||||
|
p = nodes[0].parm('$parm')
|
||||||
|
if p is None:
|
||||||
|
print(f'Parameter \"$parm\" not found on {nodes[0].path()}')
|
||||||
|
else:
|
||||||
|
print(f'{nodes[0].path()}.{p.name()} = {p.eval()}')
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_set_param() {
|
||||||
|
[[ $# -ge 2 ]] || { echo "Usage: hhb set-param <parm_name> <value>"; exit 1; }
|
||||||
|
local parm="$1"
|
||||||
|
local val="$2"
|
||||||
|
run_remote_python "
|
||||||
|
import hou
|
||||||
|
nodes = hou.selectedNodes()
|
||||||
|
if not nodes:
|
||||||
|
print('No nodes selected.')
|
||||||
|
else:
|
||||||
|
p = nodes[0].parm('$parm')
|
||||||
|
if p is None:
|
||||||
|
print(f'Parameter \"$parm\" not found on {nodes[0].path()}')
|
||||||
|
else:
|
||||||
|
p.set('$val')
|
||||||
|
print(f'{nodes[0].path()}.{p.name()} = {p.eval()} ✓')
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_get_inputs() {
|
||||||
|
run_remote_python '
|
||||||
|
import hou
|
||||||
|
nodes = hou.selectedNodes()
|
||||||
|
if not nodes:
|
||||||
|
print("No nodes selected.")
|
||||||
|
else:
|
||||||
|
for n in nodes:
|
||||||
|
inputs = n.inputConnectors()
|
||||||
|
print(f"\n{n.path()} [{n.type().name()}]")
|
||||||
|
for i, conn in enumerate(inputs):
|
||||||
|
in_nodes = n.inputConnections(i) if hasattr(n, "inputConnections") else []
|
||||||
|
if in_nodes:
|
||||||
|
for c in in_nodes:
|
||||||
|
print(f" input {i} ← {c.inputNode().path()} [{c.inputNode().type().name()}]")
|
||||||
|
else:
|
||||||
|
print(f" input {i} ← (none)")
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Main ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[[ $# -ge 1 ]] || usage
|
||||||
|
|
||||||
|
CMD="$1"
|
||||||
|
shift || true
|
||||||
|
|
||||||
|
case "$CMD" in
|
||||||
|
eval) cmd_eval "$@" ;;
|
||||||
|
script) cmd_script "$@" ;;
|
||||||
|
get-selected) cmd_get_selected ;;
|
||||||
|
get-param) cmd_get_param "$@" ;;
|
||||||
|
set-param) cmd_set_param "$@" ;;
|
||||||
|
get-inputs) cmd_get_inputs ;;
|
||||||
|
status) cmd_status ;;
|
||||||
|
help|--help|-h) usage ;;
|
||||||
|
*)
|
||||||
|
echo "Unknown command: $CMD"
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Hermes Skill: Houdini Bridge
|
||||||
|
|
||||||
|
Standard workflows to control a running Houdini session via the hermes-houdini-bridge.
|
||||||
|
|
||||||
|
## Trigger
|
||||||
|
|
||||||
|
When the user asks to:
|
||||||
|
- Modify node parameters in Houdini
|
||||||
|
- Build node setups/networks in Houdini
|
||||||
|
- Write VEX/Wrangle code in Houdini
|
||||||
|
- Query selected nodes, connections, or scene state
|
||||||
|
- Execute Python in a running Houdini session
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. User must have the Hermes Bridge shelf tool active in Houdini (port 12345 open)
|
||||||
|
2. SSH access to the Houdini workstation (default: `niklas@192.168.178.39`)
|
||||||
|
|
||||||
|
If the bridge is not open, tell the user: "Open the Hermes Bridge shelf tool in Houdini first."
|
||||||
|
|
||||||
|
## Client Location
|
||||||
|
|
||||||
|
`/opt/data/projects/hermes-houdini-bridge/hermes/bin/hhb`
|
||||||
|
|
||||||
|
Always use this full path — the script handles SSH and hcommand internally.
|
||||||
|
|
||||||
|
## Common Workflows
|
||||||
|
|
||||||
|
### 1. Query Selected Nodes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/opt/data/projects/hermes-houdini-bridge/hermes/bin/hhb get-selected
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Set a Parameter
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/opt/data/projects/hermes-houdini-bridge/hermes/bin/hhb set-param <parm_name> <value>
|
||||||
|
```
|
||||||
|
Example: `hhb set-param file /path/to/texture.exr`
|
||||||
|
|
||||||
|
### 3. Get a Parameter Value
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/opt/data/projects/hermes-houdini-bridge/hermes/bin/hhb get-param <parm_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Execute Arbitrary Python
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/opt/data/projects/hermes-houdini-bridge/hermes/bin/hhb eval "<python_code>"
|
||||||
|
```
|
||||||
|
|
||||||
|
The code runs in Houdini's Python environment with full `hou` module access.
|
||||||
|
|
||||||
|
### 5. Check Connection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/opt/data/projects/hermes-houdini-bridge/hermes/bin/hhb status
|
||||||
|
```
|
||||||
|
|
||||||
|
## Useful hou Module Patterns
|
||||||
|
|
||||||
|
### VEX in a Wrangler
|
||||||
|
```python
|
||||||
|
node = hou.selectedNodes()[0]
|
||||||
|
node.parm('snippet').set('@P.y += sin(@Frame * 0.1);\n@Cd = rand(@ptnum);')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build a Node Chain
|
||||||
|
```python
|
||||||
|
obj = hou.node('/obj')
|
||||||
|
geo = obj.createNode('geo', 'my_setup')
|
||||||
|
null_in = geo.createNode('null', 'IN')
|
||||||
|
wrangle = geo.createNode('attribwrangle', 'compute')
|
||||||
|
null_out = geo.createNode('null', 'OUT')
|
||||||
|
wrangle.setInput(0, null_in)
|
||||||
|
null_out.setInput(0, wrangle)
|
||||||
|
wrangle.parm('snippet').set('@Cd = rand(@ptnum);')
|
||||||
|
wrangle.moveToGoodPosition()
|
||||||
|
null_out.moveToGoodPosition()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Read Node Info
|
||||||
|
```python
|
||||||
|
n = hou.selectedNodes()[0]
|
||||||
|
info = {
|
||||||
|
'path': n.path(),
|
||||||
|
'type': n.type().name(),
|
||||||
|
'parms': {p.name(): p.eval() for p in n.parms() if not p.isAtDefault()},
|
||||||
|
'inputs': [c.inputNode().path() for c in n.inputConnections()],
|
||||||
|
'outputs': [c.outputNode().path() for c in n.outputConnections()],
|
||||||
|
}
|
||||||
|
print(info)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a Material
|
||||||
|
```python
|
||||||
|
matnet = hou.node('/mat')
|
||||||
|
principled = matnet.createNode('principledshader::2.0', 'my_material')
|
||||||
|
principled.parm('basecolorr').set(1.0)
|
||||||
|
principled.parm('basecolorg').set(0.2)
|
||||||
|
principled.parm('basecolorb').set(0.1)
|
||||||
|
principled.parm('rough').set(0.3)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pitfalls
|
||||||
|
|
||||||
|
- Complex Python with quotes/backticks: Use `hhb script file.py` instead of `hhb eval "..."`
|
||||||
|
- Parameter names are internal (e.g., `file`, not `File`)
|
||||||
|
- Node types: check with `node.type().name()` first
|
||||||
|
- hcommand timeouts: complex scripts may take time, especially if triggering cooks
|
||||||
|
- Selected node counts: always check `len(hou.selectedNodes())` before operating
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""
|
||||||
|
Hermes Bridge — Open a command port for remote control by Hermes Agent.
|
||||||
|
|
||||||
|
Usage (in Houdini Python Shell or Shelf Tool):
|
||||||
|
exec(open('/path/to/hermes_bridge.py').read())
|
||||||
|
|
||||||
|
Or install as shelf tool via the .shelf XML file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hou
|
||||||
|
|
||||||
|
PORT = 12345
|
||||||
|
IP_MASK = "0.0.0.0" # Accept connections from anywhere. Use "+.+.+.+" for localhost only.
|
||||||
|
|
||||||
|
|
||||||
|
def get_bridge_status():
|
||||||
|
"""Check if the bridge port is open."""
|
||||||
|
result = hou.hscript("openport")[0]
|
||||||
|
if f"Port {PORT} is open" in result:
|
||||||
|
return True, result.strip()
|
||||||
|
return False, result.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def open_bridge():
|
||||||
|
"""Open the command port."""
|
||||||
|
# Close first in case it's stuck
|
||||||
|
hou.hscript(f"closeport {PORT}")
|
||||||
|
result, _ = hou.hscript(f"openport {PORT} {IP_MASK}")
|
||||||
|
return result.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def close_bridge():
|
||||||
|
"""Close the command port."""
|
||||||
|
result, _ = hou.hscript(f"closeport {PORT}")
|
||||||
|
return result.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def show_dialog(message):
|
||||||
|
"""Show a non-blocking status dialog."""
|
||||||
|
hou.ui.displayMessage(
|
||||||
|
message,
|
||||||
|
title="Hermes Bridge",
|
||||||
|
severity=hou.severityType.ImportantMessage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
is_open, status = get_bridge_status()
|
||||||
|
|
||||||
|
if is_open:
|
||||||
|
# Bridge is already open — show status and offer close
|
||||||
|
choice = hou.ui.displayMessage(
|
||||||
|
f"Hermes Bridge is ACTIVE\n\nPort: {PORT}\nMask: {IP_MASK}\n\nClick 'Close' to shut down, or 'OK' to keep running.",
|
||||||
|
buttons=('OK', 'Close'),
|
||||||
|
default_choice=0,
|
||||||
|
close_choice=0,
|
||||||
|
title="Hermes Bridge",
|
||||||
|
severity=hou.severityType.Message,
|
||||||
|
)
|
||||||
|
if choice == 1:
|
||||||
|
result = close_bridge()
|
||||||
|
hou.ui.displayMessage(
|
||||||
|
f"Hermes Bridge CLOSED\n\n{result}",
|
||||||
|
title="Hermes Bridge",
|
||||||
|
severity=hou.severityType.ImportantMessage,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Bridge closed — offer to open
|
||||||
|
choice = hou.ui.displayMessage(
|
||||||
|
f"Hermes Bridge is INACTIVE\n\nPort {PORT} is not open.\n\nOpen it to allow Hermes remote control.",
|
||||||
|
buttons=('Open Bridge', 'Cancel'),
|
||||||
|
default_choice=0,
|
||||||
|
close_choice=1,
|
||||||
|
title="Hermes Bridge",
|
||||||
|
severity=hou.severityType.Warning,
|
||||||
|
)
|
||||||
|
if choice == 0:
|
||||||
|
result = open_bridge()
|
||||||
|
hou.ui.displayMessage(
|
||||||
|
f"Hermes Bridge ACTIVE\n\n{result}\n\nHermes can now control Houdini via:\n hhb eval \"hou.selectedNodes()[0].path()\"",
|
||||||
|
title="Hermes Bridge",
|
||||||
|
severity=hou.severityType.ImportantMessage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user