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:
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
|
||||
Reference in New Issue
Block a user