@call-me-sensei/toonlab 0.4.24 → 0.5.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.
Files changed (146) hide show
  1. package/NPM-LIBRARY.md +7 -6
  2. package/README.md +4 -4
  3. package/agents/PROMPTS.md +1 -1
  4. package/agents/skills/claude/toon-shading/SKILL.md +1 -1
  5. package/agents/skills/codex/toon-shading/SKILL.md +1 -1
  6. package/package.json +4 -2
  7. package/src/character/modelLoader.js +42 -1
  8. package/src/core/materialRoles.js +40 -5
  9. package/src/environment/environmentMaterialAdapter.js +3 -3
  10. package/src/post/postProcessing.js +12 -0
  11. package/src/shaders-tsl/character/castShadows.js +147 -0
  12. package/src/shaders-tsl/character/characterMaterial.js +764 -0
  13. package/src/shaders-tsl/character/characterState.js +90 -0
  14. package/src/shaders-tsl/character/debugViews.js +34 -0
  15. package/src/shaders-tsl/character/displayColor.js +71 -0
  16. package/src/shaders-tsl/character/highlightsRim.js +123 -0
  17. package/src/shaders-tsl/character/paintedDetails.js +86 -0
  18. package/src/shaders-tsl/character/sceneLights.js +229 -0
  19. package/src/shaders-tsl/character/surfaceLight.js +148 -0
  20. package/src/shaders-tsl/chunks/character-roles.js +10 -3
  21. package/src/shaders-tsl/chunks/environment-lighting.js +2 -2
  22. package/src/shaders-tsl/chunks/pass-depth-color.js +11 -2
  23. package/src/shaders-tsl/environment.js +2 -2
  24. package/src/shaders-tsl/flower.js +14 -2
  25. package/src/shaders-tsl/post-composite.js +14 -4
  26. package/src/styles/neutralStylePresets.js +12 -42
  27. package/src/toon/alphaCoverage.js +82 -0
  28. package/src/toon/autoRoles.js +589 -0
  29. package/src/toon/bakeAttribute.js +86 -0
  30. package/src/toon/characterParameters.js +269 -0
  31. package/src/toon/characterRenderPasses.js +430 -549
  32. package/src/toon/faceShadowBake.js +547 -0
  33. package/src/toon/featureBakes.js +353 -0
  34. package/src/toon/headBone.js +139 -0
  35. package/src/toon/occlusionBake.js +236 -0
  36. package/src/toon/settings/alphaSettings.js +68 -7
  37. package/src/toon/settings/autoRolesSettings.js +24 -0
  38. package/src/toon/settings/baseTextureSettings.js +21 -3
  39. package/src/toon/settings/faceSettings.js +56 -0
  40. package/src/toon/settings/fieldSchema.js +324 -0
  41. package/src/toon/settings/furSettings.js +1 -1
  42. package/src/toon/settings/highlightSettings.js +67 -0
  43. package/src/toon/settings/lightSettings.js +44 -0
  44. package/src/toon/settings/mapsSettings.js +132 -0
  45. package/src/toon/settings/outlineSettings.js +61 -254
  46. package/src/toon/settings/rampSettings.js +38 -0
  47. package/src/toon/settings/rimSettings.js +46 -0
  48. package/src/toon/settings/shadingSettings.js +42 -0
  49. package/src/toon/settings/shadowSettings.js +54 -0
  50. package/src/toon/skinEvidence.js +102 -0
  51. package/src/toon/sourceShading.js +176 -0
  52. package/src/toon/texturePixels.js +196 -0
  53. package/src/toon/toonMaterialAdapter.js +992 -1697
  54. package/src/toon/toonSettings.js +345 -923
  55. package/src/version.js +1 -1
  56. package/types/cloud/noise/weatherMap.d.ts +1 -1
  57. package/types/index.d.ts +18 -61
  58. package/types/post/postGenerator.d.ts +1 -0
  59. package/types/shaders-tsl/character/castShadows.d.ts +29 -0
  60. package/types/shaders-tsl/character/characterMaterial.d.ts +61 -0
  61. package/types/shaders-tsl/character/characterState.d.ts +27 -0
  62. package/types/shaders-tsl/character/debugViews.d.ts +6 -0
  63. package/types/shaders-tsl/character/displayColor.d.ts +13 -0
  64. package/types/shaders-tsl/character/highlightsRim.d.ts +43 -0
  65. package/types/shaders-tsl/character/paintedDetails.d.ts +20 -0
  66. package/types/shaders-tsl/character/sceneLights.d.ts +67 -0
  67. package/types/shaders-tsl/character/surfaceLight.d.ts +28 -0
  68. package/types/shaders-tsl/chunks/character-roles.d.ts +3 -2
  69. package/types/shaders-tsl/chunks/pass-depth-color.d.ts +3 -1
  70. package/types/shaders-tsl/post-composite.d.ts +0 -4
  71. package/types/styles/neutralStylePresets.d.ts +22 -415
  72. package/types/toon/alphaCoverage.d.ts +28 -0
  73. package/types/toon/autoRoles.d.ts +60 -0
  74. package/types/toon/bakeAttribute.d.ts +18 -0
  75. package/types/toon/characterParameters.d.ts +103 -0
  76. package/types/toon/characterRenderPasses.d.ts +45 -3
  77. package/types/toon/faceShadowBake.d.ts +31 -0
  78. package/types/toon/featureBakes.d.ts +20 -0
  79. package/types/toon/headBone.d.ts +34 -0
  80. package/types/toon/occlusionBake.d.ts +18 -0
  81. package/types/toon/settings/alphaSettings.d.ts +29 -3
  82. package/types/toon/settings/autoRolesSettings.d.ts +12 -0
  83. package/types/toon/settings/faceSettings.d.ts +43 -0
  84. package/types/toon/settings/fieldSchema.d.ts +101 -0
  85. package/types/toon/settings/highlightSettings.d.ts +77 -0
  86. package/types/toon/settings/lightSettings.d.ts +30 -0
  87. package/types/toon/settings/mapsSettings.d.ts +62 -0
  88. package/types/toon/settings/outlineSettings.d.ts +70 -124
  89. package/types/toon/settings/rampSettings.d.ts +26 -0
  90. package/types/toon/settings/rimSettings.d.ts +34 -0
  91. package/types/toon/settings/shadingSettings.d.ts +30 -0
  92. package/types/toon/settings/shadowSettings.d.ts +37 -0
  93. package/types/toon/skinEvidence.d.ts +14 -0
  94. package/types/toon/sourceShading.d.ts +36 -0
  95. package/types/toon/texturePixels.d.ts +25 -0
  96. package/types/toon/toonMaterialAdapter.d.ts +103 -855
  97. package/types/toon/toonSettings.d.ts +193 -1101
  98. package/types/version.d.ts +1 -1
  99. package/src/shaders-tsl/anime.js +0 -970
  100. package/src/shaders-tsl/chunks/character-color.js +0 -64
  101. package/src/shaders-tsl/chunks/character-highlights.js +0 -174
  102. package/src/shaders-tsl/chunks/character-lighting.js +0 -309
  103. package/src/shaders-tsl/chunks/character-material-maps.js +0 -146
  104. package/src/shaders-tsl/chunks/character-scene-lights.js +0 -270
  105. package/src/shaders-tsl/chunks/character-shadow-color.js +0 -61
  106. package/src/toon/settings/averageShadowSettings.js +0 -181
  107. package/src/toon/settings/celShadeSettings.js +0 -96
  108. package/src/toon/settings/contactShadowSettings.js +0 -114
  109. package/src/toon/settings/eyeHighlightSettings.js +0 -187
  110. package/src/toon/settings/faceLightingSettings.js +0 -135
  111. package/src/toon/settings/glitterSettings.js +0 -102
  112. package/src/toon/settings/hairHighlightSettings.js +0 -325
  113. package/src/toon/settings/indirectLightSettings.js +0 -244
  114. package/src/toon/settings/localLightSettings.js +0 -176
  115. package/src/toon/settings/materialMapSettings.js +0 -299
  116. package/src/toon/settings/perspectiveRemovalSettings.js +0 -60
  117. package/src/toon/settings/rimLightSettings.js +0 -268
  118. package/src/toon/settings/sceneShadowSettings.js +0 -147
  119. package/src/toon/settings/selfShadowSettings.js +0 -221
  120. package/src/toon/settings/shadowColorSettings.js +0 -238
  121. package/src/toon/settings/skinToneSettings.js +0 -150
  122. package/src/toon/settings/specularSettings.js +0 -314
  123. package/types/shaders-tsl/anime.d.ts +0 -9
  124. package/types/shaders-tsl/chunks/character-color.d.ts +0 -6
  125. package/types/shaders-tsl/chunks/character-highlights.d.ts +0 -12
  126. package/types/shaders-tsl/chunks/character-lighting.d.ts +0 -20
  127. package/types/shaders-tsl/chunks/character-material-maps.d.ts +0 -18
  128. package/types/shaders-tsl/chunks/character-scene-lights.d.ts +0 -48
  129. package/types/shaders-tsl/chunks/character-shadow-color.d.ts +0 -5
  130. package/types/toon/settings/averageShadowSettings.d.ts +0 -39
  131. package/types/toon/settings/celShadeSettings.d.ts +0 -37
  132. package/types/toon/settings/contactShadowSettings.d.ts +0 -30
  133. package/types/toon/settings/eyeHighlightSettings.d.ts +0 -55
  134. package/types/toon/settings/faceLightingSettings.d.ts +0 -32
  135. package/types/toon/settings/glitterSettings.d.ts +0 -43
  136. package/types/toon/settings/hairHighlightSettings.d.ts +0 -89
  137. package/types/toon/settings/indirectLightSettings.d.ts +0 -43
  138. package/types/toon/settings/localLightSettings.d.ts +0 -46
  139. package/types/toon/settings/materialMapSettings.d.ts +0 -86
  140. package/types/toon/settings/perspectiveRemovalSettings.d.ts +0 -14
  141. package/types/toon/settings/rimLightSettings.d.ts +0 -79
  142. package/types/toon/settings/sceneShadowSettings.d.ts +0 -32
  143. package/types/toon/settings/selfShadowSettings.d.ts +0 -44
  144. package/types/toon/settings/shadowColorSettings.d.ts +0 -77
  145. package/types/toon/settings/skinToneSettings.d.ts +0 -30
  146. package/types/toon/settings/specularSettings.d.ts +0 -96
