@call-me-sensei/toonlab 0.1.1

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/ATTRIBUTION.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +125 -0
  4. package/package.json +97 -0
  5. package/src/character/characterRig.js +353 -0
  6. package/src/character/freestyleSwimClip.js +541 -0
  7. package/src/character/index.js +4 -0
  8. package/src/character/modelLoader.js +433 -0
  9. package/src/core/materialRoles.js +438 -0
  10. package/src/core/presetDocuments.js +110 -0
  11. package/src/core/shaderBackend.js +24 -0
  12. package/src/debrisgen/debrisFields.js +544 -0
  13. package/src/debrisgen/debrisGenerator.js +1929 -0
  14. package/src/debrisgen/debrisPalettes.js +71 -0
  15. package/src/debrisgen/debrisPhysics.js +198 -0
  16. package/src/debrisgen/debrisPresets.js +155 -0
  17. package/src/debrisgen/debrisSettings.js +333 -0
  18. package/src/debrisgen/debrisTextures.js +380 -0
  19. package/src/debrisgen/index.js +5 -0
  20. package/src/debug/fieldValues.js +95 -0
  21. package/src/debug/index.js +7 -0
  22. package/src/debug/settingsPanel.js +170 -0
  23. package/src/environment/environmentAmbientProbe.js +160 -0
  24. package/src/environment/environmentMaterialAdapter.js +336 -0
  25. package/src/environment/environmentMaterialClassifier.js +178 -0
  26. package/src/environment/environmentPlanarReflection.js +201 -0
  27. package/src/environment/environmentPresets.js +443 -0
  28. package/src/environment/environmentRigs.js +684 -0
  29. package/src/environment/environmentSettings.js +504 -0
  30. package/src/environment/environmentShaderMaterials.js +318 -0
  31. package/src/environment/environmentSunShadowPass.js +268 -0
  32. package/src/environment/environmentTextureResolver.js +161 -0
  33. package/src/environment/environmentTimeOfDay.js +167 -0
  34. package/src/environment/environmentVertexAo.js +188 -0
  35. package/src/environment/index.js +8 -0
  36. package/src/environment/scanAssetStylize.js +103 -0
  37. package/src/index.js +21 -0
  38. package/src/loaders/index.js +1 -0
  39. package/src/post/index.js +2 -0
  40. package/src/post/postProcessing.js +1087 -0
  41. package/src/rockgen/export/glbExport.js +177 -0
  42. package/src/rockgen/heightfield/heightfieldErosion.js +7 -0
  43. package/src/rockgen/heightfield/heightfieldPatch.js +183 -0
  44. package/src/rockgen/heightfield/stylizedErosionSim.js +356 -0
  45. package/src/rockgen/index.js +14 -0
  46. package/src/rockgen/mesh/meshAttributes.js +374 -0
  47. package/src/rockgen/mesh/meshDocument.js +158 -0
  48. package/src/rockgen/mesh/surfaceNets.js +329 -0
  49. package/src/rockgen/noise/cellularNoise3.js +92 -0
  50. package/src/rockgen/noise/prng.js +62 -0
  51. package/src/rockgen/noise/simplexNoise3.js +89 -0
  52. package/src/rockgen/noise/valueNoise3.js +87 -0
  53. package/src/rockgen/rockDocument.js +255 -0
  54. package/src/rockgen/rockHelpers.js +8 -0
  55. package/src/rockgen/rockgenPresets.js +559 -0
  56. package/src/rockgen/rockgenSettings.js +756 -0
  57. package/src/rockgen/sdf/fieldCompiler.js +544 -0
  58. package/src/rockgen/sdf/sculptEdits.js +66 -0
  59. package/src/rockgen/sdf/sdfModifiers.js +45 -0
  60. package/src/rockgen/sdf/sdfOps.js +37 -0
  61. package/src/rockgen/sdf/sdfPrimitives.js +88 -0
  62. package/src/shaders-tsl/anime.js +963 -0
  63. package/src/shaders-tsl/chunks/character-color.js +64 -0
  64. package/src/shaders-tsl/chunks/character-highlights.js +174 -0
  65. package/src/shaders-tsl/chunks/character-lighting.js +309 -0
  66. package/src/shaders-tsl/chunks/character-material-maps.js +146 -0
  67. package/src/shaders-tsl/chunks/character-roles.js +52 -0
  68. package/src/shaders-tsl/chunks/character-scene-lights.js +270 -0
  69. package/src/shaders-tsl/chunks/character-shadow-color.js +61 -0
  70. package/src/shaders-tsl/chunks/character-skinning.js +140 -0
  71. package/src/shaders-tsl/chunks/environment-color.js +47 -0
  72. package/src/shaders-tsl/chunks/environment-debug.js +79 -0
  73. package/src/shaders-tsl/chunks/environment-lighting.js +260 -0
  74. package/src/shaders-tsl/chunks/environment-sun-shadow.js +87 -0
  75. package/src/shaders-tsl/chunks/foliage-fog.js +60 -0
  76. package/src/shaders-tsl/chunks/pass-depth-color.js +89 -0
  77. package/src/shaders-tsl/chunks/stylized-cloud-shadow.js +70 -0
  78. package/src/shaders-tsl/chunks/water-color.js +190 -0
  79. package/src/shaders-tsl/chunks/water-common.js +124 -0
  80. package/src/shaders-tsl/chunks/water-foam.js +93 -0
  81. package/src/shaders-tsl/chunks/water-lighting.js +116 -0
  82. package/src/shaders-tsl/chunks/water-ripple.js +64 -0
  83. package/src/shaders-tsl/chunks/water-waves.js +100 -0
  84. package/src/shaders-tsl/environment-ao-overlay.js +60 -0
  85. package/src/shaders-tsl/environment.js +680 -0
  86. package/src/shaders-tsl/flower.js +267 -0
  87. package/src/shaders-tsl/grass.js +193 -0
  88. package/src/shaders-tsl/post-composite.js +461 -0
  89. package/src/shaders-tsl/sky.js +175 -0
  90. package/src/shaders-tsl/tree-leaf.js +264 -0
  91. package/src/shaders-tsl/water-breaker.js +423 -0
  92. package/src/shaders-tsl/water-kelp.js +171 -0
  93. package/src/shaders-tsl/water-rain.js +107 -0
  94. package/src/shaders-tsl/water-simulation.js +140 -0
  95. package/src/shaders-tsl/water-splash.js +298 -0
  96. package/src/shaders-tsl/water.js +509 -0
  97. package/src/sky/index.js +2 -0
  98. package/src/sky/stylizedSky.js +341 -0
  99. package/src/toon/characterRenderPasses.js +646 -0
  100. package/src/toon/index.js +4 -0
  101. package/src/toon/settings/alphaSettings.js +170 -0
  102. package/src/toon/settings/averageShadowSettings.js +181 -0
  103. package/src/toon/settings/baseTextureSettings.js +133 -0
  104. package/src/toon/settings/celShadeSettings.js +96 -0
  105. package/src/toon/settings/contactShadowSettings.js +114 -0
  106. package/src/toon/settings/eyeHighlightSettings.js +187 -0
  107. package/src/toon/settings/faceLightingSettings.js +135 -0
  108. package/src/toon/settings/furSettings.js +112 -0
  109. package/src/toon/settings/glitterSettings.js +102 -0
  110. package/src/toon/settings/hairHighlightSettings.js +325 -0
  111. package/src/toon/settings/indirectLightSettings.js +244 -0
  112. package/src/toon/settings/localLightSettings.js +176 -0
  113. package/src/toon/settings/materialMapSettings.js +299 -0
  114. package/src/toon/settings/outlineSettings.js +263 -0
  115. package/src/toon/settings/perspectiveRemovalSettings.js +60 -0
  116. package/src/toon/settings/rimLightSettings.js +268 -0
  117. package/src/toon/settings/sceneShadowSettings.js +147 -0
  118. package/src/toon/settings/selfShadowSettings.js +221 -0
  119. package/src/toon/settings/shadowColorSettings.js +238 -0
  120. package/src/toon/settings/skinToneSettings.js +150 -0
  121. package/src/toon/settings/specularSettings.js +314 -0
  122. package/src/toon/settings/stickerSettings.js +102 -0
  123. package/src/toon/toonMaterialAdapter.js +2235 -0
  124. package/src/toon/toonSettings.js +860 -0
  125. package/src/vegetation/flowerSpecies.js +260 -0
  126. package/src/vegetation/index.js +10 -0
  127. package/src/vegetation/stylizedBush.js +136 -0
  128. package/src/vegetation/stylizedFlower.js +213 -0
  129. package/src/vegetation/stylizedFlowers.js +297 -0
  130. package/src/vegetation/stylizedGrass.js +491 -0
  131. package/src/vegetation/stylizedTree.js +2449 -0
  132. package/src/vegetation/stylizedTreeFoliage.js +691 -0
  133. package/src/vegetation/treeExport.js +300 -0
  134. package/src/vegetation/treeRecipe.js +647 -0
  135. package/src/water/index.js +2 -0
  136. package/src/water/water.js +20 -0
  137. package/src/water/waterBreakerSystem.js +591 -0
  138. package/src/water/waterInteraction.js +193 -0
  139. package/src/water/waterMaterial.js +263 -0
  140. package/src/water/waterRain.js +136 -0
  141. package/src/water/waterRippleSimulation.js +227 -0
  142. package/src/water/waterScenePasses.js +415 -0
  143. package/src/water/waterSettings.js +1138 -0
  144. package/src/water/waterSplashSystem.js +251 -0
  145. package/src/water/waterSurface.js +475 -0
  146. package/src/water/waterVegetation.js +83 -0
