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.
* 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.
# 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}, )
# 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 #00d4ffUnderwater and bioluminescence renders. Caustics, volumetric fog, emissive isolation, and particle scatter.
- Caustics Pass —
vm_usecaustics=1, transmitted diffuse LPE - Fog Volume —
vm_volumesteprate, KarmaCV.*LPE - Biolum Emission — custom
biolumVEX export, emissive LPE - Scatter Volume —
CV<RS>.*scattered volume LPE
Sci-Fi Mode
accent #bf5fffNeon, holographic, and tech-noir aesthetics. Bloom isolation, rim separation, gradient mattes, and holo overlays.
- Glow Mask — emissive
Cechannel,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 #ffb347Toon, 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 —
Opobject 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
# 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.", }, }
# ── 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')
; ══ 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
// 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.
# 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
# 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
# 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}_{layer}_ocean_{pass}_v{version}
→ HERO_PROJECT_hero_layer_ocean_caustics_v001
// 06 — Install
Getting started
Unzip or clone
Download the zip or clone the repo. The three files are self-contained — no build step required.
Standalone app — install PySide6
PySide6 is the only dependency for the desktop app. Double-click or run from terminal after install.
pip install PySide6 python standalone_app.py
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.
$HOUDINI_USER_PREF_DIR/scripts/python/aov_config.py # recommended $HIP/aov_config.py # per-project fallback
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
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.
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.
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.
aov_config.py preset data at runtime.
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.
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.