package/NPM-LIBRARY.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # ToonLab npm library guide
2
2
 
3
3
  This guide is included in `@call-me-sensei/toonlab`. It is the package-first
4
- reference for version 0.4.24 and does not require access to the source
4
+ reference for version 0.5.0 and does not require access to the source
5
5
  repository.
6
6
 
7
7
  ## Install
@@ -89,9 +89,10 @@ import {
89
89
  } from '@call-me-sensei/toonlab/toon';
90
90
 
91
91
  const settings = createToonSettings({
92
- rimLight: { intensity: 0.35, mode: 'depth' },
93
- outline: { thickness: 1.6 },
94
- skinTone: { skinShadowBrightness: 0.94 },
92
+ preset: 'call_me_sensei',
93
+ rim: { intensity: { hair: 0.35 }, mode: 'depth' },
94
+ outline: { width: { cloth: 0.006 } },
95
+ ramp: { tone: { skin: [0.93, 0.74, 0.68] } },
95
96
  });
96
97
 
97
98
  applyToonShader(characterRoot, { settings });
@@ -142,7 +143,7 @@ surface detail, not displacement that replaces a geometric high-to-low bake.
142
143
 
143
144
  ## Load Nature Reference Rocks
144
145
 
145
- Version 0.4.24 understands both the original `rock-0001`–`rock-0480`
146
+ Version 0.5.0 understands both the original `rock-0001`–`rock-0480`
146
147
  identities and the Nature Reference Rocks catalog. Acquiring an asset