@@ -0,0 +1,646 @@
1
+ import * as THREE from 'three';
2
+ import { float, Fn, If, int, uniform, uniformArray, uv, vec3, vec4 } from 'three/tsl';
3
+ import { NodeMaterial } from 'three/webgpu';
4
+
5
+ import {
6
+ applyShadowClipAdjust,
7
+ createPassDepthColorMaterial,
8
+ PassBasicNodeMaterial,
9
+ } from '../shaders-tsl/chunks/pass-depth-color.js';
10
+ import {
11
+ environmentSunShadow,
12
+ sampleEnvironmentSunShadow,
13
+ } from '../shaders-tsl/chunks/environment-sun-shadow.js';
14
+
15
+ // Runtime render passes for the character toon shader.
16
+ //
17
+ // applyToonShader() alone produces a complete material; this module adds the
18
+ // per-frame passes that unlock the screen-space and shadow-map features:
19
+ //
20
+ // 1. Scene depth prepass -> depth-texture rim light + contact shadow
21
+ // 2. Character-only orthographic shadow map -> real self shadow
22
+ // 3. Head bone tracking -> head-space face shading
23
+ // 4. Average shadow measurement -> per-character uniform scene shadow
24
+ // 5. Character mask -> character-aware bloom in the post pipeline
25
+ //
26
+ // Every pass is auto-gated: it only renders when at least one registered
27
+ // material actually consumes its output, so leaving this running with the
28
+ // features disabled costs almost nothing.
29
+ //
30
+ // Usage:
31
+ // const passes = createCharacterRenderPasses({ renderer, scene, camera });
32
+ // passes.registerCharacterRoot(modelRoot); // after applyToonShader
33
+ // ...in the render loop, before rendering:
34
+ // passes.update();
35
+
36
+ // Layer used to draw character meshes in isolation (self shadow, mask).
37
+ export const TOON_CHARACTER_LAYER = 15;
38
+
39
+ const MAX_MEASURE_SLOTS = 16;
40
+
41
+ const DEFAULT_SELF_SHADOW_OPTIONS = Object.freeze({
42
+ // 'mainLight' follows the scene's directional light; 'cameraRelative' keeps
43
+ // an art-directed angle relative to the camera so face/neck shadows stay
44
+ // attractive from any viewpoint.
45
+ directionMode: 'mainLight',
46
+ cameraRelativePitch: 30,
47
+ cameraRelativeYaw: 0,
48
+ enabled: true,
49
+ mapSize: 2048,
50
+ // World-space receiver biases, slope-scaled in the shader.
51
+ depthBias: 0.01,
52
+ normalBias: 0.015,
53
+ ndotLFix: true,
54
+ // 0 = 1 tap, 1 = 4-tap PCF, 2 = 9-tap PCF.
55
+ quality: 2,
56
+ // Re-sharpen half-width applied after PCF so soft shadows keep a cel edge.
57
+ sharpen: 0.25,
58
+ // View distance at which the self shadow fades out entirely.
59
+ fadeDistance: 20,
60
+ });
61
+
62
+ function toMaterialArray(material) {
63
+ return Array.isArray(material) ? material : [material].filter(Boolean);
64
+ }
65
+
66
+ function isAnimeToonMaterial(mat) {
67
+ return Boolean(mat?.uniforms?.materialRole);
68
+ }
69
+
70
+ function collectToonMaterials(root) {
71
+ const materials = [];
72
+ root.traverse((obj) => {
73
+ if (!obj.isMesh || !obj.material) return;
74
+ for (const mat of toMaterialArray(obj.material)) {
75
+ if (isAnimeToonMaterial(mat)) materials.push(mat);
76
+ }
77
+ });
78
+ return materials;
79
+ }
80
+
81
+ function setUniform(materials, name, value) {
82
+ for (const mat of materials) {
83
+ const uniform = mat.uniforms?.[name];
84
+ if (!uniform) continue;
85
+ if (value?.isVector3 && uniform.value?.isVector3) uniform.value.copy(value);
86
+ else if (value?.isVector2 && uniform.value?.isVector2) uniform.value.copy(value);
87
+ else if (value?.isMatrix4 && uniform.value?.isMatrix4) uniform.value.copy(value);
88
+ else uniform.value = value;
89
+ }
90
+ }
91
+
92
+ function anyUniform(materials, predicate) {
93
+ return materials.some((mat) => predicate(mat.uniforms));
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Head bone resolution
98
+ // ---------------------------------------------------------------------------
99
+
100
+ function looseName(name) {
101
+ return String(name ?? '').replace(/[^a-z0-9぀-ヿ一-鿿]/gi, '').toLowerCase();
102
+ }
103
+
104
+ function findHeadBone(root) {
105
+ const candidates = [];
106
+ root.traverse((obj) => {
107
+ if (!obj.isBone && !(obj.isObject3D && /bone/i.test(obj.type))) return;
108
+ const name = looseName(obj.name);
109
+ if (!name) return;
110
+ // Exact conventions first: VRM/Mixamo 'head', Rigify 'DEF-head'/'defhead',
111
+ // MMD '頭'. Reject end/tip/top helper bones.
112
+ if (name === 'head' || name === 'defhead' || name === 'mixamorighead' || name === '頭') {
113
+ candidates.push({ obj, score: 0 });
114
+ } else if (/head/.test(name) && !/(end|tip|top|band|phone|wear|acc)/.test(name)) {
115
+ candidates.push({ obj, score: 1 });
116
+ }
117
+ });
118
+ candidates.sort((a, b) => a.score - b.score);
119
+ return candidates[0]?.obj ?? null;
120
+ }
121
+
122
+ function createHeadTracker(root, { forward = new THREE.Vector3(0, 0, 1), up = new THREE.Vector3(0, 1, 0) } = {}) {
123
+ const headBone = findHeadBone(root);
124
+ if (!headBone) return null;
125
+
126
+ // Calibrate against the pose at registration (rest pose right after load):
127
+ // capture which bone-local directions correspond to the model's world
128
+ // forward/up so animated bone rotations carry them correctly afterwards.
129
+ root.updateMatrixWorld(true);
130
+ const restQuat = headBone.getWorldQuaternion(new THREE.Quaternion()).invert();
131
+ const forwardLocal = forward.clone().applyQuaternion(restQuat).normalize();
132
+ const upLocal = up.clone().applyQuaternion(restQuat).normalize();
133
+
134
+ const worldQuat = new THREE.Quaternion();
135
+ const state = {
136
+ forward: new THREE.Vector3(0, 0, 1),
137
+ position: new THREE.Vector3(),
138
+ up: new THREE.Vector3(0, 1, 0),
139
+ };
140
+
141
+ return {
142
+ headBone,
143
+ state,
144
+ update() {
145
+ headBone.getWorldQuaternion(worldQuat);
146
+ headBone.getWorldPosition(state.position);
147
+ state.forward.copy(forwardLocal).applyQuaternion(worldQuat).normalize();
148
+ state.up.copy(upLocal).applyQuaternion(worldQuat).normalize();
149
+ },
150
+ };
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Main factory
155
+ // ---------------------------------------------------------------------------
156
+
157
+ export function createCharacterRenderPasses({
158
+ renderer,
159
+ scene,
160
+ camera,
161
+ depthPrepass = true,
162
+ selfShadow = {},
163
+ } = {}) {
164
+ if (!renderer || !scene || !camera) {
165
+ throw new Error('createCharacterRenderPasses requires { renderer, scene, camera }.');
166
+ }
167
+
168
+ const selfShadowOptions = { ...DEFAULT_SELF_SHADOW_OPTIONS, ...(selfShadow === false ? { enabled: false } : selfShadow) };
169
+
170
+ const registered = [];
171
+ let characterMaskEnabled = false;
172
+
173
+ // Pass targets are consumed through float color attachments. Depth buffers
174
+ // still gate rasterization, but shader reads use the color targets.
175
+
176
+ // ---- Scene depth prepass ----
177
+ const drawingBufferSize = renderer.getDrawingBufferSize(new THREE.Vector2());
178
+ const depthTarget = new THREE.WebGLRenderTarget(drawingBufferSize.x, drawingBufferSize.y, {
179
+ depthBuffer: true,
180
+ magFilter: THREE.NearestFilter,
181
+ minFilter: THREE.NearestFilter,
182
+ stencilBuffer: false,
183
+ });
184
+ depthTarget.texture.name = 'ToonScenePrepass.Color';
185
+ depthTarget.depthTexture = new THREE.DepthTexture(drawingBufferSize.x, drawingBufferSize.y);
186
+ depthTarget.depthTexture.name = 'ToonScenePrepass.Depth';
187
+ depthTarget.depthTexture.type = THREE.UnsignedIntType;
188
+ depthTarget.texture.type = THREE.FloatType;
189
+ const depthOverrideMaterial = createPassDepthColorMaterial();
190
+ const depthResolution = new THREE.Vector2(drawingBufferSize.x, drawingBufferSize.y);
191
+
192
+ // ---- Character self-shadow map ----
193
+ const shadowTarget = new THREE.WebGLRenderTarget(selfShadowOptions.mapSize, selfShadowOptions.mapSize, {
194
+ depthBuffer: true,
195
+ magFilter: THREE.NearestFilter,
196
+ minFilter: THREE.NearestFilter,
197
+ stencilBuffer: false,
198
+ });
199
+ shadowTarget.texture.name = 'ToonCharSelfShadow.Color';
200
+ shadowTarget.depthTexture = new THREE.DepthTexture(selfShadowOptions.mapSize, selfShadowOptions.mapSize);
201
+ shadowTarget.depthTexture.name = 'ToonCharSelfShadow.Depth';
202
+ shadowTarget.depthTexture.type = THREE.UnsignedIntType;
203
+ shadowTarget.texture.type = THREE.FloatType;
204
+ const shadowCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.01, 10);
205
+ const shadowMatrix = new THREE.Matrix4();
206
+ const shadowLightDirection = new THREE.Vector3(0, 1, 0);
207
+
208
+ // ---- Average shadow measurement ----
209
+ const measureTarget = new THREE.WebGLRenderTarget(MAX_MEASURE_SLOTS, 1, {
210
+ depthBuffer: false,
211
+ stencilBuffer: false,
212
+ });
213
+ measureTarget.texture.name = 'ToonAverageShadowMeasure';
214
+ measureTarget.texture.minFilter = THREE.NearestFilter;
215
+ measureTarget.texture.magFilter = THREE.NearestFilter;
216
+ const measureScene = new THREE.Scene();
217
+ const measureCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
218
+ const measureMesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), null);
219
+ measureMesh.frustumCulled = false;
220
+ measureScene.add(measureMesh);
221
+
222
+ // Same 9-jitter average over each character bounding sphere, sampled from
223
+ // the shared sun-shadow pass. The output feeds vec2(slot, 0.5) lookups, so
224
+ // RT row orientation is irrelevant.
225
+ const measureJitterOffsets = [
226
+ [0.0, 0.0, 0.0], [0.7, 0.3, 0.0], [-0.7, 0.3, 0.0],
227
+ [0.0, 0.7, 0.5], [0.0, 0.7, -0.5], [0.5, -0.5, 0.5],
228
+ [-0.5, -0.5, -0.5], [0.4, -0.2, -0.6], [-0.4, 1.0, 0.2],
229
+ ];
230
+ const measureNodeUniforms = {
231
+ sphereCount: uniform(0, 'int'),
232
+ sphereData: uniformArray(
233
+ Array.from({ length: MAX_MEASURE_SLOTS }, () => new THREE.Vector4()),
234
+ 'vec4',
235
+ ),
236
+ };
237
+ let measureNodeMaterial = null;
238
+ function ensureMeasureNodeMaterial() {
239
+ if (measureNodeMaterial) return measureNodeMaterial;
240
+ measureNodeMaterial = new NodeMaterial();
241
+ measureNodeMaterial.name = 'ToonAverageShadowMeasureNode';
242
+ measureNodeMaterial.depthTest = false;
243
+ measureNodeMaterial.depthWrite = false;
244
+ measureNodeMaterial.fragmentNode = Fn(() => {
245
+ const slot = int(uv().x.mul(MAX_MEASURE_SLOTS).floor()).toVar();
246
+ const result = float(1.0).toVar();
247
+ If(slot.lessThan(measureNodeUniforms.sphereCount), () => {
248
+ const sphere = measureNodeUniforms.sphereData.element(slot);
249
+ const visibility = float(0.0).toVar();
250
+ for (const [x, y, z] of measureJitterOffsets) {
251
+ visibility.addAssign(sampleEnvironmentSunShadow(
252
+ sphere.xyz.add(vec3(x, y, z).mul(sphere.w).mul(0.6)),
253
+ ));
254
+ }
255
+ result.assign(visibility.div(measureJitterOffsets.length));
256
+ });
257
+ return vec4(vec3(result), 1.0);
258
+ })();
259
+ return measureNodeMaterial;
260
+ }
261
+
262
+ // ---- Character mask (for character-aware bloom) ----
263
+ let maskTarget = null;
264
+
265
+ const boundsBox = new THREE.Box3();
266
+ const meshBox = new THREE.Box3();
267
+ const boundsSphere = new THREE.Sphere();
268
+ const tempVector = new THREE.Vector3();
269
+ const cameraQuat = new THREE.Quaternion();
270
+ const visibilityRestore = [];
271
+
272
+ let cachedDirectionalLight = null;
273
+
274
+ function findDirectionalLight() {
275
+ if (cachedDirectionalLight?.parent && cachedDirectionalLight.visible) return cachedDirectionalLight;
276
+ cachedDirectionalLight = null;
277
+ scene.traverse((obj) => {
278
+ if (cachedDirectionalLight) return;
279
+ if (obj.isDirectionalLight && obj.visible) cachedDirectionalLight = obj;
280
+ });
281
+ return cachedDirectionalLight;
282
+ }
283
+
284
+ function registerCharacterRoot(root, { headBoneForward, headBoneUp, headCenterOffset = 0 } = {}) {
285
+ if (!root) return null;
286
+ const materials = collectToonMaterials(root);
287
+ if (materials.length === 0) return null;
288
+
289
+ // Node materials have the pass subgraphs uniform-gated and read the
290
+ // passes' float color targets, so registration no longer mutates
291
+ // per-material defines.
292
+
293
+ const characterMeshes = [];
294
+ const shadowSwaps = [];
295
+ root.traverse((obj) => {
296
+ // Outline hulls and fur shells are derived geometry: they must not cast
297
+ // into the self-shadow map or appear in the character mask twice.
298
+ if (!obj.isMesh || obj.userData?.isToonOutline || obj.userData?.isToonFurShell) return;
299
+ obj.layers.enable(TOON_CHARACTER_LAYER);
300
+ characterMeshes.push(obj);
301
+
302
+ // Per-mesh depth materials that honor the source cutout so hair cards
303
+ // cast card-shaped shadows instead of solid quads.
304
+ const buildDepthMaterial = (mat) => {
305
+ const cutoff = mat?.uniforms?.aCutoff?.value ?? -1;
306
+ const baseMap = mat?.uniforms?.base?.value ?? null;
307
+ const params = {
308
+ alphaTest: cutoff > 0 ? cutoff : 0,
309
+ map: cutoff > 0 ? baseMap : null,
310
+ side: THREE.DoubleSide,
311
+ };
312
+ return createPassDepthColorMaterial(params);
313
+ };
314
+ const depthMaterial = Array.isArray(obj.material)
315
+ ? obj.material.map(buildDepthMaterial)
316
+ : buildDepthMaterial(obj.material);
317
+ // Per-mesh PassBasicNodeMaterial swaps keep MMD-scale skeletons on the
318
+ // same storage-skinning path as the depth swaps.
319
+ const buildMaskMaterial = () => {
320
+ const material = new PassBasicNodeMaterial();
321
+ material.color.set(0xffffff);
322
+ material.side = THREE.DoubleSide;
323
+ return material;
324
+ };
325
+ const maskMaterial = Array.isArray(obj.material) ? obj.material.map(buildMaskMaterial) : buildMaskMaterial();
326
+ shadowSwaps.push({ depthMaterial, maskMaterial, mesh: obj });
327
+ });
328
+
329
+ const headTracker = createHeadTracker(root, {
330
+ forward: headBoneForward ? new THREE.Vector3(...headBoneForward) : undefined,
331
+ up: headBoneUp ? new THREE.Vector3(...headBoneUp) : undefined,
332
+ });
333
+
334
+ const entry = {
335
+ characterMeshes,
336
+ headCenterOffset,
337
+ headTracker,
338
+ materials,
339
+ root,
340
+ shadowSwaps,
341
+ slot: registered.length % MAX_MEASURE_SLOTS,
342
+ };
343
+ registered.push(entry);
344
+ return entry;
345
+ }
346
+
347
+ function unregisterCharacterRoot(root) {
348
+ const index = registered.findIndex((entry) => entry.root === root);
349
+ if (index >= 0) registered.splice(index, 1);
350
+ }
351
+
352
+ function computeCharacterBounds() {
353
+ boundsBox.makeEmpty();
354
+ for (const entry of registered) {
355
+ for (const mesh of entry.characterMeshes) {
356
+ if (!mesh.visible) continue;
357
+ // computeBoundingBox on skinned geometry ignores the pose; expand the
358
+ // static box generously instead of paying per-vertex skinning on CPU.
359
+ meshBox.setFromObject(mesh);
360
+ if (!meshBox.isEmpty()) boundsBox.union(meshBox);
361
+ }
362
+ }
363
+ if (boundsBox.isEmpty()) return false;
364
+ boundsBox.getBoundingSphere(boundsSphere);
365
+ boundsSphere.radius *= 1.15;
366
+ return true;
367
+ }
368
+
369
+ function renderDepthPrepass(anyMaterials) {
370
+ const needed = anyUniform(anyMaterials, (u) => (
371
+ (u.rimLightMode?.value === 1 && u.useRimLight?.value) || (u.contactShadowStrength?.value ?? 0) > 0
372
+ ));
373
+ if (!needed) {
374
+ setUniform(anyMaterials, 'sceneDepthReady', false);
375
+ return;
376
+ }
377
+
378
+ // Outline hulls and fully transparent meshes must not write prepass depth:
379
+ // the hull would put a rim around itself, and a transparent water plane
380
+ // would occlude the swimmer's rim.
381
+ visibilityRestore.length = 0;
382
+ scene.traverse((obj) => {
383
+ if (!obj.isMesh || !obj.visible) return;
384
+ const materials = toMaterialArray(obj.material);
385
+ // Fur shells discard by noise in their own shader; the override depth
386
+ // material would write them as solid geometry, so keep them out too.
387
+ const skip = obj.userData?.isToonOutline ||
388
+ obj.userData?.isToonFurShell ||
389
+ (materials.length > 0 && materials.every((mat) => mat?.transparent === true));
390
+ if (skip) {
391
+ obj.visible = false;
392
+ visibilityRestore.push(obj);
393
+ }
394
+ });
395
+
396
+ const previousOverride = scene.overrideMaterial;
397
+ const previousTarget = renderer.getRenderTarget();
398
+ scene.overrideMaterial = depthOverrideMaterial;
399
+ renderer.setRenderTarget(depthTarget);
400
+ renderer.clear();
401
+ renderer.render(scene, camera);
402
+ renderer.setRenderTarget(previousTarget);
403
+ scene.overrideMaterial = previousOverride;
404
+
405
+ for (const obj of visibilityRestore) obj.visible = true;
406
+ visibilityRestore.length = 0;
407
+
408
+ setUniform(anyMaterials, 'sceneDepthReady', true);
409
+ setUniform(anyMaterials, 'sceneDepthTexture', depthTarget.texture);
410
+ setUniform(anyMaterials, 'sceneDepthResolution', depthResolution);
411
+ setUniform(anyMaterials, 'cameraNearPlane', camera.near);
412
+ setUniform(anyMaterials, 'cameraFarPlane', camera.far);
413
+ setUniform(anyMaterials, 'cameraProjection11', camera.projectionMatrix.elements[5]);
414
+ }
415
+
416
+ function renderSelfShadow(anyMaterials) {
417
+ const needed = selfShadowOptions.enabled &&
418
+ anyUniform(anyMaterials, (u) => u.selfShadowSourceMode?.value === 2 && (u.selfShadowStrength?.value ?? 0) >= 0);
419
+ if (!needed || !computeCharacterBounds()) {
420
+ setUniform(anyMaterials, 'charSelfShadowReady', false);
421
+ return;
422
+ }
423
+
424
+ // Shadow direction: scene main light, or an art-directed camera-relative
425
+ // angle (classic anime trick — face shadows stay pretty from any view).
426
+ if (selfShadowOptions.directionMode === 'cameraRelative') {
427
+ camera.getWorldQuaternion(cameraQuat);
428
+ shadowLightDirection.set(0, 0, 1)
429
+ .applyAxisAngle(new THREE.Vector3(1, 0, 0), THREE.MathUtils.degToRad(selfShadowOptions.cameraRelativePitch))
430
+ .applyAxisAngle(new THREE.Vector3(0, 1, 0), THREE.MathUtils.degToRad(selfShadowOptions.cameraRelativeYaw))
431
+ .applyQuaternion(cameraQuat)
432
+ .normalize();
433
+ } else {
434
+ const light = findDirectionalLight();
435
+ if (light) {
436
+ shadowLightDirection.copy(light.position).sub(light.target?.position ?? tempVector.set(0, 0, 0)).normalize();
437
+ } else {
438
+ shadowLightDirection.set(0.35, 0.75, 0.55).normalize();
439
+ }
440
+ }
441
+
442
+ // Tight-fit orthographic camera around only the registered characters:
443
+ // all shadow-map texels are spent on the character, which is what makes a
444
+ // 2k map produce crisp hair-on-face shadows.
445
+ const radius = boundsSphere.radius;
446
+ shadowCamera.left = -radius;
447
+ shadowCamera.right = radius;
448
+ shadowCamera.top = radius;
449
+ shadowCamera.bottom = -radius;
450
+ shadowCamera.near = 0.01;
451
+ shadowCamera.far = radius * 2 + 0.1;
452
+ shadowCamera.position.copy(boundsSphere.center).addScaledVector(shadowLightDirection, radius + 0.05);
453
+ shadowCamera.up.set(0, 1, 0);
454
+ if (Math.abs(shadowLightDirection.y) > 0.99) shadowCamera.up.set(0, 0, 1);
455
+ shadowCamera.lookAt(boundsSphere.center);
456
+ shadowCamera.coordinateSystem = renderer.coordinateSystem;
457
+ shadowCamera.updateProjectionMatrix();
458
+ shadowCamera.updateMatrixWorld(true);
459
+ shadowMatrix.multiplyMatrices(shadowCamera.projectionMatrix, shadowCamera.matrixWorldInverse);
460
+ applyShadowClipAdjust(shadowMatrix, renderer);
461
+
462
+ // Swap character materials to cutout-aware depth materials, render only
463
+ // the character layer, restore.
464
+ const restores = [];
465
+ for (const entry of registered) {
466
+ for (const { depthMaterial, mesh } of entry.shadowSwaps ?? []) {
467
+ restores.push({ material: mesh.material, mesh });
468
+ mesh.material = depthMaterial;
469
+ }
470
+ }
471
+
472
+ const previousTarget = renderer.getRenderTarget();
473
+ shadowCamera.layers.set(TOON_CHARACTER_LAYER);
474
+ renderer.setRenderTarget(shadowTarget);
475
+ renderer.clear();
476
+ renderer.render(scene, shadowCamera);
477
+ renderer.setRenderTarget(previousTarget);
478
+
479
+ for (const { material, mesh } of restores) mesh.material = material;
480
+
481
+ setUniform(anyMaterials, 'charSelfShadowReady', true);
482
+ setUniform(anyMaterials, 'charSelfShadowMap', shadowTarget.texture);
483
+ setUniform(anyMaterials, 'charSelfShadowMatrix', shadowMatrix);
484
+ setUniform(anyMaterials, 'charSelfShadowLightDirection', shadowLightDirection);
485
+ setUniform(anyMaterials, 'charSelfShadowTexelSize', 1 / selfShadowOptions.mapSize);
486
+ setUniform(anyMaterials, 'charSelfShadowQuality', selfShadowOptions.quality);
487
+ setUniform(anyMaterials, 'charSelfShadowSharpen', selfShadowOptions.sharpen);
488
+ setUniform(anyMaterials, 'charSelfShadowNormalBias', selfShadowOptions.normalBias);
489
+ setUniform(anyMaterials, 'charSelfShadowDepthBias', selfShadowOptions.depthBias);
490
+ setUniform(anyMaterials, 'charSelfShadowFadeDistance', selfShadowOptions.fadeDistance);
491
+ setUniform(anyMaterials, 'charSelfShadowNdotLFix', selfShadowOptions.ndotLFix);
492
+ }
493
+
494
+ function renderAverageShadowMeasure(anyMaterials) {
495
+ const shadowSourceReady = environmentSunShadow.ready.value === true;
496
+ const needed = shadowSourceReady &&
497
+ anyUniform(anyMaterials, (u) => (u.averageShadowMeasuredBlend?.value ?? 0) > 0);
498
+ if (!needed) {
499
+ setUniform(anyMaterials, 'averageShadowMeasureReady', false);
500
+ return;
501
+ }
502
+
503
+ const sphereData = measureNodeUniforms.sphereData.array;
504
+ let count = 0;
505
+ for (const entry of registered) {
506
+ if (count >= MAX_MEASURE_SLOTS) break;
507
+ meshBox.makeEmpty();
508
+ for (const mesh of entry.characterMeshes) {
509
+ if (mesh.visible) meshBox.expandByObject(mesh);
510
+ }
511
+ if (meshBox.isEmpty()) continue;
512
+ meshBox.getBoundingSphere(boundsSphere);
513
+ entry.slot = count;
514
+ sphereData[count].set(boundsSphere.center.x, boundsSphere.center.y, boundsSphere.center.z, boundsSphere.radius);
515
+ count += 1;
516
+ }
517
+ if (count === 0) {
518
+ setUniform(anyMaterials, 'averageShadowMeasureReady', false);
519
+ return;
520
+ }
521
+
522
+ measureNodeUniforms.sphereCount.value = count;
523
+ measureMesh.material = ensureMeasureNodeMaterial();
524
+
525
+ const previousTarget = renderer.getRenderTarget();
526
+ renderer.setRenderTarget(measureTarget);
527
+ renderer.render(measureScene, measureCamera);
528
+ renderer.setRenderTarget(previousTarget);
529
+
530
+ for (const entry of registered) {
531
+ setUniform(entry.materials, 'averageShadowMeasureReady', true);
532
+ setUniform(entry.materials, 'averageShadowMeasureTexture', measureTarget.texture);
533
+ setUniform(entry.materials, 'averageShadowMeasureSlot', (entry.slot + 0.5) / MAX_MEASURE_SLOTS);
534
+ }
535
+ }
536
+
537
+ function renderCharacterMask() {
538
+ if (!characterMaskEnabled || registered.length === 0) return null;
539
+ if (!maskTarget) {
540
+ const size = renderer.getDrawingBufferSize(new THREE.Vector2());
541
+ maskTarget = new THREE.WebGLRenderTarget(Math.max(1, size.x >> 1), Math.max(1, size.y >> 1), {
542
+ depthBuffer: true,
543
+ stencilBuffer: false,
544
+ });
545
+ maskTarget.texture.name = 'ToonCharacterMask';
546
+ }
547
+
548
+ const previousTarget = renderer.getRenderTarget();
549
+ const previousOverride = scene.overrideMaterial;
550
+ const previousClearColor = renderer.getClearColor(new THREE.Color());
551
+ const previousClearAlpha = renderer.getClearAlpha();
552
+ const previousBackground = scene.background;
553
+ const previousCameraLayers = camera.layers.mask;
554
+
555
+ scene.background = null;
556
+ // Swap per-mesh mask materials so MMD-scale skeletons keep the
557
+ // storage-skinning path (see registerCharacterRoot).
558
+ const maskSwapRestores = [];
559
+ for (const entry of registered) {
560
+ for (const swap of entry.shadowSwaps) {
561
+ if (!swap.maskMaterial) continue;
562
+ maskSwapRestores.push({ material: swap.mesh.material, mesh: swap.mesh });
563
+ swap.mesh.material = swap.maskMaterial;
564
+ }
565
+ }
566
+ camera.layers.set(TOON_CHARACTER_LAYER);
567
+ renderer.setClearColor(0x000000, 1);
568
+ renderer.setRenderTarget(maskTarget);
569
+ renderer.clear();
570
+ renderer.render(scene, camera);
571
+
572
+ renderer.setRenderTarget(previousTarget);
573
+ renderer.setClearColor(previousClearColor, previousClearAlpha);
574
+ for (const restore of maskSwapRestores) restore.mesh.material = restore.material;
575
+ scene.overrideMaterial = previousOverride;
576
+ scene.background = previousBackground;
577
+ camera.layers.mask = previousCameraLayers;
578
+ return maskTarget.texture;
579
+ }
580
+
581
+ function updateHeadTracking() {
582
+ for (const entry of registered) {
583
+ if (!entry.headTracker) {
584
+ setUniform(entry.materials, 'headDataReady', false);
585
+ continue;
586
+ }
587
+ entry.headTracker.update();
588
+ const { forward, position, up } = entry.headTracker.state;
589
+ if (entry.headCenterOffset) {
590
+ tempVector.copy(position).addScaledVector(up, entry.headCenterOffset);
591
+ } else {
592
+ tempVector.copy(position);
593
+ }
594
+ setUniform(entry.materials, 'headDataReady', true);
595
+ setUniform(entry.materials, 'headPositionWS', tempVector);
596
+ setUniform(entry.materials, 'headForwardWS', forward);
597
+ setUniform(entry.materials, 'headUpWS', up);
598
+ }
599
+ }
600
+
601
+ function update() {
602
+ if (registered.length === 0) return;
603
+ const anyMaterials = registered.flatMap((entry) => entry.materials);
604
+
605
+ updateHeadTracking();
606
+ if (depthPrepass) renderDepthPrepass(anyMaterials);
607
+ renderSelfShadow(anyMaterials);
608
+ renderAverageShadowMeasure(anyMaterials);
609
+ renderCharacterMask();
610
+ }
611
+
612
+ function setSize(width, height, pixelRatio = renderer.getPixelRatio()) {
613
+ const w = Math.max(1, Math.floor(width * pixelRatio));
614
+ const h = Math.max(1, Math.floor(height * pixelRatio));
615
+ depthTarget.setSize(w, h);
616
+ depthResolution.set(w, h);
617
+ if (maskTarget) maskTarget.setSize(Math.max(1, w >> 1), Math.max(1, h >> 1));
618
+ }
619
+
620
+ function dispose() {
621
+ depthTarget.dispose();
622
+ shadowTarget.dispose();
623
+ measureTarget.dispose();
624
+ maskTarget?.dispose();
625
+ depthOverrideMaterial.dispose();
626
+ }
627
+
628
+ return {
629
+ get characterMaskTexture() {
630
+ return maskTarget?.texture ?? null;
631
+ },
632
+ get registeredCount() {
633
+ return registered.length;
634
+ },
635
+ dispose,
636
+ registerCharacterRoot,
637
+ selfShadowCamera: shadowCamera,
638
+ selfShadowOptions,
639
+ setCharacterMaskEnabled(enabled) {
640
+ characterMaskEnabled = Boolean(enabled);
641
+ },
642
+ setSize,
643
+ unregisterCharacterRoot,
644
+ update,
645
+ };
646
+ }
@@ -0,0 +1,4 @@
1
+ // Character toon-shading barrel. Import from '@call-me-sensei/toonlab/toon'.
2
+ export * from './toonMaterialAdapter.js';
3
+ export * from './toonSettings.js';
4
+ export * from './characterRenderPasses.js';