Toon Pack Demo
App: apps/toon_pack_demo/
Demonstrates the Toon / Stylized-Shading content pack (engine PLM-261): a whole stylized
look — a cel/toon surface shader plus a depth-edge ink outline post effect — built
with zero engine source changes, on only the public shader ABIs and the
set-5 custom material params. It is the reference proof of
Plume3D’s engine-vs-pack boundary: the engine ships neutral, look-free capabilities; the
pack ships the look. Drop the two .slang files (and their compiled bin/*.spv) into any
game’s content and reference them by path — exactly how the engine already resolves shaders.
Run from root
Section titled “Run from root”% ./plume3d toon_pack_demoThree primitives (a box, a cylinder, and a cone) rotate under the toon surface shader while the outline post effect inks their silhouettes. The demo captures a screenshot at frame 90 and exits at frame 110.
The two shaders
Section titled “The two shaders”| File | Kind | What it is |
|---|---|---|
shaders/src/toon.slang | surface shader | Cel diffuse: N·L quantized into hard bands, the darkest band tinted toward a cool shadow colour, plus a view-space Fresnel rim hardened into a toon rim light. Built on the mesh ABI (#include "plume3d.slang"). |
shaders/src/outline.slang | post effect | A dark ink outline from scene-depth discontinuities (silhouettes + depth steps), composited over the scene. A 4-tap depth cross → central-difference gradient → ink where neighbours disagree. Built on the post ABI (#include "plume3d_post.slang"). |
What it does
Section titled “What it does”-
Loads the toon surface shader (the active shader for the primitive draws) and the ink outline post shader:
_toon = Graphics.loadShader("shaders/toon")Graphics.loadShader("shaders/outline") // built as a post pipeline on first use -
Builds three saturated-colour primitives with
MeshGenand draws them each frame with the toon surface shader:Graphics.drawMesh(_box, _toon, model) // the cel bands quantize each base colourGraphics.drawMesh(_cyl, _toon, model)Graphics.drawMesh(_cone, _toon, model) -
Adds the outline post effect each frame (the post list is per-frame, like a draw call), passing the ink colour, thickness, and depth-edge tuning as params:
// [ inkR, inkG, inkB, thicknessPx, depthThreshold, edgeGain ]Graphics.addPostEffect("shaders/outline", [0.05, 0.05, 0.06, 1.6, 0.0009, 3.0])
Why the outline needs no new engine pass. A depth-edge outline was expected to require a new
engine pass (an inverted-hull or edge-detect pass). It does not: the post stack
(Graphics.addPostEffect, RFX A#5) plus
sampleable scene depth (RFX A#2) already expose everything an outline reads — the post shader
#include "plume3d_post.slang" and samples plume3d_sceneColor + plume3d_sceneDepth. So the
outline is pure pack content, and the whole Toon pack touches no engine source, descriptor
layout, or frozen contract.
Tuning the look
Section titled “Tuning the look”Both shaders are tuned entirely through pack params — no engine recompile.
Toon surface — set-5 custom params
Section titled “Toon surface — set-5 custom params”All optional; the shown default applies when a param is left 0. A game re-skins the toon look by
passing different set-5 custom params on the mesh
(Mesh.setCustomColor / setCustomParamFloat) — still with no engine change. (The demo itself
draws with the defaults.)
| Param | Meaning | Default |
|---|---|---|
customParam(0).xyz | shadow tint (multiplies the darkest band) | cool (0.40, 0.45, 0.60) |
customParam(0).w | number of light bands | 3 |
customParam(1).xyz | rim colour | warm (1.00, 1.00, 0.90) |
customParam(1).w | rim power (higher = tighter rim) | 4 |
customParam(2).x | rim strength 0..1 | 0.6 |
For art-directed bands, a pack can bind a 1-D ramp texture through the same set-5
custom-texture slot (Mesh.setCustomTexture) and sample it with plume3d_customTex(...) instead
of the procedural quantize — again with no engine change.
Outline — post params
Section titled “Outline — post params”Graphics.addPostEffect("shaders/outline", [inkR, inkG, inkB, thicknessPx, depthThreshold, edgeGain])
— default ink black, 1.5 px thickness, depth-edge threshold 0.0009 (raw non-linear depth
units), and edge-softness gain 3.0.
- Graphics —
loadShader,addPostEffect(the post stack the outline rides). - Mesh — custom material (set 5) — how a game tunes the toon
surface (
setCustomColor/setCustomParamFloat/setCustomTexture). - MeshGen —
box,cylinder,cone(the three primitives). - The mesh ABI
plume3d.slangand the post ABIplume3d_post.slang— the two shipped shader includes the pack is built on.
Notes and limitations
Section titled “Notes and limitations”- The outline is depth-based. It catches silhouettes and depth steps but not interior creases on a continuous surface — those need scene normals, and the engine ships a depth buffer but no normal G-buffer. A normal-buffer outline is a future engine enhancement, not a pack change.
- The toon surface uses a single fixed key-light direction. A game can drive it from a param or its own lighting; wiring a scene light into the pack shader is left to the consuming game.
- Content pack, not an engine feature. Packaging, licensing, and production art for a shippable pack are owner/business decisions; this in-repo demo is the exercised, gate-covered reference. See the Custom Material Pack example and the Custom Material Packs guide for the set-5 mechanism this look is tuned through, and the Post Depth example for the post-and-depth stack the outline rides.