render tools · td work · WIP / Live

AOV
Manager

A shared-config render pass manager for Houdini (Mantra & Karma XPU) and Unreal Engine 5 — one file drives both a standalone PySide6 desktop app and a native Houdini shelf tool, generating production-correct AOV scripts and MRQ configs per preset.

Python 3.9+ PySide6 / PySide2 Houdini Mantra Houdini Karma XPU Unreal Engine 5 Movie Render Queue LPE Strings EXR / ACES
3
Renderers
3
Style Presets
10
Standard Passes
2
App Targets
1
Config File*

* Edit aov_config.py only — both the desktop app and the Houdini shelf update automatically.

// 01 — Overview

One config.
Two apps. Three renderers.

AOV Manager uses a shared-config architecture — aov_config.py is the single source of truth for every pass definition, naming token, folder template, and renderer-specific technical parameter. Both app targets read from it at runtime, so a change in one place propagates everywhere.

🖥

Standalone App

standalone_app.py — pip install PySide6

Full desktop UI built in PySide6. Runs without Houdini installed — useful for pre-production pass planning, config review, and generating scripts to hand off.

  • Pipeline selector — Mantra, Karma XPU, or UE5
  • Style preset toggle with live accent colour theming
  • Pass cards — click to enable / disable per AOV
  • Naming tab with live token preview
  • Generate, copy, or save script to file
  • Film / Stylized mode toggle (ACES vs sRGB)

Houdini Shelf Tool

houdini_shelf.py — paste into shelf Script tab

