@call-me-sensei/toonlab 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +127 -4
- package/ATTRIBUTION.md +41 -3
- package/README.md +234 -34
- package/docs/characters.md +144 -0
- package/docs/debug-panel.md +126 -0
- package/docs/docs.css +589 -0
- package/docs/environment.md +186 -0
- package/docs/getting-started.md +180 -0
- package/docs/index.html +14 -0
- package/docs/lab-architecture.md +100 -0
- package/docs/lighting.md +825 -0
- package/docs/main.jsx +739 -0
- package/docs/mcp.md +97 -0
- package/docs/post-processing.md +98 -0
- package/docs/settings-reference.md +1878 -0
- package/docs/shader-constants.md +77 -0
- package/docs/sky.md +182 -0
- package/docs/style-labs.md +309 -0
- package/docs/texture-lab.md +135 -0
- package/docs/toon-shading.md +183 -0
- package/docs/tsl-conventions.md +167 -0
- package/docs/vegetation-sky.md +275 -0
- package/docs/water.md +430 -0
- package/docs/weather.md +200 -0
- package/docs/world-scale.md +111 -0
- package/mcp/server.mjs +610 -0
- package/mcp/style-lab-tools.mjs +371 -0
- package/mcp/vite-plugin.mjs +174 -0
- package/mcp/workspace.mjs +397 -0
- package/package.json +64 -5
- package/src/ambientfx/INTEGRATION.md +164 -0
- package/src/ambientfx/ambientFxPresets.js +83 -0
- package/src/ambientfx/ambientFxSettings.js +368 -0
- package/src/ambientfx/emitters.js +169 -0
- package/src/ambientfx/index.js +5 -0
- package/src/ambientfx/particleBackbone.js +493 -0
- package/src/ambientfx/stylizedAmbientFx.js +450 -0
- package/src/assetlib/ambientcg.js +117 -0
- package/src/assetlib/assetRef.js +91 -0
- package/src/assetlib/importedEntry.js +40 -0
- package/src/assetlib/index.js +23 -0
- package/src/assetlib/kaykit.js +192 -0
- package/src/assetlib/kaykitStaticIndex.js +700 -0
- package/src/assetlib/loadImported.js +143 -0
- package/src/assetlib/opensource3d.js +113 -0
- package/src/assetlib/polyhaven.js +182 -0
- package/src/assetlib/polypizza.js +115 -0
- package/src/assetlib/smithsonian.js +214 -0
- package/src/assetlib/sources.js +279 -0
- package/src/assetlib/zip.js +58 -0
- package/src/biome/biomeGenerator.js +385 -0
- package/src/biome/biomeRuntime.js +299 -0
- package/src/biome/index.js +2 -0
- package/src/buildinggen/buildingAsset.js +44 -0
- package/src/buildinggen/buildingGrammar.js +311 -0
- package/src/buildinggen/buildingMesh.js +451 -0
- package/src/buildinggen/buildingPresets.js +46 -0
- package/src/buildinggen/buildingRecipe.js +100 -0
- package/src/buildinggen/buildingSettings.js +238 -0
- package/src/buildinggen/index.js +6 -0
- package/src/camera/cameraDirector.js +157 -0
- package/src/camera/cameraGenerator.js +367 -0
- package/src/camera/cameraRig.js +570 -0
- package/src/camera/cameraSettings.js +236 -0
- package/src/camera/index.js +7 -0
- package/src/catalog/builtinEntries.js +245 -0
- package/src/catalog/catalog.js +210 -0
- package/src/catalog/index.js +3 -0
- package/src/catalog/manifest.js +84 -0
- package/src/core/generation.js +529 -0
- package/src/environment/environmentRigs.js +21 -1
- package/src/environment/environmentSettings.js +4 -0
- package/src/environment/environmentSunShadowPass.js +8 -0
- package/src/fauna/INTEGRATION.md +174 -0
- package/src/fauna/boids.js +861 -0
- package/src/fauna/faunaBodies.js +492 -0
- package/src/fauna/faunaPresets.js +52 -0
- package/src/fauna/faunaSettings.js +525 -0
- package/src/fauna/index.js +5 -0
- package/src/fauna/stylizedFauna.js +395 -0
- package/src/game-feel/gameFeelGenerator.js +402 -0
- package/src/game-feel/gameFeelRuntime.js +549 -0
- package/src/game-feel/index.js +2 -0
- package/src/index.js +17 -6
- package/src/lighting/colorIntensity.js +177 -0
- package/src/lighting/index.js +161 -0
- package/src/lighting/lightDescriptors.js +249 -0
- package/src/lighting/lightingCapabilities.js +79 -0
- package/src/lighting/lightingDocuments.js +247 -0
- package/src/lighting/lightingFixtures.js +446 -0
- package/src/lighting/lightingGenerator.js +449 -0
- package/src/lighting/lightingPresets.js +319 -0
- package/src/lighting/lightingRuntime.js +723 -0
- package/src/lighting/lightingStyle.js +386 -0
- package/src/lighting/lightingSystem.js +774 -0
- package/src/lighting/unrealExport.js +186 -0
- package/src/lighting/utils.js +87 -0
- package/src/motion/index.js +5 -0
- package/src/motion/motionClip.js +441 -0
- package/src/motion/motionController.js +628 -0
- package/src/motion/motionDocuments.js +225 -0
- package/src/motion/motionGraph.js +307 -0
- package/src/motion/motionSettings.js +222 -0
- package/src/pathgen/index.js +7 -0
- package/src/pathgen/pathBridge.js +232 -0
- package/src/pathgen/pathPresets.js +35 -0
- package/src/pathgen/pathRibbon.js +410 -0
- package/src/pathgen/pathRouter.js +380 -0
- package/src/pathgen/pathSettings.js +335 -0
- package/src/pathgen/pathTextures.js +123 -0
- package/src/pathgen/stylizedPaths.js +453 -0
- package/src/post/index.js +1 -0
- package/src/post/postGenerator.js +177 -0
- package/src/post/postProcessing.js +41 -0
- package/src/propgen/generatorsWave1.js +379 -0
- package/src/propgen/generatorsWave2.js +462 -0
- package/src/propgen/index.js +5 -0
- package/src/propgen/propAsset.js +323 -0
- package/src/propgen/propParts.js +170 -0
- package/src/propgen/propPlacement.js +459 -0
- package/src/propgen/propPresets.js +82 -0
- package/src/propgen/propSettings.js +395 -0
- package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
- package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
- package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
- package/src/shaders-tsl/chunks/water-waves.js +90 -11
- package/src/shaders-tsl/environment.js +15 -1
- package/src/shaders-tsl/flower.js +279 -30
- package/src/shaders-tsl/grass.js +60 -33
- package/src/shaders-tsl/sky.js +125 -31
- package/src/shaders-tsl/tree-leaf.js +61 -25
- package/src/shaders-tsl/water-breaker.js +7 -4
- package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
- package/src/shaders-tsl/water.js +439 -49
- package/src/shaders-tsl/woody-surface.js +154 -0
- package/src/sky/sceneOverrideLayers.js +10 -0
- package/src/sky/skyQuality.js +26 -0
- package/src/sky/stylizedSky.js +753 -45
- package/src/soundscape/index.js +4 -0
- package/src/soundscape/soundscapeGenerator.js +179 -0
- package/src/soundscape/soundscapeRuntime.js +806 -0
- package/src/soundscape/soundscapeSettings.js +292 -0
- package/src/styles/index.js +13 -0
- package/src/styles/styleBundle.js +325 -0
- package/src/stylizedTerrain.js +32 -2
- package/src/stylizedWorld.js +423 -20
- package/src/texgen/evaluateTexture.js +675 -0
- package/src/texgen/index.js +60 -0
- package/src/texgen/noise2.js +210 -0
- package/src/texgen/textureAi.js +436 -0
- package/src/texgen/textureGenerators.js +516 -0
- package/src/texgen/texturePresets.js +490 -0
- package/src/texgen/textureSettings.js +342 -0
- package/src/texgen/textureThree.js +59 -0
- package/src/vegetation/flowerSpecies.js +15 -3
- package/src/vegetation/grassPalettes.js +153 -0
- package/src/vegetation/index.js +6 -0
- package/src/vegetation/stylizedBush.js +2 -0
- package/src/vegetation/stylizedFlower.js +82 -0
- package/src/vegetation/stylizedFlowers.js +48 -7
- package/src/vegetation/stylizedForest.js +29 -1
- package/src/vegetation/stylizedGrass.js +291 -56
- package/src/vegetation/stylizedTree.js +38 -2
- package/src/vegetation/stylizedTreeFoliage.js +2 -1
- package/src/vegetation/vegetationShaders.js +1110 -0
- package/src/vfxgen/INTEGRATION.md +145 -0
- package/src/vfxgen/core/burstBackbone.js +380 -0
- package/src/vfxgen/core/projectileCore.js +92 -0
- package/src/vfxgen/core/spriteShapes.js +98 -0
- package/src/vfxgen/core/trailRibbon.js +272 -0
- package/src/vfxgen/core/vfxRandom.js +29 -0
- package/src/vfxgen/effects/emitHelpers.js +37 -0
- package/src/vfxgen/effects/magicEffects.js +162 -0
- package/src/vfxgen/effects/movementEffects.js +87 -0
- package/src/vfxgen/effects/weaponEffects.js +118 -0
- package/src/vfxgen/index.js +18 -0
- package/src/vfxgen/moves/moveController.js +146 -0
- package/src/vfxgen/moves/moveLibrary.js +335 -0
- package/src/vfxgen/vfxPresets.js +98 -0
- package/src/vfxgen/vfxSettings.js +384 -0
- package/src/vfxgen/vfxSystem.js +449 -0
- package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
- package/src/villagegen/index.js +4 -0
- package/src/villagegen/stylizedVillage.js +490 -0
- package/src/villagegen/villageArchetypes.js +160 -0
- package/src/villagegen/villageNames.js +40 -0
- package/src/villagegen/villageSites.js +105 -0
- package/src/water/sceneOverrideLayers.js +23 -0
- package/src/water/water.js +5 -0
- package/src/water/waterBreakerSystem.js +15 -1
- package/src/water/waterCurrentField.js +447 -0
- package/src/water/waterMaterial.js +12 -0
- package/src/water/waterNearshorePhase.js +320 -0
- package/src/water/waterScenePasses.js +83 -30
- package/src/water/waterSettings.js +325 -13
- package/src/water/waterShoreMaterial.js +322 -0
- package/src/water/waterShoreStateField.js +605 -0
- package/src/water/waterSurface.js +797 -28
- package/src/weather/index.js +6 -0
- package/src/weather/weatherPrecipitation.js +221 -0
- package/src/weather/weatherPresets.js +258 -0
- package/src/weather/weatherSettings.js +269 -0
- package/src/weather/weatherSystem.js +871 -0
- package/src/worldMinimap.js +62 -0
- package/src/worldPresets.js +4 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Texture Lab and texgen
|
|
2
|
+
|
|
3
|
+
Texture Lab (`/texture-lab/`) generates **seamless procedural PBR textures
|
|
4
|
+
for anything** — stone, ground, wood, metal, fabric, ceramics, creatures,
|
|
5
|
+
liquids, sci-fi panels, stylized prints — entirely on the CPU, deterministic
|
|
6
|
+
per seed, with zero image assets. The generator ships as the
|
|
7
|
+
`@call-me-sensei/toonlab/texgen` cluster so you can bake the same maps in
|
|
8
|
+
your own app or in Node.
|
|
9
|
+
|
|
10
|
+
## The lab
|
|
11
|
+
|
|
12
|
+
- **Gallery** — 60+ built-in material presets across ten categories, each
|
|
13
|
+
thumbnail baked live by the real generator. Your saved textures appear in
|
|
14
|
+
*Your library*.
|
|
15
|
+
- **Pattern stage** — the base generator (25 tileable patterns: fbm, ridged,
|
|
16
|
+
billow, turbulence, worley/voronoi variants, cracks, caustics, speckle,
|
|
17
|
+
bricks, tiles, hex, checker, grid, stripes, chevron, weave, basket weave,
|
|
18
|
+
scales, dots, marble, wood grain, …) plus two blendable detail layers.
|
|
19
|
+
Sliders irrelevant to the selected pattern hide automatically.
|
|
20
|
+
- **Color stage** — a five-stop height ramp (set *Band smoothness* to 0 for
|
|
21
|
+
hard cel bands), painterly hue/value jitter (optionally per pattern cell —
|
|
22
|
+
per-brick tint shifts), cavity darkening and ridge sheen for the
|
|
23
|
+
hand-painted read, and a final grade (hue/saturation/brightness/contrast/
|
|
24
|
+
gamma).
|
|
25
|
+
- **Overlays stage** — one-knob **wear macros** (*Damage* carves seeded
|
|
26
|
+
scratches and chips and roughens them; *Dirt* pools grime into crevices)
|
|
27
|
+
plus two masked colored overlays for moss, rust, grime, snow, patina,
|
|
28
|
+
stains. *Crevice bias* pools an overlay into recesses (+1) or caps ridges
|
|
29
|
+
(−1); each overlay can also shift roughness, height, and metalness (rust
|
|
30
|
+
strips metal).
|
|
31
|
+
- **Surface stage** — relief depth, derived normal strength, baked AO,
|
|
32
|
+
roughness base + height-correlated contrast, metalness, and an optional
|
|
33
|
+
emissive source (crevices / peaks / band / overlay) for lava, circuits,
|
|
34
|
+
force fields.
|
|
35
|
+
- **Preview** — lit 3D meshes (sphere, cube, cylinder, torus, knot, plane)
|
|
36
|
+
with live displacement, or a flat 2D sheet with a per-map view
|
|
37
|
+
(albedo/height/normal/roughness/metalness/AO/emissive) and 1–4× tiling to
|
|
38
|
+
eyeball the seams. `R` re-rolls the seed; keys 1–5 switch stages;
|
|
39
|
+
backtick opens the all-controls drawer.
|
|
40
|
+
|
|
41
|
+
## Image base (bring your own image)
|
|
42
|
+
|
|
43
|
+
“Use an image as the base” (Pattern stage, or *From an image* in the
|
|
44
|
+
gallery) turns **a picture of one surface into a tiling toon material** —
|
|
45
|
+
a wall photo, a bark close-up, a fabric scan, a crop from concept art.
|
|
46
|
+
There is **no AI in this path**: it converts, it does not interpret, so a
|
|
47
|
+
whole scene or screenshot just becomes repeating wallpaper (the lab warns
|
|
48
|
+
when an upload looks like a scene — crop the material you want first;
|
|
49
|
+
scene → material-list breakdown is the planned pro tool). Mechanically:
|
|
50
|
+
the bitmap is seamless-ized (torus blend), relief is derived from
|
|
51
|
+
band-split luminance (*Relief detail* / *Relief base*), and an optional
|
|
52
|
+
*Cel bands* control quantizes it toward the toon look. The image replaces
|
|
53
|
+
only the **base layer** — detail layers, wear, overlays (moss a
|
|
54
|
+
photographed wall!), glow, cavity/sheen, and the color grade all still
|
|
55
|
+
apply, and every map derives as usual. The base pattern and height-ramp
|
|
56
|
+
controls disable with a hint while an image is active. Images are stored
|
|
57
|
+
in the browser only; share URLs strip them (Recipe JSON keeps them).
|
|
58
|
+
Library callers: `imageToTextureMaps(imagePixels, { params, settings,
|
|
59
|
+
size })` — decoding stays outside so texgen remains headless.
|
|
60
|
+
|
|
61
|
+
## AI assist (bring your own key)
|
|
62
|
+
|
|
63
|
+
The **AI** stage maps plain language — *“old leather jacket”*, *“mossy
|
|
64
|
+
castle bricks”*, *“molten lava with glowing cracks”* — onto generator
|
|
65
|
+
parameters:
|
|
66
|
+
|
|
67
|
+
- **Built-in (no key)** — a deterministic offline mapper scores your words
|
|
68
|
+
against the preset library and applies wear/color modifiers (old, wet,
|
|
69
|
+
mossy, rusty, cracked, glowing, chunky, fine, color words…).
|
|
70
|
+
- **Gemini / OpenAI (your key)** — the lab sends a compact schema catalog
|
|
71
|
+
(generated from the live field schema, ~3k tokens) plus your description
|
|
72
|
+
to the model you name, and expects a small JSON recipe back: a base
|
|
73
|
+
preset, a 2–5 color palette, and a parameter patch. Cheap “mini” tiers
|
|
74
|
+
are plenty: the defaults are `gemini-2.5-flash-lite` and `gpt-5-mini`,
|
|
75
|
+
and the model id field is free text so any model you have access to
|
|
76
|
+
works. Every returned value is validated and clamped against the schema
|
|
77
|
+
before it touches your document.
|
|
78
|
+
|
|
79
|
+
Keys are stored in this browser's `localStorage` only and are sent solely
|
|
80
|
+
to `generativelanguage.googleapis.com` / `api.openai.com` — there is no
|
|
81
|
+
ToonLab server in the path. *Refine current* mode patches the texture you
|
|
82
|
+
are editing instead of starting fresh.
|
|
83
|
+
|
|
84
|
+
## Export
|
|
85
|
+
|
|
86
|
+
The Export dialog bakes at 256–2048 px (independent of the preview) and
|
|
87
|
+
downloads:
|
|
88
|
+
|
|
89
|
+
- individual PNGs per map,
|
|
90
|
+
- one ZIP with the selected maps plus `recipe.json` (re-importable) and
|
|
91
|
+
`material.json` (usage hints),
|
|
92
|
+
- the recipe JSON on its own, or a **share URL** with the whole recipe
|
|
93
|
+
inlined (`?textureRecipe=`).
|
|
94
|
+
|
|
95
|
+
Maps: `albedo` + `emissive` are sRGB; `normal` (OpenGL +Y), `roughness`,
|
|
96
|
+
`metalness`, `ao`, `height`, and the glTF-style `orm` pack
|
|
97
|
+
(R=occlusion, G=roughness, B=metalness) are linear. Everything tiles
|
|
98
|
+
seamlessly — noise wraps its lattice periodically rather than blending
|
|
99
|
+
mirrored copies.
|
|
100
|
+
|
|
101
|
+
## Library usage
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
import {
|
|
105
|
+
createTextureSettings,
|
|
106
|
+
evaluateTextureMaps,
|
|
107
|
+
syncTextureMapTextures,
|
|
108
|
+
findTexturePreset,
|
|
109
|
+
} from '@call-me-sensei/toonlab/texgen';
|
|
110
|
+
|
|
111
|
+
const settings = createTextureSettings(findTexturePreset('rusted-iron').settings);
|
|
112
|
+
const maps = await evaluateTextureMaps(settings, { size: 512 });
|
|
113
|
+
|
|
114
|
+
const { textures } = syncTextureMapTextures(maps); // THREE.DataTexture set
|
|
115
|
+
const material = new THREE.MeshStandardMaterial({
|
|
116
|
+
aoMap: textures.ao,
|
|
117
|
+
map: textures.albedo, // SRGBColorSpace, RepeatWrapping — pre-tagged
|
|
118
|
+
metalnessMap: textures.metalness,
|
|
119
|
+
metalness: 1,
|
|
120
|
+
normalMap: textures.normal,
|
|
121
|
+
roughnessMap: textures.roughness,
|
|
122
|
+
roughness: 1,
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`evaluateTextureMaps` is async and chunked (pass `onProgress` /
|
|
127
|
+
`shouldCancel`), headless-safe (no DOM), and deterministic: the same
|
|
128
|
+
settings always produce byte-identical maps. To map language to settings
|
|
129
|
+
yourself, use `keywordTextureRecipe(prompt)` (offline) or
|
|
130
|
+
`buildTextureAiPrompt` + `parseTextureAiResponse` +
|
|
131
|
+
`compileTextureAiRecipe` around any LLM call.
|
|
132
|
+
|
|
133
|
+
Settings documents are versioned (`kind: "toonlab.textureRecipe"`,
|
|
134
|
+
`version: 1`) and validated/clamped by `createTextureSettings` — unknown
|
|
135
|
+
keys are ignored, colors accept hex strings or `[r, g, b]` triplets.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Toon character shading
|
|
2
|
+
|
|
3
|
+
Modern anime character shading for any Three.js character. One call
|
|
4
|
+
converts a loaded model's materials into the toon shader; everything else is
|
|
5
|
+
settings.
|
|
6
|
+
|
|
7
|
+
The implementation is TSL/NodeMaterial-only after the WebGPU cutover. It runs
|
|
8
|
+
on native WebGPU by default and on the TSL WebGL2 fallback with
|
|
9
|
+
`?renderer=webgl`; the old GLSL material path has been removed.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { applyToonShader, createToonSettings } from '@call-me-sensei/toonlab/toon';
|
|
13
|
+
|
|
14
|
+
applyToonShader(characterRoot, {
|
|
15
|
+
settings: createToonSettings({
|
|
16
|
+
preset: 'default',
|
|
17
|
+
skinTone: { skinShadowBrightness: 0.94 },
|
|
18
|
+
rimLight: { hairIntensity: 0.2 },
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
(Inside this repo the labs import from `../../src/toon/...`.)
|
|
24
|
+
|
|
25
|
+
`applyToonShader(root, options)` traverses the model, classifies material
|
|
26
|
+
roles, converts materials in place, and adds the inverted-hull outline pass.
|
|
27
|
+
Settings groups can also be passed directly as options
|
|
28
|
+
(`applyToonShader(root, { rimLight: {...} })`). To re-tune an already
|
|
29
|
+
converted model at runtime, use
|
|
30
|
+
`applyToonSettingsToMaterial(target, settings)` — it applies uniform-safe
|
|
31
|
+
edits without reconversion (this is what the debug panel calls).
|
|
32
|
+
|
|
33
|
+
## Settings groups
|
|
34
|
+
|
|
35
|
+
`createToonSettings(options)` merges your overrides over a preset over the
|
|
36
|
+
defaults, and returns the full nested settings object. There are 23 groups
|
|
37
|
+
(`TOON_SETTING_GROUPS`), each with its own module under `src/toon/settings/`:
|
|
38
|
+
|
|
39
|
+
| Group | What it controls |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `baseTexture` | Preserves source texture, source material color, and saturation policy before toon lighting. |
|
|
42
|
+
| `materialRoles` | Classifies materials as skin, face, hair, eyes, costume, metal, transparent overlays, and outline. |
|
|
43
|
+
| `alpha` | Cutout, blend, opacity, eye overlay sorting, and transparent decoration behavior. |
|
|
44
|
+
| `skinTone` | Keeps skin and face shadows warm, readable, and separate from costume/hair shadows. |
|
|
45
|
+
| `faceLighting` | Overrides face-area cel response so noses, cheeks, and eyes do not receive harsh body shadows. |
|
|
46
|
+
| `celShade` | The primary directional cel band threshold, softness, and light-ignore amount. |
|
|
47
|
+
| `shadowColor` | Tints and reshapes lit-to-shadow transitions and fully shadowed regions. |
|
|
48
|
+
| `sceneShadow` | How renderer shadow maps darken character materials. |
|
|
49
|
+
| `selfShadow` | Character-local self shadow (dedicated shadow pass or scene-proxy source). |
|
|
50
|
+
| `averageShadow` | Averaged shadow visibility for softer role-specific shadow damping. |
|
|
51
|
+
| `indirectLight` | Mixes ambient, hemisphere, and environment light into toon shading. |
|
|
52
|
+
| `localLights` | Point and spot light response without overpowering cel bands. |
|
|
53
|
+
| `rimLight` | View-dependent edge light; classic fresnel or screen-space depth-texture mode. |
|
|
54
|
+
| `contactShadow` | Thin screen-space contact shadows (hair-on-face, arm-on-torso) from the depth prepass. |
|
|
55
|
+
| `specular` | Role-aware stylized highlights and optional source specular masks. |
|
|
56
|
+
| `hairHighlight` | Hair-specific highlight bands, optional anisotropic strand response, and source masks. |
|
|
57
|
+
| `eyeHighlight` | Role-aware eye/catchlight boosts and optional source masks. |
|
|
58
|
+
| `materialMaps` | Routes source normal, AO, emissive, MatCap, ramp, detail, roughness, metalness, and specular-color maps. |
|
|
59
|
+
| `outline` | The inverted-hull outline pass, including role-specific widths and colors. |
|
|
60
|
+
| `glitter` | Procedural view-dependent sparkles for costumes/accessories. Off by default. |
|
|
61
|
+
| `sticker` | Blends a decal/overlay texture into the albedo before lighting. Off by default. |
|
|
62
|
+
| `perspectiveRemoval` | Flattens perspective around the tracked head for anime-portrait closeups. Off by default. |
|
|
63
|
+
| `fur` | Opt-in shell fur for matched materials (collars, trims, animal parts). Off by default. |
|
|
64
|
+
|
|
65
|
+
Every field (298 of them) is listed with type, default, and range in the
|
|
66
|
+
generated [settings reference](settings-reference.md). The same schema
|
|
67
|
+
(`TOON_SETTING_FIELD_SCHEMA`) drives the [debug panel](debug-panel.md), so
|
|
68
|
+
each field is also a live slider in the Character Shader Lab.
|
|
69
|
+
|
|
70
|
+
## Material roles
|
|
71
|
+
|
|
72
|
+
Materials are classified into roles before conversion — `default`,
|
|
73
|
+
`costume`, `skin`, `face`, `hair`, `eye`, `iris`, `pupil`, `sclera`,
|
|
74
|
+
`eyeHighlight`, `catchlight`, `blush`, `transparentOverlay`, `metal`,
|
|
75
|
+
`outline`. Roles decide which settings apply where (skin shadow tint, hair
|
|
76
|
+
highlights, outline widths, alpha behavior).
|
|
77
|
+
|
|
78
|
+
The default classifier is heuristic (names, textures, PMX conventions).
|
|
79
|
+
Override it without touching shader code:
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
applyToonShader(characterRoot, {
|
|
83
|
+
materialRoles: {
|
|
84
|
+
byName: { Face: 'face', Hair: 'hair' },
|
|
85
|
+
byUuid: { [material.uuid]: 'skin' },
|
|
86
|
+
patterns: [{ pattern: /eye.*highlight/i, role: 'eyeHighlight' }],
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
A source material can also declare `material.userData.toonRole`. Inspect the
|
|
92
|
+
resolved roles with `?toonDebug=role`, or programmatically via
|
|
93
|
+
`document.body.dataset.materialRoleSummary` in the labs.
|
|
94
|
+
|
|
95
|
+
## Presets and preset documents
|
|
96
|
+
|
|
97
|
+
Presets are named partial settings registered in a registry. Built-ins:
|
|
98
|
+
`default`, `call_me_sensei`, `showcase` (`TOON_PRESET_IDS`,
|
|
99
|
+
`getToonPresetOptions()`).
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
import {
|
|
103
|
+
registerToonPreset,
|
|
104
|
+
serializeToonPreset,
|
|
105
|
+
parseToonPresetDocument,
|
|
106
|
+
} from '@call-me-sensei/toonlab/toon';
|
|
107
|
+
|
|
108
|
+
// Register in code:
|
|
109
|
+
registerToonPreset('zzz_soft', {
|
|
110
|
+
label: 'ZZZ Soft',
|
|
111
|
+
description: 'Flatter urban anime lighting.',
|
|
112
|
+
settings: { hairHighlight: { mode: 'anisotropic' } },
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Share as a versioned JSON document (same shape the Character Shader Lab exports):
|
|
116
|
+
const json = serializeToonPreset('warm_skin_test', {
|
|
117
|
+
label: 'Warm Skin Test',
|
|
118
|
+
settings: { skinTone: { skinShadowBrightness: 0.94 } },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Load one back, with validation:
|
|
122
|
+
const result = parseToonPresetDocument(json);
|
|
123
|
+
if (result.ok) registerToonPreset(result.value.id, result.value, { overwrite: true });
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Documents carry `{ type: 'toonlab/toon-preset', version, id,
|
|
127
|
+
label, description, settings }` and are validated field-by-field against the
|
|
128
|
+
schema (`validateToonPresetDocument`). Scalars, booleans, enums, colors, and
|
|
129
|
+
vectors serialize; runtime texture objects (mask maps) are intentionally
|
|
130
|
+
ignored by JSON validation — wire those in code. Related APIs:
|
|
131
|
+
`createToonPresetDocument`, `registerSerializedToonPreset`,
|
|
132
|
+
`sanitizeToonPresetSettings`, `getToonPresetDefinition`.
|
|
133
|
+
|
|
134
|
+
## Render passes
|
|
135
|
+
|
|
136
|
+
`applyToonShader` alone produces a complete material.
|
|
137
|
+
`createCharacterRenderPasses` adds the per-frame passes that unlock the
|
|
138
|
+
screen-space and shadow-map features:
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
import { createCharacterRenderPasses } from '@call-me-sensei/toonlab/toon';
|
|
142
|
+
|
|
143
|
+
const passes = createCharacterRenderPasses({ renderer, scene, camera });
|
|
144
|
+
passes.registerCharacterRoot(modelRoot); // after applyToonShader
|
|
145
|
+
|
|
146
|
+
// in the render loop, before rendering:
|
|
147
|
+
passes.update();
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The passes:
|
|
151
|
+
|
|
152
|
+
1. **Scene depth prepass** — feeds the depth-texture rim light mode and
|
|
153
|
+
contact shadows.
|
|
154
|
+
2. **Character-only orthographic shadow map** — real self shadow
|
|
155
|
+
(`selfShadow` group; direction follows the main light or a
|
|
156
|
+
camera-relative art-directed angle).
|
|
157
|
+
3. **Head bone tracking** — head-space face shading
|
|
158
|
+
(`faceLighting.headSpaceMode: 'headBone'`).
|
|
159
|
+
4. **Average shadow measurement** — per-character uniform scene shadow.
|
|
160
|
+
5. **Character mask** — character-aware bloom in the
|
|
161
|
+
[post pipeline](post-processing.md).
|
|
162
|
+
|
|
163
|
+
Every pass is auto-gated: it only renders when at least one registered
|
|
164
|
+
material actually consumes its output, so leaving the passes running with
|
|
165
|
+
the features disabled costs almost nothing.
|
|
166
|
+
|
|
167
|
+
## Debug views
|
|
168
|
+
|
|
169
|
+
`?toonDebug=<mode>` in the labs, or `setToonDebugOutput(root, mode)` in
|
|
170
|
+
code, renders one shader term in isolation:
|
|
171
|
+
|
|
172
|
+
```text
|
|
173
|
+
sourceAlbedo | albedo | band | shadow | selfShadow | directVisibility |
|
|
174
|
+
contactShadow | rim | depthRim | specular | hairHighlight | eyeHighlight |
|
|
175
|
+
normalMap | aoMap | emissiveMap | matcap | ramp | detailMap | roughnessMap |
|
|
176
|
+
metalnessMap | shadowColor | lit | role | alpha
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Typical tuning loop: `?toonDebug=band` while adjusting `celShade`,
|
|
180
|
+
`?toonDebug=shadowColor` for `shadowColor`, `?toonDebug=rim` for `rimLight`,
|
|
181
|
+
`?toonDebug=role` when a model misclassifies. The full mode map is
|
|
182
|
+
`TOON_DEBUG_OUTPUT_MODES` (aliases included); debug branches compile out
|
|
183
|
+
when off.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# TSL Porting Conventions (three r185+)
|
|
2
|
+
|
|
3
|
+
House rules for porting the GLSL shader library to TSL/NodeMaterial during
|
|
4
|
+
the WebGPU migration. Everything here was
|
|
5
|
+
verified against `three@0.185.1` on both node backends — the WGSL
|
|
6
|
+
builder (WebGPU) and the GLSL builder (`forceWebGL: true`). Re-validate the
|
|
7
|
+
"r185 quirk" items on any version bump.
|
|
8
|
+
|
|
9
|
+
## File layout
|
|
10
|
+
|
|
11
|
+
- `src/shaders-tsl/` is the canonical shader library: one module per material
|
|
12
|
+
(`sky.js`, `anime.js`, …), one module per reusable chunk under
|
|
13
|
+
`src/shaders-tsl/chunks/` (`water-common.js`, `character-lighting.js`, …).
|
|
14
|
+
The retired raw GLSL tree (`src/shaders/`) and `shaderSource.js` registry
|
|
15
|
+
were removed in Phase 11.
|
|
16
|
+
- Leaf helpers that map nodes→nodes are `Fn()` exports. Helpers with
|
|
17
|
+
compile-time-constant parameters (loop counts, feature flags) are plain JS
|
|
18
|
+
functions that unroll at graph-build time.
|
|
19
|
+
- Chunks that need material uniforms export a factory
|
|
20
|
+
(`createXxxChunk({ u, tex, v, flags })`) returning the chunk's functions;
|
|
21
|
+
the shader module assembles them. `u` = uniform nodes under GLSL names,
|
|
22
|
+
`tex` = texture nodes, `v` = varyings, `flags` = compile-time booleans.
|
|
23
|
+
|
|
24
|
+
## The `.uniforms` compatibility surface
|
|
25
|
+
|
|
26
|
+
TSL material factories attach `material.uniforms` — a map of UniformNodes
|
|
27
|
+
(and TextureNodes) keyed by the **exact GLSL uniform names**. UniformNode and
|
|
28
|
+
TextureNode both expose `.value`, so every existing write-through
|
|
29
|
+
(`mat.uniforms.celShadeMidPoint.value = x`, HUD panels, characterRenderPasses
|
|
30
|
+
`setUniform`) works identically on both backends. Do not rename uniforms in
|
|
31
|
+
a port.
|
|
32
|
+
|
|
33
|
+
## Feature gating
|
|
34
|
+
|
|
35
|
+
- Former GLSL `#ifdef USE_X` sampler-presence gates are now JS
|
|
36
|
+
`if (flags.hasX)` graph-build gates. Absent maps never enter the graph, so
|
|
37
|
+
no texture/bind slots are wasted. Keep these gates: both native WebGPU (with
|
|
38
|
+
default requested limits) and the forced WebGL2 fallback currently have a
|
|
39
|
+
16-sampled-texture ceiling.
|
|
40
|
+
- GLSL `uniform bool useX` runtime toggles → keep as uniform-driven
|
|
41
|
+
`If()`/`select()`, exactly like the GLSL branch.
|
|
42
|
+
- Debug views are always compiled into TSL materials when available; the
|
|
43
|
+
selector is a pure uniform write.
|
|
44
|
+
|
|
45
|
+
## r185 gotchas (each cost real debugging time)
|
|
46
|
+
|
|
47
|
+
1. **`matN()` scalar constructors are three.js ROW-major** (`Matrix2`
|
|
48
|
+
constructor docs), the transpose of GLSL's column-major `matN()`.
|
|
49
|
+
Transpose the scalar order when porting, or write the multiply
|
|
50
|
+
component-wise. Constructors from column *vectors* (`mat4(v0,v1,v2,v3)`)
|
|
51
|
+
match GLSL. Symptom: fbm/rotation patterns differ subtly.
|
|
52
|
+
2. **`select()` operands must be pure expressions.** A branch that creates a
|
|
53
|
+
var (`.toVar()`) or calls an `Fn()` crashes the GLSL builder with
|
|
54
|
+
`Cannot read properties of undefined (reading 'addToStack')` — the
|
|
55
|
+
ConditionalNode type-resolution fallback builds the operand subtree in a
|
|
56
|
+
detached (stack-less) flow. Referencing vars created *outside* the select
|
|
57
|
+
is fine. Hoist calls into `If()`-assigned vars instead.
|
|
58
|
+
3. **Deep nested `select()` chains** (the 24-entry debug table) hit the same
|
|
59
|
+
fallback. Use masked arithmetic sums
|
|
60
|
+
(`Σ value.mul(select(cond, 1, 0))`) for big tables, and never leave a
|
|
61
|
+
ConditionalNode as the `fragmentNode` root (wrap with `mix()` or `vec4()`).
|
|
62
|
+
4. **All mutating node code needs an active stack** — build vertex work
|
|
63
|
+
inside one `Fn()` assigned to `vertexNode`, declare varyings up front
|
|
64
|
+
(`varying(vec3(), 'vName')`) and `.assign()` them inside; the declarative
|
|
65
|
+
`varying(Fn(...)())` form breaks the GLSL builder.
|
|
66
|
+
5. **Depth-texture sampling types differently per builder** (WGSL: `float`,
|
|
67
|
+
GLSL: `vec4` snippet under a `float` node type → compile error). Passes
|
|
68
|
+
that feed shaders write linear window depth into **float COLOR targets**
|
|
69
|
+
instead; `[0,1]` window depth is numerically identical on both coordinate
|
|
70
|
+
systems (perspective *and* orthographic), so `perspectiveDepthToViewZ`
|
|
71
|
+
works unchanged.
|
|
72
|
+
6. **Render targets are written top-down on BOTH node backends.** Manual
|
|
73
|
+
shadow/projective sampling (matrix-composed uv) needs a y-flip that the
|
|
74
|
+
classic pipeline didn't; WebGPU additionally has clip z in [0,1] (the GLSL
|
|
75
|
+
`*0.5+0.5` contract needs a `z' = 2z − 1` pre-stretch). Compose both into
|
|
76
|
+
the CPU-side matrix (see characterRenderPasses `shadowClipAdjust*`).
|
|
77
|
+
three's own `screenUV` node already handles orientation — prefer it over
|
|
78
|
+
`gl_FragCoord`-style math.
|
|
79
|
+
7. **SkinningNode stores bone matrices in a uniform buffer** —
|
|
80
|
+
`GL_MAX_UNIFORM_BLOCK_SIZE` (16KB ≈ 256 bones) breaks MMD-scale skeletons
|
|
81
|
+
on the WebGL2 backend. `withToonStorageSkinning()`
|
|
82
|
+
(chunks/character-skinning.js) reroutes skinned meshes to a
|
|
83
|
+
`storage(...).setPBO(true)` bone buffer (emitted as a DataTexture +
|
|
84
|
+
texelFetch on GLSL). Note: the GLSL builder **replaces the attribute
|
|
85
|
+
array** with a padded copy at PBO setup — per-frame updates must copy
|
|
86
|
+
`skeleton.boneMatrices` into `attribute.array` and flag
|
|
87
|
+
`attribute.pbo.needsUpdate`.
|
|
88
|
+
8. **Guard uniform-dependent divisions.** GLSL branches skipped
|
|
89
|
+
`direction.xz / (up + 0.28)` below the horizon; a masked straight-line
|
|
90
|
+
port produces NaN·0 = NaN. Keep the `If()` structure (uniform conditions
|
|
91
|
+
are fine in WGSL uniformity analysis) or clamp the divisor.
|
|
92
|
+
9. **RT-fed textures sample at `.level(0)`** — no mips exist and WGSL forbids
|
|
93
|
+
implicit-derivative sampling in non-uniform control flow.
|
|
94
|
+
10. **`MeshDepthMaterial` (and other non-node classics) don't auto-convert**
|
|
95
|
+
on the node renderer (`NodeBuilder: Material "MeshDepthMaterial" is not
|
|
96
|
+
compatible`). Standard lit materials do. Pass materials are node-built.
|
|
97
|
+
11. **`renderer.capabilities` doesn't exist on WebGPURenderer** — use
|
|
98
|
+
`renderer.getMaxAnisotropy()` etc. with optional chaining at seams shared
|
|
99
|
+
by both renderers.
|
|
100
|
+
12. **KTX2 `detectSupport(renderer)` needs an initialized backend** — gate
|
|
101
|
+
loader configuration (and anything else probing capabilities) on
|
|
102
|
+
`await renderer.init()` / `whenRendererReady()`.
|
|
103
|
+
13. **`UniformArrayNode.value` is the packed GPU buffer** (null until first
|
|
104
|
+
setup); the authoring array is `.array`. Expose array uniforms on the
|
|
105
|
+
`.uniforms` surface as `{ value: node.array, node }` wrappers so
|
|
106
|
+
`uniforms.uWavesA.value[i].set()` write-throughs keep working (water).
|
|
107
|
+
14. **Depth-as-color passes get fogged by `NodeMaterial.setupOutput`** in
|
|
108
|
+
fogged scenes — distant depth blends toward the fog color. Null
|
|
109
|
+
`scene.fog` for the pass render and force `fog: false` on the variant
|
|
110
|
+
materials (water depth pass; the environment sun-shadow pass has the
|
|
111
|
+
same latent hazard in any future fogged scene).
|
|
112
|
+
15. **The node renderer force-rebuilds a camera's projection on
|
|
113
|
+
`coordinateSystem` mismatch** (and on its first reversed-depth pass) —
|
|
114
|
+
copy `camera.coordinateSystem` onto any manual virtual/pass camera or a
|
|
115
|
+
hand-mutated projection matrix (oblique clip) gets wiped on first render
|
|
116
|
+
(planar reflection, water reflection).
|
|
117
|
+
16. **`geometry.setDrawRange` doesn't limit instanced draws** on the node
|
|
118
|
+
renderer — set `geometry.instanceCount` instead (rain intensity).
|
|
119
|
+
17. **Shared uniform NODES must be adopted at graph build time** — swapping
|
|
120
|
+
entries in a material's `.uniforms` map after build changes nothing (the
|
|
121
|
+
map is a compatibility view, not the graph). Rebuild the material (or
|
|
122
|
+
build it with the shared nodes from the start) when wiring cross-material
|
|
123
|
+
shared uniforms (`attachWaveUniforms`).
|
|
124
|
+
18. **The classic pipeline's baked `colorspace_fragment` is per-render-target**
|
|
125
|
+
— `linearToOutputTexel` compiles against the BOUND target's
|
|
126
|
+
`texture.colorSpace`, so an offscreen NoColorSpace target holds LINEAR
|
|
127
|
+
color on classic despite the include. When porting a pass that reads such
|
|
128
|
+
a target, do NOT add a compensating transfer (post composite: a drafted
|
|
129
|
+
OETF-at-read washed the frame +191/255). The single encode happens at the
|
|
130
|
+
canvas draw on both pipelines.
|
|
131
|
+
|
|
132
|
+
## Scene lights
|
|
133
|
+
|
|
134
|
+
Custom lighting models that need raw light data (main-light direction, toon
|
|
135
|
+
banding per light, shadow mask separate from color) don't fit the node
|
|
136
|
+
LightingModel shape. `chunks/character-scene-lights.js` mirrors the scene's
|
|
137
|
+
lights into shared uniforms once per frame (from `Object3D.onBeforeRender`,
|
|
138
|
+
which both renderers call) replicating three's WebGLLights view-space
|
|
139
|
+
conventions and attenuation math. Materials share the module-level uniform
|
|
140
|
+
nodes, so one sync updates every toon material.
|
|
141
|
+
|
|
142
|
+
## Renderer/backends
|
|
143
|
+
|
|
144
|
+
- `?renderer=` flag: absent / `webgpu` = native WebGPU; `webgl` = TSL through
|
|
145
|
+
`WebGPURenderer({ forceWebGL: true })`; `webgpu-forced-gl` = compatibility
|
|
146
|
+
alias for `webgl`.
|
|
147
|
+
`labs/shared/rendererKind.js` resolves it; `labs/shared/rendererFactory.js`
|
|
148
|
+
creates the renderer (sync create, async `init()` gate via
|
|
149
|
+
`whenRendererReady`), sets the `src/core/shaderBackend.js` marker, and
|
|
150
|
+
reports `document.body.dataset.rendererKind` / `.rendererBackend`
|
|
151
|
+
(`webgpu` | `webgl2-fallback` — capture scripts assert this).
|
|
152
|
+
- Material factories in `src/` are TSL-first. Some helpers still branch on
|
|
153
|
+
`isTslBackend()` for shared WebGPU/forced-WebGL2 code, but the classic
|
|
154
|
+
`WebGLRenderer`/GLSL material path is gone.
|
|
155
|
+
- Headless WebGPU (Playwright): launch with
|
|
156
|
+
`--enable-unsafe-webgpu --enable-gpu` → hardware Metal adapter on macOS.
|
|
157
|
+
Captures: `TOON_BASELINE_RENDERER=webgpu|webgl|webgpu-forced-gl
|
|
158
|
+
TOON_BASELINE_SCOPE=ganyu npm run baseline:capture` (backend mismatch fails
|
|
159
|
+
the capture).
|
|
160
|
+
|
|
161
|
+
## Verification
|
|
162
|
+
|
|
163
|
+
WebGL headless captures are byte-deterministic (noise floor zero) — any
|
|
164
|
+
classic-path diff is a real regression. TSL-vs-WebGL diffs bottom out at
|
|
165
|
+
fp-level noise around toon band/fbm thresholds (mean ≈ 1/1020 per pixel);
|
|
166
|
+
structural differences always traced to a real porting bug, so do not accept
|
|
167
|
+
"looks close" while a debug view disagrees.
|