147
148
  directly returns its realistic PBR GLB. The one-call placement path additionally
148
149
  loads that asset's reviewed geology maps and `call_me_sensei` material settings
@@ -228,7 +229,7 @@ it uses the disk workspace passed through `--workspace`; no account is needed.
228
229
  "command": "npx",
229
230
  "args": [
230
231
  "-y",
231
- "--package=@call-me-sensei/toonlab@0.4.24",
232
+ "--package=@call-me-sensei/toonlab@0.5.0",
232
233
  "toonlab-mcp",
233
234
  "--workspace",
234
235
  "/absolute/path/to/your-game/.toonlab"
package/README.md CHANGED
@@ -19,7 +19,7 @@ Use ToonLab as a focused runtime library (`@call-me-sensei/toonlab` on npm), as
19
19
  an asset/preset authoring workspace, or as an asset-discovery service—not as a
20
20
  one-shot world generator.
21
21
 
22
- ## Versioned expectation: 0.4.24
22
+ ## Versioned expectation: 0.5.0
23
23
 
24
24
  For a correctly constructed and semantically labeled scene, one strict Call Me
25
25
  Sensei bundle application is expected to establish the supported visual
@@ -376,7 +376,7 @@ camera behavior, and game feel remain host-owned or pre-beta.
376
376
  | Cluster | Import | What you get |
377
377
  |---|---|---|
378
378
  | Styles and bundles | `@call-me-sensei/toonlab/styles` | Local, versioned style-bundle documents that coordinate domain profiles without storing asset identity or current scene state. Create, validate, serialize, parse, and resolve JSON with no account or database; hosts currently route explicitly labeled assets through each owning runtime. [Contract](https://github.com/call-me-sensei-app/toonlab/blob/main/docs/styles-and-bundles.md) |
379
- | Toon character shading | `@call-me-sensei/toonlab/toon` | Modern anime character shader: cel bands with art-directed face lighting, skin-tone shadow management, shadow-color HSV control, scene/self/contact shadows, average-shadow smoothing, rim light (fresnel or screen-space depth), stylized + anisotropic hair highlights, eye catchlights, role-aware specular, source map routing (normal/AO/emissive/MatCap/ramp/detail), inverted-hull outlines, glitter, stickers, perspective removal, shell fur, dither fades — 23 settings groups, all preset-serializable. [Docs](https://github.com/call-me-sensei-app/toonlab/blob/main/docs/toon-shading.md) |
379
+ | Toon character shading | `@call-me-sensei/toonlab/toon` | Anime-game character shader: flat lit colour, per-surface shadow tones with a warm band at the terminator, face maps baked automatically from the face, a nose shadow, eye-white shade and stocking sheen derived from the model, environment-aware light (sun, sky, cast shade), scenery, character and hair-on-face shadows, depth rim, hair ring and masked specular, ink outlines coloured from the surface, source map routing (normal/AO/emissive/MatCap/detail), stickers, shell fur, dither fades — 14 settings groups, all preset-serializable. [Docs](https://github.com/call-me-sensei-app/toonlab/blob/main/docs/toon-shading.md) |
380
380
  | Environment shading | `@call-me-sensei/toonlab/environment` | Modern anime-style scene shader for texture packs, standard glTF, and untextured scenes: material-role classification, wrapped lighting, packed-map hints, window cutouts, sun/lamp rigs, time-of-day, six-direction ambient probe, planar floor reflections, BVH vertex-AO baking, height fog, cloud shadows. [Docs](https://github.com/call-me-sensei-app/toonlab/blob/main/docs/environment.md) |
381
381
  | Water | `@call-me-sensei/toonlab/water` | Focused water treatment for a host-authored water footprint, continuous shore, and closed seabed: Gerstner waves, absorption color, refraction/caustics/foam, ripples, splashes, wakes, kelp, underwater treatment, and CPU buoyancy sampling. It does not design the coast or underwater habitat. [Docs](https://github.com/call-me-sensei-app/toonlab/blob/main/docs/water.md) |
382
382
  | Vegetation | `@call-me-sensei/toonlab/vegetation` | Tree/grass/flower construction and shader runtimes, including the focused procedural broadleaf `BranchTree` with deterministic branching, eight leaf silhouettes, caller-supplied leaf/bark textures, and a portable recipe. No authored tree asset collection is bundled or exported: reviewed tree recipes remain searchable Gallery assets delivered through MCP with their CC0 metadata. Package-generated trees preserve authored bark first and otherwise select a registered deterministic bark surface instead of a bare trunk. The experimental species roster remains repository-only. [Docs](https://github.com/call-me-sensei-app/toonlab/blob/main/docs/vegetation-sky.md) |
@@ -583,8 +583,8 @@ import { applyToonShader, createToonSettings } from '@call-me-sensei/toonlab/too
583
583
 
584
584
  const settings = createToonSettings({
585
585
  preset: 'default',
586
- skinTone: {
587
- skinShadowBrightness: 0.94,
586
+ ramp: {
587
+ tone: { skin: [0.94, 0.82, 0.76] },
588
588
  },
589
589
  });
590
590
 
package/agents/PROMPTS.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # ToonLab agent prompts
2
2
 
3
3
  These prompts are included with the npm package. They use only the supported
4
- 0.4.24 runtime, agent skills, and MCP workflows.
4
+ 0.5.0 runtime, agent skills, and MCP workflows.
5
5
 
6
6
  ## New playable game
7
7
 
@@ -11,7 +11,7 @@ References beginning `agents/` resolve from the installed ToonLab package root
11
11
 
12
12
  Use this skill when a developer wants ToonLab character toon materials, toon
13
13
  settings, preset documents, material-role handling, outlines, hair highlights,
14
- rim light, shadows, stickers, glitter, fur, or debug views in their app.
14
+ rim light, shadows, shadow tones, face maps, stickers, fur, or debug views in their app.
15
15
 
16
16
  Public imports:
17
17
  - `@call-me-sensei/toonlab/toon`
@@ -11,7 +11,7 @@ References beginning `agents/` resolve from the installed ToonLab package root
11
11
 
12
12
  Use this skill when a developer wants ToonLab character toon materials, toon
13
13
  settings, preset documents, material-role handling, outlines, hair highlights,
14
- rim light, shadows, stickers, glitter, fur, or debug views in their app.
14
+ rim light, shadows, shadow tones, face maps, stickers, fur, or debug views in their app.
15
15
 
16
16
  Public imports:
17
17
  - `@call-me-sensei/toonlab/toon`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@call-me-sensei/toonlab",
3
- "version": "0.4.24",
3
+ "version": "0.5.0",
4
4
  "description": "Anime-style game toolkit for Three.js with cel shading, vegetation, rocks, water, sky, clouds, procedural textures, and portable Lab artifacts",
5
5
  "license": "MIT",
6
6
  "author": "Hyperbond Studio PTE. LTD. (Call Me Sensei)",
@@ -340,6 +340,8 @@
340
340
  "verify:grass": "node scripts/verify-grass-palettes.mjs",
341
341
  "verify:grass-package": "node scripts/verify-packaged-grass.mjs",
342
342
  "verify:character-package": "node scripts/verify-packaged-character.mjs",
343
+ "verify:toon-shading": "node scripts/verify-toon-character-shading.mjs",
344
+ "verify:provenance": "node scripts/verify-provenance.mjs",
343
345
  "verify:ground-stabilizer": "node scripts/verify-ground-stabilizer.mjs",
344
346
  "verify:locomotion-state": "node scripts/verify-locomotion-state-machine.mjs",
345
347
  "verify:water-interaction": "node scripts/verify-water-interaction-controller.mjs",
@@ -371,7 +373,7 @@
371
373
  "verify:collision-package": "node scripts/verify-packaged-world-collision.mjs",
372
374
  "verify:official-catalog": "node scripts/verify-official-catalog-provider.mjs && node scripts/verify-official-catalog-rock-package.mjs && node scripts/verify-official-catalog-runtime.mjs && node scripts/verify-official-catalog-lod.mjs && node scripts/verify-official-catalog-placement.mjs && node scripts/verify-playground-official-catalog.mjs && node scripts/verify-packaged-official-catalog.mjs",
373
375
  "verify:system-style": "node scripts/verify-system-style-policy.mjs",
374
- "verify:release": "npm run verify:repository && npm run verify:system-style && npm run verify:style-contract && npm run verify:style-labels-package && npm run verify:collision-package && npm run verify:scene-collision && npm run verify:official-catalog && npm run verify:public-labs && npm run verify:live-labs && npm run verify:natural-rock-surfaces && npm run verify:rock-shader && npm run verify:rock-generation-lab && npm run verify:rock-lab-lifecycle && npm run verify:ground-shader && npm run verify:manufactured-materials && npm run verify:material-labeling:agent && npm run verify:material-labeling:mcp && npm run verify:water-lab && npm run verify:water && npm run verify:vegetation-labs && npm run verify:texgen && npm run verify:cloud-shader && npm run verify:sky-cloud-lab && npm run verify:world-quality && npm run verify:scatter && npm run verify:branch-tree && npm run verify:grass-package && npm run verify:character-package && npm run verify:locomotion-state && npm run verify:ground-stabilizer && npm run verify:water-interaction && npm run verify:walkable-character && npm run verify:playground-walkable && npm run verify:renderer-configuration && npm run verify:scene-scheduler && npm run verify:scene-surface && npm run verify:shared-shadow-receivers && npm run verify:scene-quality && npm run verify:quality-integration && npm run verify:lab-asset-provenance && npm run verify:api && npm run verify:types && npm run verify:mcp && npm run verify:skills && npm run verify:docs && npm run verify:local-database && npm run verify:package",
376
+ "verify:release": "npm run verify:repository && npm run verify:system-style && npm run verify:style-contract && npm run verify:style-labels-package && npm run verify:collision-package && npm run verify:scene-collision && npm run verify:official-catalog && npm run verify:public-labs && npm run verify:live-labs && npm run verify:natural-rock-surfaces && npm run verify:rock-shader && npm run verify:rock-generation-lab && npm run verify:rock-lab-lifecycle && npm run verify:ground-shader && npm run verify:manufactured-materials && npm run verify:material-labeling:agent && npm run verify:material-labeling:mcp && npm run verify:water-lab && npm run verify:water && npm run verify:vegetation-labs && npm run verify:texgen && npm run verify:cloud-shader && npm run verify:sky-cloud-lab && npm run verify:world-quality && npm run verify:scatter && npm run verify:branch-tree && npm run verify:grass-package && npm run verify:character-package && npm run verify:toon-shading && npm run verify:locomotion-state && npm run verify:ground-stabilizer && npm run verify:water-interaction && npm run verify:walkable-character && npm run verify:playground-walkable && npm run verify:renderer-configuration && npm run verify:scene-scheduler && npm run verify:scene-surface && npm run verify:shared-shadow-receivers && npm run verify:scene-quality && npm run verify:quality-integration && npm run verify:lab-asset-provenance && npm run verify:api && npm run verify:types && npm run verify:mcp && npm run verify:skills && npm run verify:docs && npm run verify:local-database && npm run verify:package",
375
377
  "verify:generation": "node scripts/verify-generation.mjs",
376
378
  "verify:post": "node scripts/verify-post.mjs",
377
379
  "verify:camera": "node scripts/verify-camera.mjs",
@@ -5,6 +5,7 @@ import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js';
5
5
  import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
6
6
  import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
7
7
  import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
8
+ import { TGALoader } from 'three/examples/jsm/loaders/TGALoader.js';
8
9
  import { USDZLoader } from 'three/examples/jsm/loaders/USDZLoader.js';
9
10
  import { unzipSync } from 'three/examples/jsm/libs/fflate.module.js';
10
11
  import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
@@ -248,6 +249,9 @@ function createModelLoadingManager() {
248
249
  manager.setURLModifier((url) => {
249
250
  return resolveKnownModelAssetUrl(url) ?? url;
250
251
  });
252
+ // FBX and OBJ exports from Max/Maya commonly reference TGA textures, which
253
+ // the browser's image decoder cannot read.
254
+ manager.addHandler(/\.tga$/i, new TGALoader(manager));
251
255
 
252
256
  return manager;
253
257
  }
@@ -331,7 +335,20 @@ function configureGltfLoader(loader, {
331
335
  }
332
336
  }
333
337
 
338
+ // Records the source format on every material so conversion can apply the
339
+ // format's colour semantics (glTF records more detail itself).
340
+ function tagMaterialSourceFormat(root, format) {
341
+ root?.traverse?.((obj) => {
342
+ if (!obj.isMesh || !obj.material) return;
343
+ for (const material of toMaterialArray(obj.material)) {
344
+ if (!material || material.userData?.toonSource?.format) continue;
345
+ material.userData.toonSource = { ...(material.userData.toonSource || {}), format };
346
+ }
347
+ });
348
+ }
349
+
334
350
  function createModelAsset({ clips = [], format, materialUrl = null, resourcePath, root, url, vrm = null }) {
351
+ tagMaterialSourceFormat(root, format);
335
352
  return {
336
353
  clips,
337
354
  format,
@@ -378,6 +395,7 @@ function annotateGltfSourceMetadata(gltf, url) {
378
395
 
379
396
  material.userData.toonSource = {
380
397
  ...(material.userData.toonSource || {}),
398
+ alphaMode: materialDef.alphaMode || 'OPAQUE',
381
399
  baseColorTextureIndex: Number.isInteger(baseColorTextureIndex) ? baseColorTextureIndex : null,
382
400
  format: 'gltf',
383
401
  imageCount: json.images?.length ?? 0,
@@ -399,6 +417,24 @@ function annotateGltfSourceMetadata(gltf, url) {
399
417
  }
400
418
  }
401
419
 
420
+ // Records on each MMD toon ramp (material.gradientMap) its file and whether it
421
+ // is one of MMD's shared toon01–10 presets or a ramp the model's author
422
+ // shipped: only authored ramps are imported as the material's shadow tone.
423
+ // Wraps three-stdlib's material builder; without it ramps simply stay
424
+ // unimported.
425
+ function tagMmdToonTextures(loader) {
426
+ const materialBuilder = loader.meshBuilder?.materialBuilder;
427
+ const loadTexture = materialBuilder?._loadTexture;
428
+ if (typeof loadTexture !== 'function') return;
429
+ materialBuilder._loadTexture = function loadTaggedTexture(filePath, textures, params, ...rest) {
430
+ const texture = loadTexture.call(this, filePath, textures, params, ...rest);
431
+ if (params?.isToonTexture && texture?.isTexture) {
432
+ texture.userData.mmdToon = { file: String(filePath ?? ''), shared: params.isDefaultToonTexture === true };
433
+ }
434
+ return texture;
435
+ };
436
+ }
437
+
402
438
  export async function loadModelAsset(url, {
403
439
  decoderBasePath = null,
404
440
  materialUrl = null,
@@ -413,6 +449,7 @@ export async function loadModelAsset(url, {
413
449
  const { MMDLoader } = await loadMmdSupport();
414
450
  const loader = new MMDLoader(manager);
415
451
  loader.setResourcePath(resourcePath);
452
+ tagMmdToonTextures(loader);
416
453
  const root = await loadAsync(loader, url);
417
454
 
418
455
  return createModelAsset({
@@ -453,8 +490,12 @@ export async function loadModelAsset(url, {
453
490
 
454
491
  const vrm = gltf.userData?.vrm ?? null;
455
492
  if (vrm) {
456
- // VRM0 models face +Z after this, matching VRM1/three convention.
493
+ // VRM0 models face +Z after this, matching VRM1/three convention. The
494
+ // turn is a 180° rotation on the root itself, so in the root's own frame
495
+ // a VRM0 character faces −Z; record that for head-frame consumers
496
+ // (automatic roles, the face shadow bake, head tracking).
457
497
  vrmUtils.rotateVRM0(vrm);
498
+ if (vrm.meta?.metaVersion === '0') gltf.scene.userData.toonForward = [0, 0, -1];
458
499
  // Clips animate the raw scene bones directly; without this vrm.update()
459
500
  // would overwrite them from the untouched normalized rig every frame.
460
501
  vrm.humanoid.autoUpdateHumanBones = false;
@@ -87,8 +87,10 @@ const FACE_TOKENS = [
87
87
  'mouth',
88
88
  'tooth',
89
89
  'teeth',
90
+ 'tongue',
90
91
  'brow',
91
92
  'lash',
93
+ 'eyelid',
92
94
  '颜',
93
95
  '顏',
94
96
  '面',
@@ -171,6 +173,36 @@ export function hasAnyToken(text, tokens) {
171
173
  return tokens.some((token) => text.includes(token));
172
174
  }
173
175
 
176
+ // Words that contain a role token without meaning that role. They are blanked
177
+ // out before that role's tokens are tested, so "armor" never reads as an arm,
178
+ // "headphones" never as a head, and a hair clip is an accessory, not hair.
179
+ const SEPARATOR = '[\\s_-]*';
180
+ const EYE_EXCLUSIONS = [
181
+ new RegExp(`eye${SEPARATOR}(brow|lash|line|liner|lid|shadow)`, 'g'),
182
+ ];
183
+ const FACE_EXCLUSIONS = [
184
+ /surface|interface|facet/g,
185
+ new RegExp(`head${SEPARATOR}(phone|set|band|gear|wear|piece|dress|acc|scarf|lamp|light)`, 'g'),
186
+ /splash|flash/g,
187
+ ];
188
+ const SKIN_EXCLUSIONS = [
189
+ new RegExp(`(arm|leg)${SEPARATOR}warmer`, 'g'),
190
+ /armou?r|charm|alarm|warm|farm|harm/g,
191
+ new RegExp(`arm${SEPARATOR}(band|let|guard|ring)`, 'g'),
192
+ /legging|legend|elegan/g,
193
+ /handle|handkerchief|handbag/g,
194
+ new RegExp(`body${SEPARATOR}suit|bodice`, 'g'),
195
+ ];
196
+ const HAIR_EXCLUSIONS = [
197
+ /chair/g,
198
+ new RegExp(`hair${SEPARATOR}(pin|clip|band|tie|ornament|acc|ribbon|bow|flower|stick|comb|ring|clasp|jewel|net)`, 'g'),
199
+ ];
200
+
201
+ function hasRoleToken(text, tokens, exclusions = []) {
202
+ const cleaned = exclusions.reduce((value, pattern) => value.replace(pattern, ' '), text);
203
+ return hasAnyToken(cleaned, tokens);
204
+ }
205
+
174
206
  export function normalizeMaterialRole(value, fallback = 'default') {
175
207
  if (Number.isFinite(value)) {
176
208
  return MATERIAL_ROLE_NAMES_BY_VALUE[Math.round(value)] ?? fallback;
@@ -315,7 +347,10 @@ function flagsForPrimaryRole(primaryRole) {
315
347
  function classifyMaterialHeuristics(mat) {
316
348
  const text = materialText(mat);
317
349
  const isCatchlight = hasAnyToken(text, CATCHLIGHT_TOKENS);
318
- const isEyeHighlight = hasAnyToken(text, EYE_HIGHLIGHT_TOKENS);
350
+ // "highlight" alone is ambiguous: a hair highlight overlay is not an eye.
351
+ const namesHairOnly = hasRoleToken(text, HAIR_TOKENS, HAIR_EXCLUSIONS) &&
352
+ !hasRoleToken(text, EYE_TOKENS, EYE_EXCLUSIONS);
353
+ const isEyeHighlight = hasAnyToken(text, EYE_HIGHLIGHT_TOKENS) && !namesHairOnly;
319
354
  const isBlush = hasAnyToken(text, BLUSH_TOKENS);
320
355
  const isTransparentOverlay = isEyeHighlight ||
321
356
  isCatchlight ||
@@ -324,10 +359,10 @@ function classifyMaterialHeuristics(mat) {
324
359
  const isIris = hasAnyToken(text, IRIS_TOKENS);
325
360
  const isPupil = hasAnyToken(text, PUPIL_TOKENS);
326
361
  const isSclera = hasAnyToken(text, SCLERA_TOKENS);
327
- const isEye = hasAnyToken(text, EYE_TOKENS);
328
- const isFace = hasAnyToken(text, FACE_TOKENS);
329
- const isSkin = hasAnyToken(text, SKIN_TOKENS);
330
- const isHair = hasAnyToken(text, HAIR_TOKENS);
362
+ const isEye = hasRoleToken(text, EYE_TOKENS, EYE_EXCLUSIONS);
363
+ const isFace = hasRoleToken(text, FACE_TOKENS, FACE_EXCLUSIONS);
364
+ const isSkin = hasRoleToken(text, SKIN_TOKENS, SKIN_EXCLUSIONS);
365
+ const isHair = hasRoleToken(text, HAIR_TOKENS, HAIR_EXCLUSIONS);
331
366
  const isMetal = hasAnyToken(text, METAL_TOKENS);
332
367
  const isCostume = hasAnyToken(text, COSTUME_TOKENS);
333
368
 
@@ -1,4 +1,4 @@
1
- import { syncToonSceneLights } from '../shaders-tsl/chunks/character-scene-lights.js';
1
+ import { syncToonSceneLights } from '../shaders-tsl/character/sceneLights.js';
2
2
  import {
3
3
  classifyEnvironmentMaterialRole,
4
4
  toMaterialArray,
@@ -13,12 +13,12 @@ import {
13
13
  } from './manufacturedMaterialContract.js';
14
14
 
15
15
  // Environment node materials read scene lights from the shared toon light
16
- // uniforms (shaders-tsl/chunks/character-scene-lights.js) instead of three's
16
+ // uniforms (shaders-tsl/character/sceneLights.js) instead of three's
17
17
  // classic light blocks; converted meshes mirror the light state right before
18
18
  // they render, exactly like the toon adapter's toonNodeLightSync. Both
19
19
  // renderers invoke Object3D.onBeforeRender.
20
20
  function environmentNodeLightSync(renderer, scene, camera) {
21
- syncToonSceneLights(scene, camera);
21
+ syncToonSceneLights(scene, camera, renderer);
22
22
  }
23
23
  import {
24
24
  applyEnvironmentSettingsToMaterial,
@@ -37,6 +37,10 @@ export const DEFAULT_POST_PROCESSING_PARAMETERS = Object.freeze({
37
37
  // Defaults of 1/1 reproduce plain bloom.
38
38
  bloomBackgroundSuppress: 1.0,
39
39
  bloomCharacterBoost: 1.0,
40
+ // Saturation of bloom gathered from character pixels. Bloom is additive and
41
+ // pushes skin toward white; above 1 it keeps the glow the character's own
42
+ // hue (warm skin glows warm). 1 = unchanged.
43
+ bloomCharacterSaturation: 1.0,
40
44
  // 'single' = the original one-pass 9-tap bloom. 'pyramid' = multi-pass
41
45
  // dual-filter mip chain: wider, softer glow for roughly the same cost at
42
46
  // high radii, at the price of extra render targets.
@@ -160,6 +164,7 @@ export const POST_PROCESSING_PRESETS = Object.freeze({
160
164
  },
161
165
  parameters: {
162
166
  bloomCharacterBoost: 1.6,
167
+ bloomCharacterSaturation: 1.4,
163
168
  bloomLevels: 5,
164
169
  bloomMode: 'pyramid',
165
170
  bloomRadius: 0.5,
@@ -275,6 +280,11 @@ const POST_PROCESSING_FIELD_DEFINITIONS = Object.freeze({
275
280
  label: 'Bloom Character Boost',
276
281
  range: { max: 4, min: 0, step: 0.05 },
277
282
  },
283
+ bloomCharacterSaturation: {
284
+ description: 'Scales the saturation of bloom gathered from character pixels when a character mask is connected.',
285
+ label: 'Bloom Character Saturation',
286
+ range: { max: 3, min: 0, step: 0.05 },
287
+ },
278
288
  bloomLevels: {
279
289
  description: 'Number of mip levels in the pyramid bloom chain (pyramid mode only).',
280
290
  label: 'Bloom Levels',
@@ -879,6 +889,7 @@ function applyCompositeSettings(material, settings, camera, depthTexture, width,
879
889
  const uniforms = material.uniforms;
880
890
  uniforms.bloomBackgroundSuppress.value = parameters.bloomBackgroundSuppress;
881
891
  uniforms.bloomCharacterBoost.value = parameters.bloomCharacterBoost;
892
+ uniforms.bloomCharacterSaturation.value = parameters.bloomCharacterSaturation;
882
893
  uniforms.bloomRadius.value = parameters.bloomRadius;
883
894
  uniforms.bloomStrength.value = features.bloom ? parameters.bloomStrength : 0.0;
884
895
  uniforms.bloomThreshold.value = parameters.bloomThreshold;
@@ -1028,6 +1039,7 @@ export function createPostProcessingPipeline({
1028
1039
  bloomPrefilterMaterial.uniforms.tInput.value = target.texture;
1029
1040
  bloomPrefilterMaterial.uniforms.bloomThreshold.value = settings.parameters.bloomThreshold;
1030
1041
  bloomPrefilterMaterial.uniforms.bloomCharacterBoost.value = settings.parameters.bloomCharacterBoost;
1042
+ bloomPrefilterMaterial.uniforms.bloomCharacterSaturation.value = settings.parameters.bloomCharacterSaturation;
1031
1043
  bloomPrefilterMaterial.uniforms.bloomBackgroundSuppress.value = settings.parameters.bloomBackgroundSuppress;
1032
1044
  bloomPrefilterMaterial.uniforms.tCharacterMask.value = compositeMaterial.uniforms.tCharacterMask.value;
1033
1045
  bloomPrefilterMaterial.uniforms.useCharacterMask.value = compositeMaterial.uniforms.useCharacterMask.value;
@@ -0,0 +1,147 @@
1
+ // Cast shadows on the character (spec §5.5). Each returns a visibility
2
+ // (1 lit, 0 shadowed) that multiplies the lit amount — a cast shadow moves a
3
+ // pixel into the shadow tone, it never darkens the tone.
4
+
5
+ import {
6
+ abs,
7
+ mix,
8
+ cameraFar,
9
+ cameraNear,
10
+ cameraProjectionMatrix,
11
+ cameraViewMatrix,
12
+ clamp,
13
+ dot,
14
+ float,
15
+ If,
16
+ max,
17
+ min,
18
+ orthographicDepthToViewZ,
19
+ perspectiveDepthToViewZ,
20
+ screenUV,
21
+ select,
22
+ smoothstep,
23
+ step,
24
+ vec2,
25
+ vec4,
26
+ } from 'three/tsl';
27
+
28
+ // Screen UV (top-left origin, like screenUV) of a view-space point.
29
+ export function viewToScreenUV(viewPosition) {
30
+ const clip = cameraProjectionMatrix.mul(vec4(viewPosition, 1));
31
+ const ndc = clip.xy.div(max(clip.w, 1e-6));
32
+ return vec2(ndc.x.mul(0.5).add(0.5), ndc.y.mul(-0.5).add(0.5));
33
+ }
34
+
35
+ /** Screen UV of this pixel shifted by a view-space offset at `viewPosition`. */
36
+ export function offsetScreenUV(viewPosition, viewOffset, baseUV = viewToScreenUV(viewPosition)) {
37
+ return screenUV.add(viewToScreenUV(viewPosition.add(viewOffset)).sub(baseUV));
38
+ }
39
+
40
+ /**
41
+ * Screen-UV change per metre of a lateral (view-plane) offset at this
42
+ * pixel's depth: exact for perspective and orthographic cameras.
43
+ */
44
+ export function uvPerMetre(viewPosition) {
45
+ const orthographic = cameraProjectionMatrix.element(3).w.equal(1);
46
+ const w = select(orthographic, float(1), viewPosition.z.negate().max(1e-4));
47
+ return vec2(cameraProjectionMatrix.element(0).x, cameraProjectionMatrix.element(1).y.negate()).mul(0.5).div(w);
48
+ }
49
+
50
+ // The character passes store window depth in [0, 1], plus 2 on face (and
51
+ // eye) surfaces so face receivers can ignore them; node materials clamp
52
+ // their output at 0, so the flag must stay positive.
53
+ export const FACE_DEPTH_OFFSET = 2;
54
+
55
+ /** `{ depth, face }` from a value stored by the character passes. */
56
+ export function decodeStoredDepth(stored) {
57
+ const face = stored.greaterThan(1.5);
58
+ return { depth: select(face, stored.sub(FACE_DEPTH_OFFSET), stored), face };
59
+ }
60
+
61
+ /** Window depth written by the depth prepass → distance in front of the camera. */
62
+ export function prepassDistance(windowDepth) {
63
+ const orthographic = cameraProjectionMatrix.element(3).w.equal(1);
64
+ const viewZ = select(
65
+ orthographic,
66
+ orthographicDepthToViewZ(windowDepth, cameraNear, cameraFar),
67
+ perspectiveDepthToViewZ(windowDepth, cameraNear, cameraFar),
68
+ );
69
+ return viewZ.negate();
70
+ }
71
+
72
+ /**
73
+ * The character's own orthographic shadow map (characterRenderPasses.js):
74
+ * normal-offset and slope-scaled bias, bilinear filtering, and a fade toward the
75
+ * edge of the map's coverage.
76
+ */
77
+ export function characterShadowVisibility(u, state, worldPosition, geometryNormal, faceLike, gate) {
78
+ const visibility = float(1).toVar('toonCharacterShadow');
79
+ If(state.shadowReady.greaterThan(0.5).and(gate), () => {
80
+ // Offset along the normal side that faces the light (thin, two-sided
81
+ // cards such as hair are lit from either side).
82
+ const facingSigned = dot(geometryNormal, state.shadowDirection);
83
+ const towardLight = geometryNormal.mul(select(facingSigned.lessThan(0), float(-1), float(1)));
84
+ const facing = abs(facingSigned).clamp(0, 1);
85
+ const receiver = worldPosition.add(towardLight.mul(u.characterShadowNormalBias));
86
+ const clip = state.shadowMatrix.mul(vec4(receiver, 1));
87
+ const coord = clip.xyz.div(clip.w).mul(0.5).add(0.5).toVar();
88
+ const inside = coord.x.greaterThanEqual(0).and(coord.x.lessThanEqual(1))
89
+ .and(coord.y.greaterThanEqual(0)).and(coord.y.lessThanEqual(1))
90
+ .and(coord.z.greaterThanEqual(0)).and(coord.z.lessThanEqual(1));
91
+ If(inside, () => {
92
+ const slope = float(1).add(float(1).sub(facing).mul(3));
93
+ const reference = coord.z.sub(u.characterShadowDepthBias.mul(slope).div(max(state.shadowDepthRange, 1e-4)));
94
+ // Bilinearly weighted comparisons of the 2×2 (softness-scaled) texels
95
+ // around the receiver: smooth edges from four taps.
96
+ const grid = state.shadowMapSize.div(max(u.characterShadowSoftness, 0.25));
97
+ const cell = coord.xy.mul(grid).sub(0.5);
98
+ const base = cell.floor();
99
+ const blend = cell.sub(base);
100
+ // Faces and eyes ignore face occluders (layered lashes, brows and
101
+ // eye whites a few millimetres apart must not shadow each other).
102
+ const ignoreFace = faceLike.greaterThan(0.5);
103
+ const tap = (x, y) => {
104
+ const stored = decodeStoredDepth(state.shadowMap.sample(base.add(vec2(x, y)).add(0.5).div(grid)).level(0).x);
105
+ return select(stored.face.and(ignoreFace), float(1), step(reference, stored.depth));
106
+ };
107
+ const lit = mix(
108
+ mix(tap(0, 0), tap(1, 0), blend.x),
109
+ mix(tap(0, 1), tap(1, 1), blend.x),
110
+ blend.y,
111
+ );
112
+ const edge = max(abs(coord.x.sub(0.5)), abs(coord.y.sub(0.5))).mul(2);
113
+ const coverage = float(1).sub(smoothstep(0.9, 1, edge));
114
+ visibility.assign(mix1(lit, coverage));
115
+ });
116
+ });
117
+ return visibility;
118
+ }
119
+
120
+ function mix1(value, amount) {
121
+ return float(1).add(value.sub(1).mul(amount));
122
+ }
123
+
124
+ /**
125
+ * Screen-space shadow toward the light against the depth prepass: something
126
+ * in front of the short ray toward the light (bangs over the forehead, hair
127
+ * on the cheek) shadows the pixel. Face receivers ignore face occluders
128
+ * (`faceWeight`), so a face never shadows itself.
129
+ */
130
+ export function screenSpaceShadowVisibility(u, state, viewPosition, lightDirectionWorld, faceWeight, gate) {
131
+ const occlusion = float(0).toVar('toonScreenShadow');
132
+ If(state.depthReady.greaterThan(0.5).and(gate), () => {
133
+ const lightView = cameraViewMatrix.mul(vec4(lightDirectionWorld, 0)).xyz;
134
+ const baseUV = viewToScreenUV(viewPosition);
135
+ for (const fraction of [0.5, 1]) {
136
+ const rayPoint = viewPosition.add(lightView.mul(u.screenShadowWidth.mul(fraction)));
137
+ const stored = decodeStoredDepth(state.depthMap.sample(offsetScreenUV(viewPosition, lightView.mul(u.screenShadowWidth.mul(fraction)), baseUV)).level(0).x);
138
+ const nearer = rayPoint.z.negate().sub(prepassDistance(stored.depth));
139
+ const faceOccluder = select(stored.face, float(1), float(0));
140
+ const hit = smoothstep(0.002, 0.006, nearer)
141
+ .mul(float(1).sub(smoothstep(0.12, 0.25, nearer)))
142
+ .mul(float(1).sub(faceOccluder.mul(min(faceWeight.mul(2), 1))));
143
+ occlusion.assign(max(occlusion, hit));
144
+ }
145
+ });
146
+ return float(1).sub(occlusion);
147
+ }