MoonRay Adventures
Getting DreamWorks' MoonRay built for Houdini on macos and farm
Intro
Open MoonRay is DreamWorks’ production path tracer, open-sourced and now ASWF-governed. It
ships with a Hydra render delegate (hdMoonray) and a set of Houdini DCC plugins. This is my attempt
at getting it built and tested, currently for macos and our farm, then working towards a windows
build… maybe.
Over a few months I:
- built MoonRay from source on Apple Silicon (standalone, then a Houdini variant)
- fixed the delegate and the Houdini plugin layer until Solaris was genuinely usable — lights, volumes, materials, in viewport and cli denoising, node menu cleanup, matched cli settings to match houdini viewport
- added new Houdini nodes that didn’t exist (a
VdbVolumeVOP, a Moonray Mesh Light LOP) - ported the whole thing to a headless, CPU-only Rocky Linux 9 render server off the same branch
The work lives in a personal fork
of the superproject plus two forked submodules
(hdMoonray, moonray_dcc_plugins), all on one shared branch.

Why bother
Besides it being interesting? We’re looking into alternatives for Karma. It’s a great renderer, but a tad expensive for us mortals here at the bottom of the planet. Getting it in houdini and on the farm will allow us to start testing real production scenes, and seeing where intergration is needed.
The standalone build
The standalone renderer came first, and mostly to prove the dependency chain worked on
Apple Silicon. MoonRay is 19 git repos tied together as submodules, with ~27 third-party
dependencies built from source via CMake ExternalProject.
Platform: M1 Pro, macOS 15.7.4 Sequoia, Xcode 16.4 (AppleClang 17), CMake 3.31.5.
It built — after five real fixes. The ones worth repeating:
Dylib portability (.. baked into install names). Eight autotools-built dependency
dylibs (OpenSSL, libcurl, log4cplus, microhttpd, libuuid, cppunit) ended up with install
names like:
/Applications/MoonRay/source/openmoonray/building/macOS/../../../../installs/lib/libssl.3.dylib
macOS dyld requires every intermediate directory in a .. traversal to exist — so any
machine without the source/ tree simply cannot load them. Root fix: canonicalise the
install prefix with get_filename_component(... ABSOLUTE) before handing it to autotools.
For an already-built tree, a script rewrites the -id and every LC_LOAD_DYLIB reference
(862 files patched on mine).
MicroHttpd’s autotools trap. The 0.9.72 tarball hardcodes aclocal-1.16 /
automake-1.16 in its Makefile; on automake 1.18 make install dies with error 127. The
robust fix is to no-op the regeneration tools entirely
(ACLOCAL=: AUTOCONF=: AUTOMAKE=: AUTOHEADER=: MAKEINFO=:) plus --disable-doc. Touching
timestamps — the obvious fix — is fragile, because ./configure re-establishes the broken
mtime ordering.
Qt platform plugin. moonray_gui aborted with Could not find the Qt platform plugin "cocoa" in "" because setup.sh never exported QT_QPA_PLATFORM_PLUGIN_PATH /
QT_PLUGIN_PATH. Two lines. Also a distribution lesson: installs/plugins/ must ship
alongside installs/lib/.
Textures must be .tx. Plain JPEG/PNG/untiled EXR fail with unknown oiio sample failure, once per sample. MoonRay initialises OIIO’s TextureSystem in strict mode
(accept_untiled=false, accept_unmipped=false). Convert first:
maketx input.jpg -o input.tx
This bites in Houdini too, so it’s worth internalising early. (A USD ArResolver that
auto-generates .tx on demand is still on my list.)
The Houdini build, and the USD version issues
MoonRay’s dependency set includes USD 22.11. Houdini 20.5.939 ships USD 24.3, with
its libraries renamed to libpxr_* and its Python at 3.11. The Houdini variant of MoonRay
is supposed to build against Houdini’s USD — but the include and link paths conspire against
you in three separate ways.
The include race
Xcode’s generator puts -isystem <installs>/include before the Houdini toolkit include
directory. So even with everything else configured correctly, the 22.11 headers win, and the
resulting objects link against the wrong libpxr_*/libusd_* namespace. Symptoms are
downstream and confusing: shader parameters silently do nothing, “Invalid info:id
DwaBaseMaterial” in the log viewer, the delegate half-working.
The fix that actually holds: a build wrapper that temporarily moves
installs/include/pxr aside so clang falls through to Houdini’s toolkit headers. Ugly,
but deterministic — and verified with otool -L (the delegate must show libpxr_* and
zero libusd_*).
The cleaner long-term fix is to build the deps with -DNO_USD=1 so standalone USD is never
installed at all. That’s what the fork does now.
Houdini 20.5 renames and Python 3.11
Three files needed to be told the truth about Houdini 20.5:
CMakeMacOSPresets.json— Houdini install dir,libhboost_python311-mt-a64.dylib(waspython39), andCMAKE_PREFIX_PATHwithPREFIX_PXRfirst.building/macOS/pxr-houdini/pxrTargets.cmake— Python 3.9 → 3.11 include/lib paths, and removing the toolkit include from every target’sINTERFACE_SYSTEM_INCLUDE_DIRECTORIES(26 targets) so CMake stops classifying it as a system include.pxrTargets-release.cmake—usdRiImaging→libpxr_usdRiPxrImaging.dylib, a Houdini 20.5 rename. Without it, the MoonRay delegate simply disappears from Houdini’s renderer list with a “not supported” note.
A compiler bug you have to patch around
Houdini’s sdf/childrenProxy.h trips a clang phase-1 error on SdfChildrenProxy::_Set.
A tiny header patch adding a _Set stub unblocks it. The same include chain is why
MoonRay’s hdMoonrayAdapters plugin is excluded from the Houdini build entirely, which
turns out to matter a lot later (see the mesh lights section).
Python bindings and setup.sh
Do not source scripts/setup.sh for the Houdini variant. It prepends the standalone
USD 22.11 Python bindings and crashes Houdini’s 3.11 interpreter. Use setupHoudini.sh,
which strips those paths and exports PXR_PLUGINPATH_NAME / PXR_PLUGIN_PATH. I later
hardened it with a prepend_unique_path helper (sourcing twice used to duplicate entries)
and made HOUDINI_PATH composable with whatever houdini_setup already set.
Also: BUILD_QT_APPS=NO for the Houdini preset. It drops moonray_gui and arras_render,
but the Arras runtime (arras4_core, mcrt_*, execComp) still builds, so the delegate’s
render path is intact.
Making Solaris work
The build succeeding is roughly the halfway point. Here’s what was broken once MoonRay showed up in the Houdini viewport.
long vs long long on arm64
On macOS arm64, long and long long are distinct C++ types. Houdini hands integer render
settings across as long long; the delegate’s ValueConverter only handled long and
int. Result: cannot convert long long to Int for basically every scene variable.
Fixed by adding long long handling to TYPE_BOOL/TYPE_INT/TYPE_LONG/TYPE_FLOAT,
later refactored into two helpers — _extractIntegral64() (int/long/long long → int64_t)
and _narrowIntChecked() (bounds-checked narrowing that logs instead of silently wrapping).
A companion case: Houdini exports ramp interpolation as a single TfToken ("hermite"),
while MoonRay’s iridescence_interpolations is an int vector. The RDL2 default is already
all-hermite, so the right answer is a silent skip.
Scene-object references from USD
ValueConverter::setAttribute declared TYPE_SCENE_OBJECT and
TYPE_SCENE_OBJECT_VECTOR “not supported yet”. That’s pervasive: it breaks
OpenVdbMap.openvdb_geometry, every camera’s medium_geometry/medium_material,
BakeCamera.geometry, ProjectCameraMap*.projector — anything that points at another
scene object.
The fix is central rather than per-site: setAttribute now takes a RenderDelegate* and
resolves the authored prim path(s) via getSceneObject(), with the five object-handling
call sites (Material, Camera ×2, Light, LightFilter) passing their delegate through.
Lights
Three separate bugs, all user-visible on day one of using the Light LOP:
| Light | Symptom | Cause |
|---|---|---|
| Point | Couldn't find DSO for 'PointLight' |
The .ds authored moonray:class from the Houdini menu label; MoonRay has no PointLight (a point is a SphereLight) |
| Env / Dome | Couldn't find DSO for 'None' |
The dome LOP has no lighttype parm and its prim type is DomeLight_1, so the class expression fell through to None |
| Distant | Renders black | MoonRay’s DistantLight needs normalized = true; Houdini authors UsdLux’s normalize = false, which the delegate honours |
The class fix is the satisfying one: rather than patching the label→class expression, stop
authoring moonray:class at all (flip its control companion default from "set" to
"none") and let the delegate’s own type map do the derivation —
sphereLight→SphereLight, domeLight→EnvLight, distantLight→DistantLight. Fewer moving
parts, .ds-only, no rebuild.
Distant normalize is scoped by narrowing the hidewhen on the moonray:normalized control
to UsdLuxDistantLight only, so area lights are untouched.
Point lights needed a delegate change too. Houdini’s “Point” light authors a
UsdLuxSphereLight with treatAsPoint = true and no radius — so it inherited the UsdLux
schema default of 0.5 and rendered as a fat sphere (and, with a cone, an oversized soft
spot, because lens_radius maps from the same parameter). The delegate ignored
treatAsPoint entirely. Now it maps to a small radius — 0.01, deliberately not 0,
because MoonRay bakes 1/radius into the light transform and rejects a zero radius as
singular, which turns the light off. normalized is forced true so intensity behaves as
power, the standard point-light convention.
Worth flagging for anyone adopting this: existing point lights get roughly 3× dimmer, because they were previously an unnormalised 0.5-radius sphere. That’s the correct behaviour, but it’s a re-tune.
Volumes
The mental model that nobody writes down: the geometry holds the grids, the shader only
tints. VdbGeometry owns model / density_grid / emission_grid / velocity_grid;
VdbVolume only multiplies the baked result.
So emission does not come from the shader. If emission_grid is unset, there is nothing
for incandescence_gain_mult to multiply, and your volume will never glow no matter what
you do on the material.
The delegate only special-cases two USD field names — density (which also sets
model) and velocity. temperature is ignored. Houdini’s default velocity field name is
v, which also doesn’t match. Everything else must go through the generic primvar override
path, and it must be a primvar in the primvars: namespace:
string primvars:moonray:emission_grid = "temperature_rgb" # ✅ read
string moonray:emission_grid = "temperature_rgb" # ❌ silently ignored
Denoising
“Optix mode not supported in this build” in Houdini, while standalone denoising was fine.
Three fixes in hdMoonray, and only the third one is the interesting one:
ArrasSettings::applySettings()never calledsetDenoiseEngine()— the OIDN toggle was dead code and the engine stayed on its OptiX default.setDenoiseEngine()had no macOS guard — OptiX is compiled out on macOS, so it must force OIDN.- The decisive one:
ArrasRenderer::connect()set the denoise mode on each freshly createdClientReceiverFb, but not the engine. So the receiver kept its OptiX default and the corrected engine value never actually arrived.
Fixes 1 and 2 are necessary and completely insufficient without 3. Both 1 and 3 are platform-independent — upstream PR candidates.
Per-output denoise controls that actually author
Adding denoise toggles to the RenderSettings LOP took three attempts, and each failure taught something about Solaris:
- Bare-named parms don’t route — husk only forwards
moonray:-namespaced attributes from the RenderSettings prim. - So rename them to the Houdini-encoded namespaced form
(
xn__moonrayenableDenoise_o8a=moonray:enableDenoise, computed withhou.text.encode()). Still nothing. - The missing piece — spotted by staring at every other MoonRay setting — is the control companion parm (the “Set or Create / Block / Do Nothing” menu). That’s what tells the Render Settings LOP to author the attribute onto the prim at all.
Delegate-side, the flag coalesces the bare (viewport) and namespaced (RenderSettings prim) keys with override semantics rather than OR, so the node can also turn denoise off. Defaults are “Set or Create”, i.e. the node drives the default rather than inheriting from the viewport.
While in there: MoonRay’s .ds generator had emitted a batch of scene variables as
top-level parms outside any group{}, so they rendered as loose widgets under the tab
bar. Moved into their proper tabs (Frame / Sampling / Driver), with Denoising placed right
after Sampling.
The TAB menu
The 126 MoonRay shader HDAs each baked four redundant TAB-menu placements into their
Tools.shelf section: DW Moonray, Digital Assets, DreamWorks/All, and
DreamWorks/HIDDEN. HIDDEN was a byte-identical duplicate of All; DW Moonray was the
incomplete one, missing 10 nodes.
A Python tool rewrites each HDA’s Tools.shelf via a hotl expand/collapse round-trip into
a single Moonray/<Subfolder> entry, suffix-classified into six folders — Materials 25,
Normal Maps 11, Maps 65, Displacement 4, Display Filters 18, Volumes 3 (= 126, full
coverage). Idempotent, backs up *.hda.orig, 21 tests.
Nodes that didn’t exist
VdbVolume as a first-class Solaris node
VdbVolume is the shader you need for non-trivial VDB rendering, and it was registered with
USD’s Sdr registry (so hand-authored USD worked) but had no Houdini UI at all — unlike
its siblings BaseVolume and CutoutVolume. Authoring a Volume + OpenVDBAsset without
it gets you has no moonray or fallback shaders.
Three pieces:
- VOP HDA — cloned from BaseVolume by expanding with
hotl -X, editing the text sections, and re-collapsing withhotl -C. 9 attributes fromVdbVolume.json. - LOP dialog
.ds— generated, withhou.text.encodedxn__inputs…parameter names and Set/Block/Do-Nothing control companions, byte-for-byte matching BaseVolume’s conventions down to the tab indentation in help strings. moonray_nodes.jsonentry — full shape parity. (Interesting finding: nothing in the open-source codebase reads this file at runtime —grepturns up only the CMake install rule. It’s a metadata mirror for a DreamWorks-internal tool.)
Also fixed along the way: the CMake install still shipped python3.9libs/, so the .ds
loader never ran under Houdini 20.5’s Python 3.11. (An existing symlink in the installed
tree was quietly masking it.)
Mesh lights, and the plugin that isn’t there
This one had a false start worth including, because the correction is the story.
I initially assumed mesh lights worked via direct USD authoring and only lacked a UI. They
don’t. A MoonrayMeshLight prim — and a standard UsdLuxGeometryLight — is silently
dropped: MoonRay never sees a light, and the emitter mesh just becomes render geometry.
Root cause, confirmed with otool -L: the hdMoonrayAdapters plugin (which provides
MoonrayMeshLightAdapter) shows 26 libusd_* and zero libpxr_* — it’s a leftover from
the standalone build. It’s intentionally excluded from the Houdini build because it
doesn’t compile against USD 24.3 (that same SdfChildrenProxy::_Set include chain). So
Houdini’s UsdImaging never images those prim types.
Porting the adapters is a real project. The workaround is a carrier prim piggyback:
carrier SphereLight (radius 0.01)
+ uniform token moonray:class = "MeshLight"
+ string moonray:geometry = "/path/to/emitter"
+ MeshLight params
│ UsdImaging images the SphereLight (a type it knows) → sphereLight Sprim
▼
delegate reads moonray:class first → builds an RDL2 MeshLight
delegate resolves moonray:geometry (string) → emitter Rprim → attaches geometry
▼
importance-sampled MeshLight renders
Two components:
- Delegate: a
moonray:geometrystring fallback in the geometry handler (used when theinputs:geometryrelationship yields nothing, because the adapter that would forward it isn’t running), reusingMesh::geometryForMeshLightand the scene-object path resolution from the earlier fix. Plus suppressing the now-legitimate “class may not be compatible with USD light type” warning. - A
Moonray Mesh LightLOP HDA so the user never sees the piggyback: full MeshLight parameter UI matching the other MoonRay lights, a Scene Graph Tree prim picker for the emitter, and an On toggle that drives the carrier’s visibility (the delegate ignoresmoonray:on— a light’s on/off is visibility plus intensity).
The critical, non-obvious requirement: the emitter must be invisible / out of the render layer, or MoonRay refuses it (“cannot be referenced … in Layer” → “MeshLight did not load”). The node always hides it.
Status: working and user-verified emitting in the Solaris viewport. Open items — the
standalone variant isn’t rebuilt with the change yet (so farm hd_render / RDL export don’t
have it), and moving the emitter live doesn’t re-sync the running render (the delegate never
marks the referencing light dirty when its emitter changes).
The same piggyback should extend to the nine light filters, since the delegate already
reads moonray:class for filters and stock UsdImaging images a base UsdLuxLightFilter.
Procedurals are the one case with no workaround — no native carrier to piggyback on — so
those genuinely need the adapters ported.
The debugging technique that paid for itself
Set one environment variable and the delegate dumps the exact RDL2 scene it built:
export HDMOONRAY_RDLA_OUTPUT=/tmp/dump.rdla
This turns “why does it look wrong?” into a diff. The case that made it: husk CLI renders of a subdivided head (DwaSkinMaterial SSS + 8K displacement) looked flatter and waxier than the Solaris viewport, as if SSS wasn’t contributing. Both used the same RenderSettings LOP.
Dumping RDL from both paths and diffing showed it was scene translation, not render settings — two independent causes:
- Husk’s default complexity had been OOM-killing the render (
mesh_resolution256 on a catmullClark cage), so--complexity 0had been added as a workaround. That overshoots: refineLevel 0 makes the delegate disable subdivision entirely — raw faceted cage, displacement applied at cage vertices only. - Husk defaults meshes to force single-sided, while the viewport
.dsdefaultsdoubleSided=1→ force two-sided. Backlit ear translucency needs those backfaces.
Fix: HDMOONRAY_DOUBLESIDED=1 and --complexity 1. The filtered RDL geometry sections then
came out byte-identical to the viewport dump, and the visual match was confirmed
viewport ↔ MPlay ↔ EXR. ~63 s, 1.3 GB peak.
One caveat: the env var works for husk but not the Solaris viewport, where the hip’s
saved sceneviewrenderopts overrides it — set the viewport’s “Rdl output” field manually
instead.
Two upstream-worthy gaps fell out of this: the delegate’s default mesh_resolution of 256
is pathological for subd and should adapt or cap, and there’s no scene-level tessellation
control anywhere in the MoonRay RenderSettings tab — you’re stuck between husk’s global
--complexity and per-mesh moonray:mesh_resolution.
From patches to forks
For a while the work lived as uncommitted edits plus .patch files in a separate “managing”
repo. That model collapses under its own weight around the point where you have delegate
changes, HDA changes, .ds changes, and CMake changes in flight simultaneously.
The migration: three personal forks (openmoonray, hdMoonray, moonray_dcc_plugins), all
on one branch, with the other 17 submodules tracking upstream and MoonRay core unforked.
The one trap: .gitmodules must use absolute URLs. Upstream uses relative ones
(../hdMoonray), which resolve against your fork’s origin and 404 for the 17 repos you
haven’t forked. After the rewrite, a clean recursive clone resolves all 19 submodules with
zero 404s and no patch step.
Reconciling the trees also caught genuine undocumented drift (the scene-object reference work existed only in the build tree), which is its own argument for committing source instead of shuffling patches.
The from-scratch sign-off
Then I did the thing everyone skips: wiped installs/, build/, build-deps/, and ran the
documented bootstrap from zero.
It found five defects on the happy path, none of which the working tree had ever exercised:
- A
NOUSD→NO_USDtypo across the docs. CMake silently ignores unused variables, so the deps build cheerfully installed USD 22.11 anyway — recreating exactly the stale-USD condition that breaks Houdini linkage. - The MicroHttpd autotools fix was documented but never actually committed.
- The build wrapper hard-required
installs/include/pxr— which a correctNO_USDbuild never creates. The wrapper errored on its own happy path. - An Xcode dependency cycle (
EnvLight↔moonray_rendering_pbr_tests) failed the full build; tests aren’t needed for the render path, soBUILD_TESTING: OFF. - The render helper broke under macOS’s stock bash 3.2 (
set -uplus an empty array expansion), and shipped without its execute bit.
Verified end-to-end afterwards via all three paths: husk sign-off render, interactive
Houdini, and CLI. 26 deps, 163 shader .so, 175 shader_json.
One operational lesson: the deps ExternalProject build cannot be incrementally
resumed — git-based deps re-run their update step on every cmake --build and cascade a
rebuild downstream, which defeats stamp-touching. Run it uninterrupted, in one pass.
Porting to a headless Rocky 9 render server
Same branch, same delegate source, Houdini 20.5.939 / Python 3.11 / USD 24.3 — but rooted at
/opt/MoonRay, no Qt, no CUDA, no OptiX, CPU only.
Only two Linux-specific problems, and both are the same shape as the macOS pxr-header dance: Houdini shadowing system libraries.
Houdini on every shell’s LD_LIBRARY_PATH. The box installs Houdini via
/etc/profile.d/houdini-20.5.*.sh, which forces /opt/hfs*/dsolib into every login shell.
Houdini’s bundled libcurl (dead CA path) and libtiff (unversioned symbols) shadow the
system ones — GitHub fetches fail during the deps build, and OpenImageIO fails to link. The
fix is a build-env.sh that scrubs Houdini out of the shell before every build step.
Nothing MoonRay builds needs it there: the deps are NO_USD, and husk plus the delegate
resolve their libraries via RUNPATH.
OpenVDB. Rocky gets most dependencies from dnf, but OpenVDB has to be source-built
into installs/ (like macOS) for the Houdini build, otherwise Houdini’s bundled OpenVDB 11
headers shadow dnf’s 9.1 and you get a link mismatch.
The payoff of a CPU-only preset: MOONRAY_USE_OPTIX=NO ⇒ HDMOONRAY_NO_OPTIX ⇒ the
delegate auto-selects OIDN and never reaches the OptiX abort. Verified by deliberately
enabling denoise without specifying the engine and confirming no
"Optix mode not supported in this build" — which is exactly what the three-part denoise
fix from before was for.
Render-verified end to end, including OIDN on CPU, 2026-07-21.
One helper script, two operating systems
The husk wrapper is a single cross-OS script installed to $OMR/bin/render-usd. It picks
OMR/HFS by uname, scrubs Houdini’s dsolib on Linux, hard-codes the renderer and
--complexity 1 (the OOM guard from Part 5), and passes everything after the input file
straight through to husk. A sibling render-denoise wraps MoonRay’s denoiser in -mode oidn, the only mode available on a GPU-less server.
$OMR/bin/render-usd scene.usd --frame 1 --output /tmp/out.exr
$OMR/bin/render-denoise beauty.exr denoised/beauty.exr
What’s still open
- Light filters (9 shaders) via the same carrier piggyback as mesh lights.
- Porting
hdMoonrayAdaptersto USD 24.3 — the only route to procedurals, and the cleanup that would retire the whole dual-USD split. - The Houdini-variant
hd_renderCLI links both USDs (Python 3.9 and 3.11) and aborts on double-registeredTfdebug symbols. Workaround: use the standalonehd_render. - Automatic
.txconversion via a customArResolver, so textures just work. - Display filter
.dsfiles (18 shaders) — possibly intentional, needs verifying. - Delegate-side subd defaults —
mesh_resolution256 is a footgun for catmullClark, and there’s no scene-level tessellation control. - Houdini 21 — the upgrade path is scoped (paths,
libpxr_pythonreplacinglibhboost_python311), not yet taken. - Deadline intergration.
- Houdini Version not hard coded O_o.
Several of these are platform-independent and upstream-worthy: the denoise engine wiring,
the long long conversions, the scene-object reference support, the subd defaults. The
macOS-specific guards are not.
Deets
- Fork:
github.com/llienofdoom/openmoonray(branchmacos-houdini), plus forks ofhdMoonrayandmoonray_dcc_plugins. - Upstream: Open MoonRay
, now under the
Academy Software Foundation (moved from
dreamworksanimationin 2026). - macOS: M1 Pro, Sequoia 15.7.4, Xcode 16.4, Houdini 20.5.939.
- Linux: Rocky 9, 8 cores, headless, Houdini 20.5.939.