Authoring a custom-material pack shader
A shader can bind its own textures and parameters — foam/ramp/flow/noise maps, toon ramps, tint
colours, scroll speeds — through a fixed custom-material set 5, with zero engine change. This
is the mechanism that lets stylized looks ship as content packs: pure .slang + textures + Wren,
no engine source (ADR 0073 / PLM-234).
Companion guide: Animating shaders with the frame clock & wind. A
real pack shader usually uses both — e.g. a water shader scrolls its own flow map by
plume3d_time() and binds its own normal/foam maps at set 5.
The set-5 contract
Section titled “The set-5 contract”The engine reserves set 5 for your material — you never touch engine code, you just declare and
read these (all via #include "plume3d.slang"):
| set 5 binding | slang (from plume3d.slang) | Wren writes it with |
|---|---|---|
| 0 | Texture2D Plume3D_CustomTextures[8] — read via plume3d_customTex(slot, uv) | mesh.setCustomTexture(slot, texture) — slot 0..7 |
| 1 | SamplerState Plume3D_CustomSampler (shared) | — (automatic) |
| 2 | plume3d_customParams[16] — read via plume3d_customParam(index) | mesh.setCustomColor(byteOffset, r,g,b,a) · mesh.setCustomParamFloat(byteOffset, v) · mesh.setCustomParams(list) |
Params are 256 bytes = 16 × float4, with byteOffset / 16 = index: setCustomColor(0, …) fills
plume3d_customParam(0); setCustomParamFloat(16, …) fills plume3d_customParam(1).x. The layout is
your pack’s own convention — the engine just carries the bytes. Full method reference:
Mesh → Custom material (set 5).
1. Write the pack shader
Section titled “1. Write the pack shader”apps/<pack>/shaders/src/<name>.slang (from the custom-material-pack example):
#include "plume3d.slang"// ... VSInput / VSOutput as usual; pass a UV to the fragment ...
[shader("fragment")]float4 fragment(VSOutput input): SV_Target { float4 tint = plume3d_customParam(0); // params[0] = your tint colour float scroll = plume3d_customParam(1).x; // params[1].x = your scroll speed
float4 ramp = plume3d_customTex(0, input.UV); // slot 0 float4 flow = plume3d_customTex(1, input.UV + float2(plume3d_time() * scroll, 0)); // slot 1, scrolled
return float4(ramp.rgb * tint.rgb * (0.4 + 0.6 * flow.r), 1.0);}2. Drive it from Wren
Section titled “2. Drive it from Wren”import "engine" for Graphics, Mesh, Texture
_mesh.setCustomTexture(0, Texture.load("textures/ramp.png")) // → slot 0_mesh.setCustomTexture(1, Texture.load("textures/flow.png")) // → slot 1_mesh.setCustomColor(0, 1.0, 0.55, 0.2, 1.0) // → params[0] tint_mesh.setCustomParamFloat(16, 0.15) // → params[1].x scroll speedAny setCustom* call marks the mesh as carrying a custom material; the engine binds set 5 for its
draws automatically. mesh.clearCustom() drops it.
3. Ship it as a pack
Section titled “3. Ship it as a pack”A pack is just content dropped into a game’s package (no separate engine format): the folder holds
shaders/src/*.slang (plus compiled shaders/bin/*.spv), textures/, .blend models, and Wren
helpers. The consuming game references your assets by path. See the reference app
apps/custom_material_pack.
Verify it works
Section titled “Verify it works”plume3d apps/custom_material_pack # self-screenshots, then exitsThe reference proof: with a neutral grayscale ramp texture, the output is a warm-tinted
gradient. A coloured result from a colourless texture means your tint params reached the shader,
and the gradient means your texture is sampled. If you see flat white, the custom set isn’t bound
(check that a setCustom* was called before drawing).
Rules & limits
Section titled “Rules & limits”- Slots 0–7 and 256 param bytes are the budget — the engine bounds-checks every setter (an out-of-range slot/offset is ignored). That is the whole set a pack can drive; there is no way to add more bindings from content, by design (this keeps packs engine-change-free and MoltenVK-safe).
- Read the Wren-scripting rules before writing
.wren(enginedocs/wren-scripting-gotchas.md). - v1 limitations (follow-ups): custom textures sample through the sRGB view — fine for colour ramps; a linear-data variant (for flow/noise/data maps that must not be sRGB-decoded) is planned. The per-frame custom-draw budget is 1024 draws.
- Custom material is additive and inert for ordinary draws — existing shaders never see set 5.
Related
Section titled “Related”- API: Mesh → Custom material (set 5) and the set-5 binding contract.
- Example: Custom Material Pack (
apps/custom_material_pack). - Companion guide: Frame clock & wind.