Identical UI rebuilt in PySide2 (Houdini's built-in Qt). Adds a Build in Houdini button that executes the generated script directly in the live session — creating the ROP node and output folders on the spot.

  • All standalone features, inside Houdini's own Qt context
  • Build in Houdini ⬡ — exec's script into live session
  • Creates Mantra / Karma ROP with correct AOV nodes
  • Auto-creates all output directories via os.makedirs
  • Dialog stored at hou.session to prevent GC
  • Searches for aov_config.py in $HOUDINI_USER_PREF_DIR then $HIP

// 01b — Config Loading

How the apps
find the config

The standalone app imports aov_config directly as a sibling module — keep all three files in the same folder. The Houdini shelf tool searches two locations in priority order so it works as both a studio-wide shared install and a per-shot local override.

Python standalone_app.py — direct sibling import
# All three files sit in the same folder — import is always local.
import aov_config as cfg

# Presets, pass lists, and generator functions all come from cfg.
preset    = cfg.PRESETS["ocean"]
pipelines = cfg.PIPELINES     # ["Houdini / Mantra", "Houdini / Karma", "Unreal Engine 5"]
script    = cfg.generate_houdini_script(
    pipeline_idx    = 0,        # 0 = Mantra, 1 = Karma
    preset_key      = "ocean",
    project         = "HERO",
    layer           = "hero_layer",
    version         = 1,
    root            = "/renders",
    film_mode       = True,
    standard_states = {"beauty": True, "zdepth": True},
    custom_states   = {"caustics": True, "fog_vol": True},
)
Python houdini_shelf.py — dynamic path search
# The shelf searches two locations — shared install first, then $HIP fallback.
import sys, os, importlib

search_paths = [
    os.path.join(hou.getenv("HOUDINI_USER_PREF_DIR"), "scripts", "python"),
    hou.getenv("HIP"),    # per-shot override: drop aov_config.py next to your .hip
]

cfg = None
for path in search_paths:
    candidate = os.path.join(path or "", "aov_config.py")
    if os.path.exists(candidate):
        if path not in sys.path:
            sys.path.insert(0, path)
        cfg = importlib.import_module("aov_config")
        break

if cfg is None:
    hou.ui.displayMessage(
        "aov_config.py not found.\n"
        "Place it in $HOUDINI_USER_PREF_DIR/scripts/python/ or next to your .hip file."
    )

// 02 — Presets

Style presets &
custom AOV passes

Each preset defines its own naming convention, folder structure, accent colour, and a set of custom passes stacked on top of the 10 standard passes. All passes carry full renderer metadata — Mantra VEX variables, Karma LPEs, and UE5 MRQ class names.

🌊

Ocean Mode

accent #00d4ff

Underwater and bioluminescence renders. Caustics, volumetric fog, emissive isolation, and particle scatter.

  • Caustics Pass — vm_usecaustics=1, transmitted diffuse LPE
  • Fog Volume — vm_volumesteprate, Karma CV.* LPE
  • Biolum Emission — custom biolum VEX export, emissive LPE
  • Scatter Volume — CV<RS>.* scattered volume LPE
🌌

Sci-Fi Mode

accent #bf5fff

Neon, holographic, and tech-noir aesthetics. Bloom isolation, rim separation, gradient mattes, and holo overlays.

  • Glow Mask — emissive Ce channel, C[ES].* LPE
  • Rim Light Pass — custom shader export, specular LPE
  • Gradient Mask — custom float export, world-space Y driven
  • Hologram Pass — custom export, ObjectID stencil in UE5
🎨

Painterly Mode

accent #ffb347

Toon, cel, and stylised renders. Ramp gradients, edge linework, shadow isolation, and flat colour mattes.

  • Toon Ramp Pass — custom float 0–1 shading gradient export
  • Linework Pass — normal discontinuity, vm_edgesamples=4
  • Shadow Isolation — vm_shadowtype=1, shadow LPE
  • Color ID Flat — Op object identifier, Cryptomatte

// 03 — Passes

Standard pass
reference

All 10 standard passes are available in every preset. Each carries correct renderer parameters — no placeholder values.

Pass Mantra — vm_variable / filter Karma — LPE / variable UE5 — MRQ Class Default
Beauty Cf+Af · gaussian · float16 C.* DeferredPassBase ✦ on
Diffuse diffuse · gaussian · float16 C<RD>.* ImagePassBase ⚠ deferred ✦ on
Specular specular · gaussian · float16 C<RS>.* ImagePassBase ⚠ deferred — off
SSS sss · gaussian · vm_useshading=1 C<TD>.* ImagePassBase ⚠ deferred — off
Z-Depth Pz · minmax min · float32 variable: z ImagePassBase ⚠ deferred ✦ on
ID / Mask Op · box · vm_cryptolayers=6 variable: cryptomatte ObjectIdRenderPass — off
Emission Ce · gaussian · float16 C[ES].* ImagePassBase ⚠ deferred — off
World Normal N · gaussian · float16 variable: N ImagePassBase ⚠ deferred — off
Ambient Occ. occlusion · gaussian · vm_occlusionsamples=16 C<AO> ImagePassBase ⚠ deferred — off
Shadow Matte shadow · gaussian · vm_shadowtype=1 C.*<L.> ImagePassBase ⚠ deferred — off

⚠ deferred = requires Deferred Rendering mode in UE5 MRQ. The config output flags this automatically.

// 04 — Code

Key implementation
patterns

Python aov_config.py — renderer metadata per pass
# Every pass is a dict with renderer-specific technical data.
# One source of truth — both apps read from this at runtime.

{
    "id":         "zdepth",
    "label":      "Z-Depth",
    "default_on": True,
    "mantra": {
        "vm_variable": "Pz",          # VEX export to sample
        "vm_channel":  "zdepth",      # AOV output channel name
        "vm_vextype":  "float",
        "vm_pfilter":  "minmax min", # never blur depth
        "vm_quantize": "float32",    # full precision for depth
        "extra":       {},
        "custom_var":  False,
    },
    "karma": {
        "lpe":        "",             # no LPE — shader variable export
        "variable":   "z",
        "type":       "float",
        "light_group":"",             # set to e.g. "rim" once lights named
        "custom_var": False,
    },
    "ue5": {
        "pass_class":    "MoviePipelineImagePassBase",
        "deferred_only": True,
        "notes":         "Enable SceneDepth in Buffer Visualization.",
    },
}
Python — Generated Output aov_config.py → generate_houdini_script() · Mantra example
# ── Z-Depth ──────────────────────────────────
aov_zdepth = rop.createNode('aov')
aov_zdepth.parm('vm_variable').set('Pz')
aov_zdepth.parm('vm_channel').set('zdepth')
aov_zdepth.parm('vm_vextype').set('float')
aov_zdepth.parm('vm_pfilter').set('minmax min')
aov_zdepth.parm('vm_quantize').set('float32')
aov_zdepth.parm('vm_filename').set(r'/renders/HERO/ocean/hero_layer/zdepth/HERO_hero_layer_ocean_zdepth_v001.$F4.exr')

# ── Caustics Pass ──────────────────────────────────
# ⚠ CUSTOM SHADER EXPORT: vm_variable='caustics' — replace with your actual VEX export name if different
aov_caustics = rop.createNode('aov')
aov_caustics.parm('vm_variable').set('caustics')
aov_caustics.parm('vm_channel').set('caustics')
aov_caustics.parm('vm_vextype').set('vector')
aov_caustics.parm('vm_pfilter').set('minmax median')
aov_caustics.parm('vm_quantize').set('float16')
aov_caustics.parm('vm_usecaustics').set(1)
aov_caustics.parm('vm_filename').set(r'/renders/HERO/ocean/hero_layer/caustics/HERO_hero_layer_ocean_caustics_v001.$F4.exr')
INI — Generated Output aov_config.py → generate_ue5_config() · UE5 MRQ example
; ══ AOV Manager — UE5 Movie Render Queue ═════════════
; Preset    : 🌊 Ocean Mode
; Colorspace: ACES_ACEScg

[RenderPasses]
; Format: PassID=True  |  MRQ Class  |  Rendering mode
EnablePass_BEAUTY=True             ; MoviePipelineDeferredPassBase    |  ✓ Any mode
EnablePass_ZDEPTH=True             ; MoviePipelineImagePassBase       |  ⚠ Deferred only
EnablePass_CAUSTICS=True           ; MoviePipelineImagePassBase       |  ⚠ Deferred only
EnablePass_EMISSION2=True          ; MoviePipelineImagePassBase       |  ⚠ Deferred only

[PassSetupNotes]
; CAUSTICS:   No native caustics pass in UE5 MRQ. Use CustomDepthStencil on caustics decal actors.
; EMISSION2:  Enable Emissive in Buffer Visualization. Separate biolum actors onto a stencil layer.

; ⚠ DEFERRED RENDERING REQUIRED for some passes above.
; In MRQ: Add a 'Deferred Rendering' setting block.

Output Folder Structure

/renders/ HERO_PROJECT/ ocean/ hero_layer/ beauty/ HERO_PROJECT_hero_layer_ocean_beauty_v001.$F4.exr zdepth/ HERO_PROJECT_hero_layer_ocean_zdepth_v001.$F4.exr caustics/ HERO_PROJECT_hero_layer_ocean_caustics_v001.$F4.exr fog_vol/ HERO_PROJECT_hero_layer_ocean_fog_vol_v001.$F4.exr ← all dirs created automatically emission2/ HERO_PROJECT_hero_layer_ocean_emission2_v001.$F4.exr scatter/ ...

// 04b — Generator Internals

How the script
generator works

Both generators follow the same pattern: collect active passes, resolve paths, then write renderer-specific code line by line into a list that's joined at the end. Private helper functions (_gen_mantra_aov and _gen_karma_aov) handle the per-pass block so the main generator stays readable.

Python aov_config.py — build_active_passes()
# Collects all enabled pass dicts in order: standard first, then preset custom passes.
# standard_states / custom_states are {pass_id: bool} dicts from the UI card state.
def build_active_passes(preset_key, standard_states, custom_states):
    active = []
    for p in STANDARD_PASSES:
        if standard_states.get(p["id"], False):
            active.append(p)
    for p in PRESETS[preset_key]["passes"]:
        if custom_states.get(p["id"], True):
            active.append(p)
    return active   # list of full pass dicts — each has mantra / karma / ue5 blocks
Python aov_config.py — _gen_mantra_aov() helper
# Called once per active pass. Returns a list of lines to append to the script.
# The custom_var flag adds a visible warning comment so the artist knows
# which vm_variable to swap for their own shader export.
def _gen_mantra_aov(p, folder, filename):
    m         = p["mantra"]
    pid       = p["id"]
    full_path = "{}/{}.$F4.exr".format(folder, filename)
    lines     = ["", "# -- {} --".format(p["label"])]

    if m["custom_var"]:
        lines.append(
            "# ⚠ CUSTOM SHADER EXPORT: vm_variable='{}' — replace with your actual VEX export"
            .format(m["vm_variable"])
        )

    lines += [
        "aov_{0} = rop.createNode('aov')".format(pid),
        "aov_{0}.parm('vm_variable').set('{1}')".format(pid, m["vm_variable"]),
        "aov_{0}.parm('vm_channel').set('{1}')" .format(pid, m["vm_channel"]),
        "aov_{0}.parm('vm_vextype').set('{1}')" .format(pid, m["vm_vextype"]),
        "aov_{0}.parm('vm_pfilter').set('{1}')" .format(pid, m["vm_pfilter"]),
        "aov_{0}.parm('vm_quantize').set('{1}')".format(pid, m["vm_quantize"]),
        "aov_{0}.parm('vm_filename').set(r'{1}')".format(pid, full_path),
    ]
    for parm, val in m["extra"].items():   # e.g. vm_usecaustics=1, vm_occlusionsamples=16
        lines.append("aov_{0}.parm('{1}').set({2})".format(pid, parm, val))
    return lines
Python aov_config.py — resolve_name() and resolve_folder()
# Tokens are resolved by simple string replacement — no regex, no templating engine.
# {frame} is the only token that varies by renderer.
def resolve_name(template, project, layer, pass_id, version, pipeline_idx=0):
    frame_token = "$F4" if pipeline_idx < 2 else "{frame_number}"
    return (template
        .replace("{project}", project)
        .replace("{layer}",   layer)
        .replace("{pass}",    pass_id)
        .replace("{version}", str(version).zfill(3))
        .replace("{date}",    datetime.date.today().strftime("%Y_%m_%d"))
        .replace("{frame}",   frame_token))

# Example resolution — Ocean preset, Mantra pipeline:
# "{project}_{layer}_ocean_{pass}_v{version}"
# → "HERO_hero_layer_ocean_caustics_v001"   (then appended with .$F4.exr)

// 05 — Naming

Naming convention
tokens

Templates are fully configurable per preset in aov_config.py. Tokens are resolved at generation time — frame tokens output renderer-native syntax automatically.

{project}
Project name field → e.g. HERO_PROJECT
{layer}
Layer name field → e.g. hero_layer
{pass}
Pass identifier → e.g. caustics, zdepth
{version}
Zero-padded version → e.g. 001, 012
{date}
Resolved at generation → YYYY_MM_DD
{frame}
Renderer-native → $F4 (Houdini) · {frame_number} (UE5)
Template Ocean Mode default — naming_template
{project}_{layer}_ocean_{pass}_v{version}
→ HERO_PROJECT_hero_layer_ocean_caustics_v001

// 06 — Install

Getting started

01

Unzip or clone

Download the zip or clone the repo. The three files are self-contained — no build step required.

aov_manager/ aov_config.py ← only file you ever need to edit standalone_app.py ← double-click or python standalone_app.py houdini_shelf.py ← paste into Houdini shelf Script tab
02

Standalone app — install PySide6

PySide6 is the only dependency for the desktop app. Double-click or run from terminal after install.

Bash
pip install PySide6
python standalone_app.py
03

Houdini shelf — paste and run once

In Houdini, right-click the shelf → New Tool → Script tab. Paste the entire contents of houdini_shelf.py and click Accept. The tool will appear on your shelf.

Path — where to put aov_config.py
$HOUDINI_USER_PREF_DIR/scripts/python/aov_config.py   # recommended
$HIP/aov_config.py                                    # per-project fallback
04

Configure — edit aov_config.py

Set your DEFAULT_PROJECT, DEFAULT_ROOT, and DEFAULT_PIPELINE at the top of aov_config.py. For custom shader passes flagged ⚠ CUSTOM in the generated output, swap vm_variable to match your actual VEX export name. For Karma light groups, fill in light_group once you've named your lights.

// 07 — Architecture

Design decisions &
technical patterns

Shared Config
aov_config.py
Single source of truth — all presets, passes, naming templates, and renderer parameters live in one file. Both apps import it at runtime. Adding a pass or changing a template propagates to both interfaces instantly, with no duplication to maintain.
Renderer Metadata
aov_config.py — pass dicts
Each pass carries a mantra, karma, and ue5 block with correct technical parameters — actual VEX export variable names, proper pixel filters (e.g. minmax min for depth), Karma LPE strings, UE5 MRQ C++ class names, and deferred-only flags. No placeholder strings or free-form pass names that a renderer wouldn't understand.
Custom Var Flagging
generate_houdini_script()
Passes that require a custom shader export — like biolum, rimlight, or toon_ramp — set "custom_var": True. The generator writes a # ⚠ CUSTOM SHADER EXPORT comment directly above the relevant vm_variable line so the artist knows exactly which line to swap without reading any documentation.
LPE Light Groups
aov_config.py — karma block
Karma LPEs default to generic syntax with no light group dependency so the tool works before lights are named. A light_group field is present on every pass — set it to e.g. "rim" and the generator injects the group into the LPE string automatically. A comment in the generated output reminds the artist to do this.
PySide2 / PySide6
both app files
The standalone app uses PySide6. The shelf tool uses PySide2 — Houdini's bundled Qt — to avoid version conflicts with Houdini's own UI thread. Both share the same widget structure and stylesheet patterns, with accent colours and card states driven from aov_config.py preset data at runtime.
UE5 Deferred Warning
generate_ue5_config()
The UE5 generator checks every active pass's deferred_only flag. If any enabled pass requires Deferred Rendering mode, a warning block is automatically appended to the config with MRQ setup instructions — the artist doesn't need to remember which passes need it.
Live Session Build
houdini_shelf.py — Build in Houdini
The shelf tool's Build in Houdini button calls exec() on the generated script directly inside the running Houdini Python session. This creates the ROP node tree, sets all AOV parameters, and calls os.makedirs on every output folder — turning the UI into a one-click production setup rather than a script generator requiring a separate paste step.

// 08 — Extending

Adding passes
& custom presets

Everything lives in aov_config.py. Adding a new standard pass, a new custom preset pass, or an entirely new style preset requires only editing that one file — no UI code to touch.

Python aov_config.py — adding a new standard pass
# Append to STANDARD_PASSES. The UI will show it as a new card automatically.
{
    "id":         "velocity",
    "label":      "Motion Vector",
    "desc":       "Per-pixel velocity for motion blur in comp",
    "default_on": False,
    "mantra": {
        "vm_variable": "vel",        # VEX export — must exist in your shader
        "vm_channel":  "velocity",
        "vm_vextype":  "vector",
        "vm_pfilter":  "box",        # never filter velocity with gaussian
        "vm_quantize": "float16",
        "extra":       {},
        "custom_var":  False,
    },
    "karma": {
        "lpe":        "",
        "variable":   "velocity",
        "type":       "vector",
        "light_group":"",
        "custom_var": False,
    },
    "ue5": {
        "pass_class":    "MoviePipelineImagePassBase",
        "deferred_only": True,
        "notes":         "Enable MotionBlur / Velocity in Buffer Visualization.",
    },
},
Python aov_config.py — adding a new style preset
# Add a new key to PRESETS. The UI preset buttons rebuild from this dict at startup.
PRESETS["horror"] = {
    "label":           "🩸 Horror Mode",
    "color_hex":       "#e05c6e",
    "naming_template": "{project}_{layer}_horror_{pass}_v{version}",
    "folder_template": "{root}/{project}/horror/{layer}/{pass}/",
    "passes": [
        {
            "id": "blood_mask", "label": "Blood Mask",
            "desc": "Fluid / blood isolation matte", "default_on": True,
            "mantra": {
                "vm_variable": "blood_mask", "vm_channel": "blood_mask",
                "vm_vextype": "float", "vm_pfilter": "gaussian",
                "vm_quantize": "float16", "extra": {}, "custom_var": True,
            },
            "karma":  {"lpe": "", "variable": "blood_mask", "type": "float", "light_group": "", "custom_var": True},
            "ue5":    {"pass_class": "MoviePipelineObjectIdRenderPass", "deferred_only": False, "notes": "Stencil-isolate fluid actors."},
        },
    ],
}

# Also add to PRESET_COLORS so the UI accent colour resolves correctly:
PRESET_COLORS["horror"] = "#e05c6e"
Python aov_config.py — changing default project settings
# These values pre-fill the UI on launch. Change once per project.
DEFAULT_PROJECT  = "ABYSS_SQ010"    # project name field
DEFAULT_LAYER    = "ocean_hero"      # layer name field
DEFAULT_VERSION  = 1                 # version spinner starting value
DEFAULT_ROOT     = "/mnt/renders"   # root path field
DEFAULT_PRESET   = "ocean"           # which preset is selected on open
DEFAULT_PIPELINE = 0                 # 0 = Mantra · 1 = Karma · 2 = UE5

# Output format — applies to all renderers:
OUTPUT_FORMAT    = "EXR"
OUTPUT_BITDEPTH  = "16"
FILM_COLORSPACE  = "ACES_ACEScg"
STYL_COLORSPACE  = "sRGB"