@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,318 @@
1
+ import * as THREE from 'three';
2
+
3
+ import { createEnvironmentAoOverlayNodeMaterial } from '../shaders-tsl/environment-ao-overlay.js';
4
+ import {
5
+ createEnvironmentNodeMaterial,
6
+ environmentSharedUniformNodes,
7
+ } from '../shaders-tsl/environment.js';
8
+ import {
9
+ isEmissiveEnvironmentMaterial,
10
+ isFoliageMaterial,
11
+ isWindowCutoutMaterial,
12
+ materialBaseColor,
13
+ sourceOpacity,
14
+ usesAlphaCutout,
15
+ } from './environmentMaterialClassifier.js';
16
+ import {
17
+ ENVIRONMENT_DEBUG_MODES,
18
+ applyEnvironmentSettingsToMaterial,
19
+ createEnvironmentSettings,
20
+ normalizeEnvironmentDebugMode,
21
+ } from './environmentSettings.js';
22
+ import {
23
+ fallbackEnvironmentBlackTexture,
24
+ fallbackEnvironmentWhiteTexture,
25
+ } from './environmentTextureResolver.js';
26
+
27
+ // Uniform objects shared by reference across every environment material, so a
28
+ // single write drives the whole converted scene: the animation clock, the
29
+ // drifting cloud-shadow field, the debug selector, the interior openings, the
30
+ // ambient probe, and the floor reflection pass output. These names are
31
+ // excluded from the per-material settings snapshot (see
32
+ // ENVIRONMENT_SHARED_UNIFORM_NAMES).
33
+ const environmentSharedUniforms = {
34
+ time: { value: 0 },
35
+ cloudShadowStrength: { value: 0 },
36
+ cloudShadowCoverage: { value: 0.45 },
37
+ cloudShadowScale: { value: 0.012 },
38
+ cloudShadowVelocity: { value: new THREE.Vector2(0.02, 0.006) },
39
+ envDebugMode: { value: 0 },
40
+ environmentOpenings: {
41
+ value: [new THREE.Vector4(), new THREE.Vector4(), new THREE.Vector4(), new THREE.Vector4()],
42
+ },
43
+ environmentOpeningCount: { value: 0 },
44
+ ambientProbe: {
45
+ value: [
46
+ new THREE.Color(1, 1, 1), new THREE.Color(1, 1, 1),
47
+ new THREE.Color(1, 1, 1), new THREE.Color(1, 1, 1),
48
+ new THREE.Color(1, 1, 1), new THREE.Color(1, 1, 1),
49
+ ],
50
+ },
51
+ planarReflectionMap: { value: fallbackEnvironmentBlackTexture },
52
+ planarReflectionMatrix: { value: new THREE.Matrix4() },
53
+ };
54
+
55
+ // The module-level setters below write the classic shared uniforms and
56
+ // mirror each write into the TSL shared uniform nodes
57
+ // (src/shaders-tsl/environment.js), so scene-wide updates reach both
58
+ // backends. Per-material writes (applyEnvironmentSettingsToMaterial, HUD)
59
+ // need no mirroring: on TSL the material `.uniforms` slots ARE the shared
60
+ // nodes, exactly like the classic shared-by-reference uniform objects.
61
+
62
+ // Advance the shared environment clock once per frame (drives cloud drift).
63
+ export function advanceEnvironmentShaderTime(delta) {
64
+ environmentSharedUniforms.time.value += Math.min(Math.max(delta ?? 0, 0), 0.1);
65
+ environmentSharedUniformNodes.time.value = environmentSharedUniforms.time.value;
66
+ }
67
+
68
+ // Reset the shared clock (deterministic captures need a repeatable time).
69
+ export function resetEnvironmentShaderTime(value = 0) {
70
+ environmentSharedUniforms.time.value = Number.isFinite(value) ? value : 0;
71
+ environmentSharedUniformNodes.time.value = environmentSharedUniforms.time.value;
72
+ }
73
+
74
+ // Retune the scene-wide cloud shadows. strength 0 (the default) disables the
75
+ // effect, so indoor scenes never see it. velocity is uv-space drift per
76
+ // second (worldDrift = velocity / scale).
77
+ export function setEnvironmentCloudShadow({ strength, coverage, scale, velocity } = {}) {
78
+ if (Number.isFinite(strength)) environmentSharedUniforms.cloudShadowStrength.value = strength;
79
+ if (Number.isFinite(coverage)) environmentSharedUniforms.cloudShadowCoverage.value = coverage;
80
+ if (Number.isFinite(scale)) environmentSharedUniforms.cloudShadowScale.value = scale;
81
+ if (velocity) {
82
+ environmentSharedUniforms.cloudShadowVelocity.value.set(
83
+ velocity[0] ?? velocity.x ?? 0, velocity[1] ?? velocity.y ?? 0);
84
+ }
85
+ environmentSharedUniformNodes.cloudShadowStrength.value = environmentSharedUniforms.cloudShadowStrength.value;
86
+ environmentSharedUniformNodes.cloudShadowCoverage.value = environmentSharedUniforms.cloudShadowCoverage.value;
87
+ environmentSharedUniformNodes.cloudShadowScale.value = environmentSharedUniforms.cloudShadowScale.value;
88
+ environmentSharedUniformNodes.cloudShadowVelocity.value.copy(environmentSharedUniforms.cloudShadowVelocity.value);
89
+ }
90
+
91
+ function forEachEnvironmentShaderMaterial(root, callback) {
92
+ root?.traverse?.((obj) => {
93
+ if (!obj.isMesh || !obj.material) return;
94
+ const materials = Array.isArray(obj.material) ? obj.material : [obj.material];
95
+ for (const mat of materials) {
96
+ if (mat?.userData?.environmentMaterial) callback(mat, obj);
97
+ }
98
+ });
99
+ }
100
+
101
+ // Switch every converted environment material under root to a debug view
102
+ // ('off' restores normal rendering). Materials compile the debug branch out
103
+ // unless a debug view has been requested at least once.
104
+ export function setEnvironmentDebugOutput(root, mode = 'off') {
105
+ const resolved = normalizeEnvironmentDebugMode(mode);
106
+ environmentSharedUniforms.envDebugMode.value = resolved;
107
+ // Node materials compile the debug table in from the start (and pre-set the
108
+ // define marker), so on the TSL backend this stays a pure uniform write.
109
+ environmentSharedUniformNodes.envDebugMode.value = resolved;
110
+ forEachEnvironmentShaderMaterial(root, (mat) => {
111
+ if (resolved > 0 && !mat.defines?.ENV_DEBUG_VIEWS) {
112
+ mat.defines = { ...mat.defines, ENV_DEBUG_VIEWS: 1 };
113
+ mat.needsUpdate = true;
114
+ }
115
+ });
116
+ return resolved;
117
+ }
118
+
119
+ // Register up to four interior "openings" (windows, doors, strong lamps):
120
+ // world position + light reach radius. Interior occlusion darkens surfaces
121
+ // away from every opening; passing an empty list disables the term.
122
+ export function setEnvironmentOpenings(openings = []) {
123
+ const slots = environmentSharedUniforms.environmentOpenings.value;
124
+ const count = Math.min(Array.isArray(openings) ? openings.length : 0, slots.length);
125
+ for (let i = 0; i < slots.length; i += 1) {
126
+ const opening = openings[i];
127
+ if (i < count && opening) {
128
+ const position = opening.position ?? opening;
129
+ slots[i].set(
130
+ position.x ?? position[0] ?? 0,
131
+ position.y ?? position[1] ?? 0,
132
+ position.z ?? position[2] ?? 0,
133
+ opening.radius ?? opening.w ?? 1,
134
+ );
135
+ } else {
136
+ slots[i].set(0, 0, 0, 0);
137
+ }
138
+ }
139
+ environmentSharedUniforms.environmentOpeningCount.value = count;
140
+ for (let i = 0; i < slots.length; i += 1) {
141
+ environmentSharedUniformNodes.environmentOpenings.array[i].copy(slots[i]);
142
+ }
143
+ environmentSharedUniformNodes.environmentOpeningCount.value = count;
144
+ return count;
145
+ }
146
+
147
+ // Write the six-direction ambient probe (+x, -x, +y, -y, +z, -z). Colors
148
+ // come from captureEnvironmentAmbientProbe or can be authored directly.
149
+ export function setEnvironmentAmbientProbeColors(colors = []) {
150
+ const slots = environmentSharedUniforms.ambientProbe.value;
151
+ for (let i = 0; i < slots.length; i += 1) {
152
+ const color = colors[i];
153
+ if (color?.isColor) slots[i].copy(color);
154
+ else if (Array.isArray(color)) slots[i].setRGB(color[0] ?? 1, color[1] ?? 1, color[2] ?? 1);
155
+ environmentSharedUniformNodes.ambientProbe.array[i].copy(slots[i]);
156
+ }
157
+ }
158
+
159
+ // Bind (or clear) the shared planar-reflection pass output consumed by
160
+ // glossFloor-role materials.
161
+ export function setEnvironmentPlanarReflection({ texture, matrix } = {}) {
162
+ environmentSharedUniforms.planarReflectionMap.value = texture ?? fallbackEnvironmentBlackTexture;
163
+ if (matrix) environmentSharedUniforms.planarReflectionMatrix.value.copy(matrix);
164
+ environmentSharedUniformNodes.planarReflectionMap.value = environmentSharedUniforms.planarReflectionMap.value;
165
+ environmentSharedUniformNodes.planarReflectionMatrix.value.copy(environmentSharedUniforms.planarReflectionMatrix.value);
166
+ }
167
+
168
+ export function createEnvironmentAoMaterial(mat, textureSet) {
169
+ const material = createEnvironmentAoOverlayNodeMaterial({
170
+ aoMap: textureSet.alphaMap ?? textureSet.baseMap ?? fallbackEnvironmentWhiteTexture,
171
+ side: mat?.side ?? THREE.DoubleSide,
172
+ });
173
+ material.name = mat?.name ?? '';
174
+ material.visible = mat?.visible ?? true;
175
+ material.userData.environmentAoOverlay = true;
176
+ return material;
177
+ }
178
+
179
+ export function createEnvironmentShadowMaterial(mat, { hasVertexColors = false } = {}) {
180
+ const material = new THREE.MeshBasicMaterial({
181
+ color: 0x3d2d20,
182
+ opacity: 0.18,
183
+ side: mat?.side ?? THREE.DoubleSide,
184
+ transparent: true,
185
+ vertexColors: hasVertexColors,
186
+ depthWrite: false,
187
+ });
188
+ material.name = mat?.name ?? '';
189
+ material.visible = mat?.visible ?? true;
190
+ material.userData.environmentShadow = true;
191
+ return material;
192
+ }
193
+
194
+ export function createEnvironmentWindowOpeningMaterial(mat) {
195
+ const material = new THREE.MeshBasicMaterial({
196
+ color: 0x000000,
197
+ opacity: 0,
198
+ side: mat?.side ?? THREE.DoubleSide,
199
+ transparent: true,
200
+ depthWrite: false,
201
+ });
202
+ material.name = mat?.name ?? '';
203
+ material.visible = false;
204
+ material.userData.windowCutout = true;
205
+ return material;
206
+ }
207
+
208
+ function alphaCutoffFor(mat, isFoliage) {
209
+ if (isFoliage) return Math.max(mat?.alphaTest ?? 0.12, 0.08);
210
+ return usesAlphaCutout(mat) ? Math.max(mat?.alphaTest ?? 0.35, 0.35) : -1.0;
211
+ }
212
+
213
+ export function createEnvironmentMaterial(mat, textureSet, {
214
+ environmentBox = null,
215
+ environmentSettings = createEnvironmentSettings(),
216
+ hasSun = false,
217
+ hasUv2 = false,
218
+ hasVertexAo = false,
219
+ hasVertexColors = false,
220
+ openWindows = false,
221
+ role = null,
222
+ } = {}) {
223
+ const features = environmentSettings.features;
224
+ const isFoliage = role ? role === 'foliage' : isFoliageMaterial(mat);
225
+ const isEmissive = role ? role === 'emissive' : isEmissiveEnvironmentMaterial(mat);
226
+ const isWindow = role ? role === 'window' : isWindowCutoutMaterial(mat);
227
+ const isGlossFloor = role === 'glossFloor';
228
+ const alphaCutout = usesAlphaCutout(mat);
229
+ const alphaBlend = mat?.transparent === true && sourceOpacity(mat) < 0.999;
230
+ const windowCutout = features.windowCutout && openWindows && isWindow;
231
+ if (windowCutout) return createEnvironmentWindowOpeningMaterial(mat);
232
+
233
+ const useNormalMap = Boolean(textureSet.normalMap) && features.normalMap;
234
+ const useAoMap = Boolean(textureSet.aoMap) && features.aoMap;
235
+ const useLightMap = Boolean(textureSet.lightMap) && features.lightMap;
236
+ const useEmissiveMap = Boolean(textureSet.emissiveMap) && features.emissiveMap;
237
+ const useUntextured = Boolean(textureSet.untextured) && features.untexturedGradient;
238
+ const usePlanarReflection = isGlossFloor && features.planarReflection;
239
+ const aoUsesUv2 = hasUv2 && (textureSet.aoMap?.channel ?? 1) >= 1;
240
+ const lightUsesUv2 = hasUv2 && (textureSet.lightMap?.channel ?? 1) >= 1;
241
+ const useUv2 = (useAoMap && aoUsesUv2) || (useLightMap && lightUsesUv2);
242
+
243
+ const material = createEnvironmentNodeMaterial({
244
+ alphaBlend,
245
+ alphaCutoff: alphaCutoffFor(mat, isFoliage),
246
+ baseColor: materialBaseColor(mat, textureSet),
247
+ environmentBox,
248
+ flags: {
249
+ aoUsesUv2: useUv2 && aoUsesUv2,
250
+ hasAlphaMap: Boolean(textureSet.alphaMap),
251
+ hasAoMap: useAoMap,
252
+ hasEmissiveMap: useEmissiveMap,
253
+ hasLightMap: useLightMap,
254
+ hasNormalMap: useNormalMap,
255
+ hasPackedMap: Boolean(textureSet.packedMap),
256
+ hasPlanarReflection: usePlanarReflection,
257
+ hasUntextured: useUntextured,
258
+ hasVertexAo,
259
+ lightUsesUv2: useUv2 && lightUsesUv2,
260
+ useUv2,
261
+ useVertexColors: hasVertexColors,
262
+ },
263
+ hasSun,
264
+ isEmissive,
265
+ isFoliage,
266
+ isGlossFloor,
267
+ opacity: sourceOpacity(mat),
268
+ side: mat?.side ?? THREE.DoubleSide,
269
+ textureSet,
270
+ });
271
+
272
+ const baseMapTexture = textureSet.baseMap;
273
+ if (baseMapTexture?.isTexture) {
274
+ if (baseMapTexture.matrixAutoUpdate) baseMapTexture.updateMatrix();
275
+ material.uniforms.baseMapTransform.value.copy(baseMapTexture.matrix);
276
+ }
277
+
278
+ material.name = mat?.name ?? '';
279
+ material.visible = mat?.visible ?? true;
280
+ material.userData.windowCutout = windowCutout;
281
+ material.userData.environmentMaterial = true;
282
+ material.userData.environmentRole = role ?? (isFoliage ? 'foliage' : isEmissive ? 'emissive' : 'standard');
283
+ material.userData.environmentFeatures = { ...features };
284
+ return applyEnvironmentSettingsToMaterial(material, environmentSettings);
285
+ }
286
+
287
+ export function createDebugEnvironmentMaterial(mat, shaderMode, textureSet, { hasVertexColors = false } = {}) {
288
+ if (shaderMode === 'basic') {
289
+ const material = new THREE.MeshBasicMaterial({
290
+ alphaTest: usesAlphaCutout(mat) ? Math.max(mat?.alphaTest ?? 0.35, 0.35) : mat?.alphaTest ?? 0,
291
+ color: materialBaseColor(mat, textureSet),
292
+ map: textureSet.baseMap === fallbackEnvironmentWhiteTexture ? null : textureSet.baseMap,
293
+ opacity: sourceOpacity(mat),
294
+ side: mat?.side ?? THREE.DoubleSide,
295
+ transparent: mat?.transparent === true && sourceOpacity(mat) < 0.999,
296
+ vertexColors: hasVertexColors,
297
+ });
298
+ material.visible = mat?.visible ?? true;
299
+ return material;
300
+ }
301
+
302
+ const material = new THREE.MeshStandardMaterial({
303
+ alphaTest: usesAlphaCutout(mat) ? Math.max(mat?.alphaTest ?? 0.35, 0.35) : mat?.alphaTest ?? 0,
304
+ color: materialBaseColor(mat, textureSet),
305
+ map: textureSet.baseMap === fallbackEnvironmentWhiteTexture ? null : textureSet.baseMap,
306
+ normalMap: mat?.normalMap ?? null,
307
+ roughness: 0.82,
308
+ metalness: 0.02,
309
+ opacity: sourceOpacity(mat),
310
+ side: mat?.side ?? THREE.DoubleSide,
311
+ transparent: mat?.transparent === true && sourceOpacity(mat) < 0.999,
312
+ vertexColors: hasVertexColors,
313
+ });
314
+ material.visible = mat?.visible ?? true;
315
+ return material;
316
+ }
317
+
318
+ export { ENVIRONMENT_DEBUG_MODES };
@@ -0,0 +1,268 @@
1
+ // Sun shadow pass for the node backends (WebGPU / forced WebGL2).
2
+ //
3
+ // The classic pipeline gets scene shadows from three's shadow-map system via
4
+ // materials with lights:true; the TSL toon/environment materials never use
5
+ // the node lighting system, so nothing would render the sun's shadow map.
6
+ // This pass renders the scene from the shadow-casting directional light's
7
+ // own shadow camera (the sun rig configures its bounds) into a float color
8
+ // target holding linear window depth, then publishes map/matrix through the
9
+ // shared uniforms in src/shaders-tsl/chunks/environment-sun-shadow.js, which
10
+ // both the environment material (getShadowMask replacement) and the toon
11
+ // character (sceneShadowVisibility) sample.
12
+ //
13
+ // Classic WebGL path: unused — three's own shadow system keeps doing the job.
14
+
15
+ import * as THREE from 'three';
16
+
17
+ import {
18
+ applyShadowClipAdjust,
19
+ createPassDepthColorMaterial,
20
+ } from '../shaders-tsl/chunks/pass-depth-color.js';
21
+ import { environmentSunShadow, sunShadowMapNode } from '../shaders-tsl/chunks/environment-sun-shadow.js';
22
+
23
+ export function createEnvironmentSunShadowPass({ renderer, scene } = {}) {
24
+ if (!renderer || !scene) {
25
+ throw new Error('createEnvironmentSunShadowPass requires { renderer, scene }.');
26
+ }
27
+
28
+ const isNodeBackend = Boolean(renderer.isWebGPURenderer);
29
+ let shadowTarget = null;
30
+ const shadowMatrix = new THREE.Matrix4();
31
+ const depthMaterialCache = new WeakMap();
32
+ const skinnedDepthMaterialCache = new WeakMap();
33
+ // Swapped material (or material ARRAY) per mesh, keyed by the source
34
+ // material identity: handing the renderer a fresh array every frame churns
35
+ // its render-object cache, destroying object uniform buffers that queued
36
+ // submits still reference (WebGPU validation error).
37
+ const meshSwapCache = new WeakMap();
38
+ const materialRestores = [];
39
+ const visibilityRestores = [];
40
+ let cachedSunLight = null;
41
+
42
+ function findShadowSun() {
43
+ if (cachedSunLight?.parent && cachedSunLight.visible && cachedSunLight.castShadow) {
44
+ return cachedSunLight;
45
+ }
46
+ cachedSunLight = null;
47
+ scene.traverse((obj) => {
48
+ if (cachedSunLight) return;
49
+ if (obj.isDirectionalLight && obj.visible && obj.castShadow) cachedSunLight = obj;
50
+ });
51
+ return cachedSunLight;
52
+ }
53
+
54
+ // three's WebGLShadowMap renders shadow depth with flipped culling unless
55
+ // material.shadowSide overrides it (FrontSide casters render their back
56
+ // faces). One-sided planes facing the light — the environments' baked
57
+ // shadow-decal meshes — therefore never occlude in the classic map; the
58
+ // pass must match or those decals black out the sunlit interior.
59
+ const shadowSideFor = {
60
+ [THREE.FrontSide]: THREE.BackSide,
61
+ [THREE.BackSide]: THREE.FrontSide,
62
+ [THREE.DoubleSide]: THREE.DoubleSide,
63
+ };
64
+
65
+ function cacheForDepthMaterial(object) {
66
+ if (!object?.isSkinnedMesh) return depthMaterialCache;
67
+ let cache = skinnedDepthMaterialCache.get(object);
68
+ if (!cache) {
69
+ cache = new WeakMap();
70
+ skinnedDepthMaterialCache.set(object, cache);
71
+ }
72
+ return cache;
73
+ }
74
+
75
+ function depthMaterialFor(material, object = null) {
76
+ const cache = cacheForDepthMaterial(object);
77
+ let depthMaterial = cache.get(material);
78
+ if (!depthMaterial) {
79
+ if (typeof material?.userData?.createDepthColorVariant === 'function') {
80
+ // Materials with custom vertex displacement (tree foliage cards)
81
+ // provide their own cutout depth variant.
82
+ depthMaterial = material.userData.createDepthColorVariant();
83
+ } else {
84
+ // Toon/environment TSL materials expose their cutout through the
85
+ // same-name uniform surface; classic materials through alphaTest/map.
86
+ const cutoff = material?.uniforms?.aCutoff?.value ??
87
+ material?.uniforms?.alphaCutoff?.value ??
88
+ (material?.alphaTest > 0 ? material.alphaTest : -1);
89
+ const map = material?.uniforms?.base?.value ??
90
+ material?.uniforms?.baseMap?.value ??
91
+ material?.map ?? null;
92
+ depthMaterial = createPassDepthColorMaterial({
93
+ alphaTest: cutoff > 0 ? cutoff : 0,
94
+ map: cutoff > 0 ? map : null,
95
+ side: THREE.DoubleSide,
96
+ });
97
+ depthMaterial.side = material?.shadowSide ?? shadowSideFor[material?.side] ?? THREE.DoubleSide;
98
+ }
99
+ cache.set(material, depthMaterial);
100
+ }
101
+ // three's WebGLShadowMap skips invisible source materials per group
102
+ // (multi-material building meshes carry their opened window panes as a
103
+ // hidden group); keep the depth replacement in step every update.
104
+ depthMaterial.visible = material?.visible !== false;
105
+ return depthMaterial;
106
+ }
107
+
108
+ // Static scenes render the shadow map once: r185's render-object
109
+ // revalidation churn (dispose + recreate) intermittently destroys buffers
110
+ // that queued submits still reference, and a pass that stops submitting
111
+ // when nothing changed sidesteps the race entirely. Callers pass
112
+ // `dynamic: true` while animation moves casters.
113
+ let lastRenderSignature = '';
114
+
115
+ function renderSignature(sun) {
116
+ return `${sun.uuid}:${sun.matrixWorld.elements.join(',')}:${sun.target?.matrixWorld.elements.join(',')}:${sun.shadow.camera.far}:${scene.children.length}`;
117
+ }
118
+
119
+ function update({ dynamic = false } = {}) {
120
+ if (!isNodeBackend) return;
121
+
122
+ // Note: renderer.shadowMap stays DISABLED on the node backends (the labs
123
+ // gate it) — three's own shadow-map pass would re-render every skinned
124
+ // caster with built-in buffer skinning (the >256-bone UBO overflow) and
125
+ // nothing consumes its maps here. The light's castShadow flag alone
126
+ // drives this pass.
127
+ const sun = findShadowSun();
128
+ if (!sun) {
129
+ environmentSunShadow.ready.value = false;
130
+ return;
131
+ }
132
+
133
+ if (!dynamic) {
134
+ const signature = renderSignature(sun);
135
+ if (signature === lastRenderSignature && environmentSunShadow.ready.value) return;
136
+ lastRenderSignature = signature;
137
+ } else {
138
+ lastRenderSignature = '';
139
+ }
140
+
141
+ const shadow = sun.shadow;
142
+ const mapSize = Math.max(shadow.mapSize?.x ?? 2048, 1);
143
+ if (!shadowTarget || shadowTarget.width !== mapSize) {
144
+ shadowTarget?.dispose();
145
+ shadowTarget = new THREE.WebGLRenderTarget(mapSize, mapSize, {
146
+ depthBuffer: true,
147
+ stencilBuffer: false,
148
+ });
149
+ shadowTarget.texture.name = 'EnvironmentSunShadow.Depth';
150
+ shadowTarget.texture.type = THREE.FloatType;
151
+ shadowTarget.texture.minFilter = THREE.NearestFilter;
152
+ shadowTarget.texture.magFilter = THREE.NearestFilter;
153
+ }
154
+
155
+ // The sun rig frames the light's orthographic shadow camera bounds, but
156
+ // POSING the camera (position/orientation from light + target) is a
157
+ // side effect of three's own shadow pass — which is disabled on the node
158
+ // backends. updateMatrices() is that pose step as a public API.
159
+ sun.updateMatrixWorld(true);
160
+ sun.target?.updateMatrixWorld(true);
161
+ shadow.updateMatrices(sun);
162
+ const shadowCamera = shadow.camera;
163
+ shadowCamera.coordinateSystem = renderer.coordinateSystem;
164
+ shadowCamera.updateProjectionMatrix();
165
+ shadowCamera.updateMatrixWorld(true);
166
+ shadowMatrix.multiplyMatrices(shadowCamera.projectionMatrix, shadowCamera.matrixWorldInverse);
167
+ applyShadowClipAdjust(shadowMatrix, renderer);
168
+
169
+ scene.updateMatrixWorld(true);
170
+
171
+ // Swap every visible caster to a depth-color material (cutout-aware);
172
+ // hide non-casters and derived hulls/shells for the duration.
173
+ materialRestores.length = 0;
174
+ visibilityRestores.length = 0;
175
+ scene.traverse((obj) => {
176
+ if (!obj.isMesh || !obj.visible) return;
177
+ if (!obj.castShadow || obj.userData?.isToonOutline || obj.userData?.isToonFurShell) {
178
+ obj.visible = false;
179
+ visibilityRestores.push(obj);
180
+ return;
181
+ }
182
+ // three's WebGLShadowMap skips materials with visible === false; the
183
+ // environment's window-opening materials rely on that so sunlight can
184
+ // stream through opened windows. Match it before the material swap.
185
+ const sourceMaterials = Array.isArray(obj.material) ? obj.material : [obj.material];
186
+ if (!sourceMaterials.some((mat) => mat && mat.visible !== false)) {
187
+ obj.visible = false;
188
+ visibilityRestores.push(obj);
189
+ return;
190
+ }
191
+ if (obj.isSkinnedMesh) obj.skeleton?.update();
192
+ materialRestores.push({ material: obj.material, mesh: obj });
193
+ const customDepthMaterial = obj.customDepthMaterial ?? null;
194
+ let swap = meshSwapCache.get(obj);
195
+ if (!swap || swap.source !== obj.material || swap.customDepthMaterial !== customDepthMaterial) {
196
+ swap = {
197
+ customDepthMaterial,
198
+ source: obj.material,
199
+ swapped: customDepthMaterial ?? (
200
+ Array.isArray(obj.material)
201
+ ? obj.material.map((mat) => depthMaterialFor(mat, obj))
202
+ : depthMaterialFor(obj.material, obj)
203
+ ),
204
+ };
205
+ meshSwapCache.set(obj, swap);
206
+ } else if (!customDepthMaterial && Array.isArray(swap.swapped)) {
207
+ // Keep per-group visibility in step (depthMaterialFor refreshes the
208
+ // single-material case internally).
209
+ for (let i = 0; i < swap.source.length; i += 1) {
210
+ depthMaterialFor(swap.source[i], obj);
211
+ }
212
+ } else if (!customDepthMaterial) {
213
+ depthMaterialFor(swap.source, obj);
214
+ }
215
+ obj.material = swap.swapped;
216
+ });
217
+
218
+ const previousTarget = renderer.getRenderTarget();
219
+ const previousBackground = scene.background;
220
+ // If a lab keeps three's native shadows on alongside this pass (the water
221
+ // playground does, for its MeshToonMaterial ground), rendering our
222
+ // depth-swapped scene must not also trigger three's native shadow-map
223
+ // render of that swapped scene — disable it just for this render.
224
+ const previousShadowAutoUpdate = renderer.shadowMap.enabled;
225
+ renderer.shadowMap.enabled = false;
226
+ scene.background = null;
227
+ renderer.setRenderTarget(shadowTarget);
228
+ renderer.setClearColor(0xffffff, 1);
229
+ renderer.clear();
230
+ renderer.render(scene, shadowCamera);
231
+ renderer.setRenderTarget(previousTarget);
232
+ renderer.shadowMap.enabled = previousShadowAutoUpdate;
233
+ scene.background = previousBackground;
234
+
235
+ for (const { material, mesh } of materialRestores) mesh.material = material;
236
+ for (const obj of visibilityRestores) obj.visible = true;
237
+ materialRestores.length = 0;
238
+ visibilityRestores.length = 0;
239
+
240
+ environmentSunShadow.matrix.value.copy(shadowMatrix);
241
+ environmentSunShadow.mapSize.value = mapSize;
242
+ environmentSunShadow.radius.value = shadow.radius ?? 1;
243
+ // Native shadow maps keep far more precision than the old packed-depth
244
+ // approximation. Keep the shared node-backend pass in float precision too,
245
+ // otherwise low sunset angles can collapse canopy-vs-grass depth and make
246
+ // nearby tree shadows disappear while taller/farther trees still work.
247
+ environmentSunShadow.bias.value = shadow.bias ?? -0.00004;
248
+ sunShadowMapNode().value = shadowTarget.texture;
249
+ environmentSunShadow.ready.value = true;
250
+ }
251
+
252
+ function dispose() {
253
+ shadowTarget?.dispose();
254
+ shadowTarget = null;
255
+ environmentSunShadow.ready.value = false;
256
+ }
257
+
258
+ return {
259
+ get shadowTexture() {
260
+ return shadowTarget?.texture ?? null;
261
+ },
262
+ get shadowMatrix() {
263
+ return shadowMatrix;
264
+ },
265
+ dispose,
266
+ update,
267
+ };
268
+ }