@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,260 @@
1
+ // TSL port of src/shaders/chunks/environment-fragment-lighting.glsl — the
2
+ // wrapped-diffuse light bands, banded Blinn-Phong specular, six-direction
3
+ // ambient-probe irradiance, and the derivative-TBN normal perturbation.
4
+ //
5
+ // Scene light data comes from the shared toonSceneLights uniforms
6
+ // (chunks/character-scene-lights.js) instead of three's <lights_pars_begin>
7
+ // blocks; syncToonSceneLights replicates WebGLLights' view-space conventions,
8
+ // so the light ingredients match what the GLSL loops consumed. The GLSL
9
+ // directionalLights[i] loop is evaluated against the mirrored main light —
10
+ // environment scenes use a single sun, and the NUM_DIR_LIGHTS == 0 fallback
11
+ // (L = normalize(vec3(0.35, 0.75, 0.55)), white color) maps onto
12
+ // hasMainLight = 0 with identical band math.
13
+ //
14
+ // NOTE (preserved GLSL quirk): the wrapped diffuse terms dot the "world"
15
+ // normal — which the vertex stage actually writes in VIEW space — against
16
+ // three's view-space light data. That mismatch is part of the approved
17
+ // baseline look, so it is ported as-is; the specular term additionally
18
+ // re-applies the view matrix to the already-view-space normal, exactly like
19
+ // the GLSL did.
20
+ //
21
+ // GLSL out-parameters (strongestLight) become returned { light, strongest }
22
+ // node-var pairs. Uniform-driven early returns keep their If() shape.
23
+
24
+ import {
25
+ clamp,
26
+ cross,
27
+ dFdx,
28
+ dFdy,
29
+ dot,
30
+ float,
31
+ Fn,
32
+ If,
33
+ inverseSqrt,
34
+ length,
35
+ max,
36
+ mix,
37
+ normalize,
38
+ pow,
39
+ select,
40
+ smoothstep,
41
+ vec3,
42
+ vec4,
43
+ } from 'three/tsl';
44
+
45
+ import {
46
+ getMainLightDirection,
47
+ MAX_TOON_POINT_LIGHTS,
48
+ MAX_TOON_SPOT_LIGHTS,
49
+ toonSceneLights,
50
+ } from './character-scene-lights.js';
51
+
52
+ function maxComponent3(value) {
53
+ return max(value.r, max(value.g, value.b));
54
+ }
55
+
56
+ // three's getDistanceAttenuation (common.glsl) — physically based falloff
57
+ // with the artist cutoff window, matching getPointLightInfo/getSpotLightInfo.
58
+ const distanceAttenuation = /*@__PURE__*/ Fn(([lightDistance, cutoffDistance, decayExponent]) => {
59
+ const distanceFalloff = float(1.0).div(max(pow(lightDistance, decayExponent), 0.01)).toVar();
60
+ If(cutoffDistance.greaterThan(0.0), () => {
61
+ const ratio = lightDistance.div(cutoffDistance);
62
+ const ratio2 = ratio.mul(ratio);
63
+ const window1 = ratio2.mul(ratio2).oneMinus().clamp(0.0, 1.0);
64
+ distanceFalloff.mulAssign(window1.mul(window1));
65
+ });
66
+ return distanceFalloff;
67
+ });
68
+
69
+ export function createEnvironmentLightingChunk({ u, tex, flags, cameraViewMatrixNode }) {
70
+ const s = toonSceneLights;
71
+
72
+ // The wrapped half-Lambert curve (0.16..0.86 smoothstep window) is the
73
+ // canonical toonlab band; consumers outside this stitched shader must keep
74
+ // the constants in step (see the GLSL chunk header).
75
+ const evaluateDirectionalLight = (normal) => {
76
+ const strongest = float(0.0).toVar();
77
+ const light = vec3(0.0).toVar();
78
+ If(u.enableDirectionalLights.greaterThanEqual(0.5), () => {
79
+ // GLSL NUM_DIR_LIGHTS > 0 uses directionalLights[0]; NUM_DIR_LIGHTS == 0
80
+ // falls back to the fixed direction with white color. Both cases share
81
+ // the band formulas, so hasMainLight selects between them at runtime.
82
+ const L = getMainLightDirection();
83
+ const lightColor = mix(vec3(1.0), s.mainLightColor, s.hasMainLight);
84
+ const halfLambert = dot(normal, L).mul(0.5).add(0.5);
85
+ const wrappedLight = smoothstep(
86
+ float(0.16).sub(u.shadeSoftness),
87
+ float(0.86).add(u.shadeSoftness),
88
+ halfLambert,
89
+ );
90
+ const lightBand = mix(1.0, wrappedLight, u.shadeStrength);
91
+ strongest.assign(wrappedLight);
92
+ light.assign(lightColor.mul(lightBand));
93
+ });
94
+ return { light, strongest };
95
+ };
96
+
97
+ const evaluatePointLights = (normal, geometryPosition) => {
98
+ const strongest = float(0.0).toVar();
99
+ const light = vec3(0.0).toVar();
100
+ If(u.enablePointLights.greaterThanEqual(0.5), () => {
101
+ for (let i = 0; i < MAX_TOON_POINT_LIGHTS; i += 1) {
102
+ If(float(i).lessThan(s.pointLightCount), () => {
103
+ // getPointLightInfo(pointLights[i], geometryPosition, pointLight)
104
+ const lVector = s.pointLightPositions.element(i).sub(geometryPosition);
105
+ const lightDistance = length(lVector);
106
+ const direction = lVector.div(max(lightDistance, 1e-6));
107
+ const params = s.pointLightParams.element(i);
108
+ const attenuation = distanceAttenuation(lightDistance, params.x, params.y);
109
+ const color = s.pointLightColors.element(i).mul(attenuation).toVar();
110
+ If(maxComponent3(color).greaterThan(0.0), () => { // pointLight.visible
111
+ const halfLambert = dot(normal, direction).mul(0.5).add(0.5);
112
+ const wrappedLight = smoothstep(0.08, 0.88, halfLambert);
113
+ const lightBand = mix(0.18, wrappedLight, u.shadeStrength);
114
+ strongest.assign(max(strongest, wrappedLight.mul(maxComponent3(color))));
115
+ light.addAssign(color.mul(lightBand));
116
+ });
117
+ });
118
+ }
119
+ });
120
+ return { light, strongest };
121
+ };
122
+
123
+ const evaluateSpotLights = (normal, geometryPosition) => {
124
+ const strongest = float(0.0).toVar();
125
+ const light = vec3(0.0).toVar();
126
+ If(u.enableSpotLights.greaterThanEqual(0.5).and(u.spotLightStrength.greaterThan(0.0)), () => {
127
+ for (let i = 0; i < MAX_TOON_SPOT_LIGHTS; i += 1) {
128
+ If(float(i).lessThan(s.spotLightCount), () => {
129
+ // getSpotLightInfo(spotLights[i], geometryPosition, spotLight)
130
+ const lVector = s.spotLightPositions.element(i).sub(geometryPosition);
131
+ const lightDistance = length(lVector);
132
+ const direction = lVector.div(max(lightDistance, 1e-6));
133
+ const params = s.spotLightParams.element(i);
134
+ const angleCos = dot(direction, normalize(s.spotLightDirections.element(i)));
135
+ const spotAttenuation = smoothstep(params.z, params.w, angleCos);
136
+ const attenuation = distanceAttenuation(lightDistance, params.x, params.y)
137
+ .mul(spotAttenuation);
138
+ const color = s.spotLightColors.element(i).mul(attenuation).toVar();
139
+ If(maxComponent3(color).greaterThan(0.0), () => { // spotLight.visible
140
+ const halfLambert = dot(normal, direction).mul(0.5).add(0.5);
141
+ const wrappedLight = smoothstep(0.08, 0.88, halfLambert);
142
+ const lightBand = mix(0.12, wrappedLight, u.shadeStrength);
143
+ strongest.assign(max(strongest, wrappedLight.mul(maxComponent3(color))));
144
+ light.addAssign(color.mul(lightBand));
145
+ });
146
+ });
147
+ }
148
+ });
149
+ return { light, strongest };
150
+ };
151
+
152
+ // Banded Blinn-Phong highlight for glossy interiors. The GLSL transforms
153
+ // its (already view-space, see quirk note above) normal by viewMatrix again
154
+ // — ported verbatim for parity.
155
+ const evaluateEnvironmentSpecular = (worldNormal, geometryPosition, sunlightVisibility) => {
156
+ const result = vec3(0.0).toVar();
157
+ const normalView = normalize(cameraViewMatrixNode.mul(vec4(worldNormal, 0.0)).xyz);
158
+ const viewDir = normalize(geometryPosition.negate());
159
+
160
+ const bandedSpecular = (lightDirection) => {
161
+ const halfDir = normalize(normalize(lightDirection).add(viewDir));
162
+ const spec = pow(max(dot(normalView, halfDir), 0.0), u.specularShininess);
163
+ return smoothstep(
164
+ float(0.5).sub(u.specularSoftness),
165
+ float(0.5).add(u.specularSoftness),
166
+ spec,
167
+ );
168
+ };
169
+
170
+ // Directional loop — main light only, gated on presence (the GLSL
171
+ // NUM_DIR_LIGHTS == 0 build compiles no directional specular at all).
172
+ If(s.hasMainLight.greaterThan(0.5), () => {
173
+ const spec = bandedSpecular(s.mainLightDirection);
174
+ result.addAssign(s.mainLightColor.mul(spec).mul(sunlightVisibility));
175
+ });
176
+
177
+ for (let i = 0; i < MAX_TOON_POINT_LIGHTS; i += 1) {
178
+ If(float(i).lessThan(s.pointLightCount), () => {
179
+ const lVector = s.pointLightPositions.element(i).sub(geometryPosition);
180
+ const lightDistance = length(lVector);
181
+ const direction = lVector.div(max(lightDistance, 1e-6));
182
+ const params = s.pointLightParams.element(i);
183
+ const attenuation = distanceAttenuation(lightDistance, params.x, params.y);
184
+ const color = s.pointLightColors.element(i).mul(attenuation).toVar();
185
+ If(maxComponent3(color).greaterThan(0.0), () => {
186
+ result.addAssign(color.mul(bandedSpecular(direction)));
187
+ });
188
+ });
189
+ }
190
+
191
+ for (let i = 0; i < MAX_TOON_SPOT_LIGHTS; i += 1) {
192
+ If(float(i).lessThan(s.spotLightCount), () => {
193
+ const lVector = s.spotLightPositions.element(i).sub(geometryPosition);
194
+ const lightDistance = length(lVector);
195
+ const direction = lVector.div(max(lightDistance, 1e-6));
196
+ const params = s.spotLightParams.element(i);
197
+ const angleCos = dot(direction, normalize(s.spotLightDirections.element(i)));
198
+ const spotAttenuation = smoothstep(params.z, params.w, angleCos);
199
+ const attenuation = distanceAttenuation(lightDistance, params.x, params.y)
200
+ .mul(spotAttenuation);
201
+ const color = s.spotLightColors.element(i).mul(attenuation).toVar();
202
+ If(maxComponent3(color).greaterThan(0.0), () => {
203
+ result.addAssign(color.mul(bandedSpecular(direction)));
204
+ });
205
+ });
206
+ }
207
+
208
+ return result;
209
+ };
210
+
211
+ // Six-direction irradiance from the scene ambient probe (shared uniform
212
+ // array). Element access on uniform arrays is a pure expression, so the
213
+ // ternaries stay select()s like the GLSL.
214
+ const environmentProbeIrradiance = (normal) => {
215
+ const probe = u.ambientProbe;
216
+ const weights = normal.mul(normal);
217
+ const x = select(normal.x.greaterThanEqual(0.0), probe.element(0), probe.element(1));
218
+ const y = select(normal.y.greaterThanEqual(0.0), probe.element(2), probe.element(3));
219
+ const z = select(normal.z.greaterThanEqual(0.0), probe.element(4), probe.element(5));
220
+ return x.mul(weights.x).add(y.mul(weights.y)).add(z.mul(weights.z));
221
+ };
222
+
223
+ // Derivative-TBN normal perturbation (mirrors three's tangentless
224
+ // normal_fragment_maps path) — only built when the material has a normal
225
+ // map (the USE_ENV_NORMAL_MAP define analog).
226
+ const perturbEnvironmentNormal = flags.hasNormalMap
227
+ ? (normal, worldPosition, uvNode) => {
228
+ const mapN = tex.normalMapTex.sample(uvNode).xyz.mul(2.0).sub(1.0).toVar();
229
+ mapN.xy.mulAssign(u.normalMapScale.mul(u.normalMapStrength));
230
+
231
+ const q0 = dFdx(worldPosition);
232
+ const q1 = dFdy(worldPosition);
233
+ const st0 = dFdx(uvNode);
234
+ const st1 = dFdy(uvNode);
235
+
236
+ const n = normalize(normal);
237
+ const q1perp = cross(q1, n);
238
+ const q0perp = cross(n, q0);
239
+ const tangent = q1perp.mul(st0.x).add(q0perp.mul(st1.x));
240
+ const bitangent = q1perp.mul(st0.y).add(q0perp.mul(st1.y));
241
+
242
+ const det = max(dot(tangent, tangent), dot(bitangent, bitangent));
243
+ const scale = select(det.equal(0.0), float(0.0), inverseSqrt(det));
244
+ return normalize(
245
+ tangent.mul(mapN.x.mul(scale))
246
+ .add(bitangent.mul(mapN.y.mul(scale)))
247
+ .add(n.mul(mapN.z)),
248
+ );
249
+ }
250
+ : null;
251
+
252
+ return {
253
+ environmentProbeIrradiance,
254
+ evaluateDirectionalLight,
255
+ evaluateEnvironmentSpecular,
256
+ evaluatePointLights,
257
+ evaluateSpotLights,
258
+ perturbEnvironmentNormal,
259
+ };
260
+ }
@@ -0,0 +1,87 @@
1
+ // Shared sun-shadow sampling for TSL environment + character materials —
2
+ // the node-backend replacement for three's <shadowmask_pars_fragment>
3
+ // getShadowMask() (the classic renderer's shadow system is driven by
4
+ // materials with lights:true, which the toon pipeline never uses on the node
5
+ // backends).
6
+ //
7
+ // A dedicated pass (src/environment/environmentSunShadowPass.js) renders the
8
+ // scene from the shadow-casting sun into a float color target (linear window
9
+ // depth — see docs/tsl-conventions.md on depth-texture sampling) and fills
10
+ // these shared uniforms. Until that pass runs, `ready` stays false and the
11
+ // mask is 1.0 everywhere — identical to a scene with no shadow-casting sun.
12
+
13
+ import * as THREE from 'three';
14
+ import {
15
+ clamp,
16
+ dot,
17
+ float,
18
+ Fn,
19
+ If,
20
+ mix,
21
+ smoothstep,
22
+ step,
23
+ texture,
24
+ uniform,
25
+ vec2,
26
+ vec4,
27
+ } from 'three/tsl';
28
+
29
+ export const environmentSunShadow = {
30
+ bias: uniform(0.0025),
31
+ map: null, // TextureNode, created lazily below (needs a fallback texture)
32
+ mapSize: uniform(2048),
33
+ matrix: uniform(new THREE.Matrix4()),
34
+ radius: uniform(1),
35
+ ready: uniform(false, 'bool'),
36
+ };
37
+
38
+ let fallbackTexture = null;
39
+
40
+ export function sunShadowMapNode() {
41
+ if (!environmentSunShadow.map) {
42
+ if (!fallbackTexture) {
43
+ const data = new Uint8Array([255, 255, 255, 255]);
44
+ fallbackTexture = new THREE.DataTexture(data, 1, 1);
45
+ fallbackTexture.needsUpdate = true;
46
+ }
47
+ environmentSunShadow.map = texture(fallbackTexture);
48
+ }
49
+ return environmentSunShadow.map;
50
+ }
51
+
52
+ /**
53
+ * getShadowMask() replacement: 1 fully lit, 0 fully shadowed. worldPosition
54
+ * is the fragment's world position node. The matrix is CPU-composed to yield
55
+ * GL-convention clip coords under the shader's *0.5+0.5 remap on every
56
+ * backend (see shadowClipAdjust in the pass).
57
+ */
58
+ export const sampleEnvironmentSunShadow = /*@__PURE__*/ Fn(([worldPosition]) => {
59
+ const shadow = float(1.0).toVar();
60
+ If(environmentSunShadow.ready, () => {
61
+ const shadowNdc = environmentSunShadow.matrix.mul(vec4(worldPosition, 1.0));
62
+ const coord = shadowNdc.xyz.div(shadowNdc.w).mul(0.5).add(0.5).toVar();
63
+ const inside = coord.x.greaterThanEqual(0.0).and(coord.x.lessThanEqual(1.0))
64
+ .and(coord.y.greaterThanEqual(0.0)).and(coord.y.lessThanEqual(1.0))
65
+ .and(coord.z.lessThanEqual(1.0)).and(coord.z.greaterThanEqual(0.0));
66
+ If(inside, () => {
67
+ const texel = float(1.0).div(environmentSunShadow.mapSize);
68
+ const reference = coord.z.sub(environmentSunShadow.bias);
69
+ const map = sunShadowMapNode();
70
+ // 3×3 PCF (classic PCFSoftShadowMap kernel footprint): averages the
71
+ // 8-bit-quantized self-shadowing into the fine dither the approved
72
+ // interior look was tuned around; radius scales the spread.
73
+ const spread = texel.mul(environmentSunShadow.radius);
74
+ const visibility = float(0.0).toVar();
75
+ for (let y = -1; y <= 1; y += 1) {
76
+ for (let x = -1; x <= 1; x += 1) {
77
+ visibility.addAssign(step(
78
+ reference,
79
+ map.sample(coord.xy.add(vec2(x, y).mul(spread))).level(0).x,
80
+ ));
81
+ }
82
+ }
83
+ shadow.assign(visibility.div(9.0));
84
+ });
85
+ });
86
+ return shadow;
87
+ });
@@ -0,0 +1,60 @@
1
+ // Manual linear scene fog for the world-anchored foliage cards (grass, tree
2
+ // canopies) on the node backends.
3
+ //
4
+ // Why not `material.fog = true`? Those materials override `vertexNode` with a
5
+ // custom world-space billboard, so their real rendered depth lives in a
6
+ // varying (viewPosition.z). three's built-in node fog instead reads the
7
+ // built-in `positionView` (modelViewMatrix * positionLocal), which is derived
8
+ // independently of the custom vertex — for a distant billboard that view-Z is
9
+ // wrong and the crown over-fogs into the sky (only visible under aggressive
10
+ // near fog like the water lab's). The classic GLSL path never had this: its
11
+ // `fog_vertex` chunk reads the same billboarded mvPosition the vertex outputs.
12
+ //
13
+ // So we mirror the GLSL `fog_fragment` exactly — linear smoothstep(near, far)
14
+ // on the true billboarded view depth — and set `material.fog = false`.
15
+ //
16
+ // Defaults are a no-op (far = 1e9), so a material whose lab never calls
17
+ // syncFoliageFog renders unfogged rather than wrong.
18
+
19
+ import * as THREE from 'three';
20
+ import { mix, smoothstep, uniform, vec3 } from 'three/tsl';
21
+
22
+ /** Fog uniform block for a foliage material. Default = effectively no fog. */
23
+ export function createFoliageFogUniforms() {
24
+ return {
25
+ uFogColor: uniform(new THREE.Color(0xffffff)),
26
+ uFogNear: uniform(1),
27
+ uFogFar: uniform(1e9),
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Applies linear fog to `color` using the billboarded view depth.
33
+ * @param {*} color vec3 node (mutated via return)
34
+ * @param {*} vViewZ varying float: viewPosition.z (negative forward)
35
+ * @param {*} u the fog uniform block
36
+ * @returns fogged vec3 node
37
+ */
38
+ export function applyFoliageFog(color, vViewZ, u) {
39
+ // GLSL fog_fragment uses vFogDepth = -mvPosition.z (positive). viewPosition.z
40
+ // is negative forward, so depth = -vViewZ.
41
+ const fogFactor = smoothstep(u.uFogNear, u.uFogFar, vViewZ.negate());
42
+ return mix(color, vec3(u.uFogColor), fogFactor);
43
+ }
44
+
45
+ /**
46
+ * Copies a scene's linear Fog into a foliage material's fog uniforms. Call
47
+ * once per frame (fog color/range can change with time of day). A null/absent
48
+ * fog, or a non-linear FogExp2, disables fog (far = 1e9).
49
+ */
50
+ export function syncFoliageFog(material, fog) {
51
+ const u = material?.uniforms;
52
+ if (!u?.uFogFar) return;
53
+ if (fog && fog.isFog && typeof fog.near === 'number') {
54
+ u.uFogColor.value.copy(fog.color);
55
+ u.uFogNear.value = fog.near;
56
+ u.uFogFar.value = fog.far;
57
+ } else {
58
+ u.uFogFar.value = 1e9;
59
+ }
60
+ }
@@ -0,0 +1,89 @@
1
+ // Depth-as-color pass material for the node backends, shared by the
2
+ // character passes (scene depth prepass, self-shadow map) and the
3
+ // environment sun-shadow pass.
4
+ //
5
+ // The node renderer's WGSL and GLSL builders type depth-texture samples
6
+ // differently (float vs vec4 — docs/tsl-conventions.md), so passes write
7
+ // linear window depth into a float COLOR attachment instead: [0,1] window
8
+ // depth is numerically identical on both coordinate systems for perspective
9
+ // and orthographic cameras alike. The select keys off the active camera, so
10
+ // one material serves both projections.
11
+ //
12
+ // Manual samplers of these targets need the GL-convention matrix pre-adjust
13
+ // exported below (node-renderer RTs are written top-down on BOTH backends;
14
+ // WebGPU clip z is additionally already [0,1]).
15
+
16
+ import * as THREE from 'three';
17
+ import {
18
+ cameraFar,
19
+ cameraNear,
20
+ cameraProjectionMatrix,
21
+ positionView,
22
+ select,
23
+ texture as textureNode,
24
+ vec3,
25
+ vec4,
26
+ viewZToOrthographicDepth,
27
+ viewZToPerspectiveDepth,
28
+ } from 'three/tsl';
29
+ import { MeshBasicNodeMaterial } from 'three/webgpu';
30
+
31
+ import { withToonStorageSkinning } from './character-skinning.js';
32
+
33
+ // Skinned meshes (MMD-scale skeletons included) skin through the storage/PBO
34
+ // path on the WebGL2 backend — see character-skinning.js.
35
+ export const PassBasicNodeMaterial = withToonStorageSkinning(MeshBasicNodeMaterial);
36
+
37
+ export function createPassDepthColorMaterial({
38
+ alphaTest = 0,
39
+ map = null,
40
+ side = THREE.DoubleSide,
41
+ // The classic sun shadow map stored packed-RGBA8 depth; its ~1/255
42
+ // quantization produces the dithered self-shadowing the environment look
43
+ // was tuned around. The sun pass opts in to reproduce that exact
44
+ // mechanism; the character passes keep full float precision (classic used
45
+ // 24-bit depth textures there).
46
+ quantize256 = false,
47
+ } = {}) {
48
+ const material = new PassBasicNodeMaterial({ side });
49
+ // three's shadow-pass exemption: keeps the render-object cache key free of
50
+ // per-frame scene state (background/environment toggled around the pass),
51
+ // which otherwise disposes+recreates every depth render object each frame —
52
+ // destroying object uniform buffers that in-flight submits still reference.
53
+ material.isShadowPassMaterial = true;
54
+ const orthographic = cameraProjectionMatrix.element(3).w.equal(1.0);
55
+ let depth01 = select(
56
+ orthographic,
57
+ viewZToOrthographicDepth(positionView.z, cameraNear, cameraFar),
58
+ viewZToPerspectiveDepth(positionView.z, cameraNear, cameraFar),
59
+ );
60
+ if (quantize256) depth01 = depth01.mul(255.0).floor().div(255.0);
61
+ const alpha = map && alphaTest > 0 ? textureNode(map).a : 1.0;
62
+ material.colorNode = vec4(vec3(depth01), alpha);
63
+ if (alphaTest > 0) material.alphaTest = alphaTest;
64
+ return material;
65
+ }
66
+
67
+ // Pre-multiply onto proj * viewInverse so a shader using the GL-convention
68
+ // `clip.xyz * 0.5 + 0.5` remap samples the right texel and compares the right
69
+ // depth on each backend.
70
+ export const shadowClipAdjustWebGPU = new THREE.Matrix4().set(
71
+ 1, 0, 0, 0,
72
+ 0, -1, 0, 0,
73
+ 0, 0, 2, -1,
74
+ 0, 0, 0, 1,
75
+ );
76
+ export const shadowClipAdjustGL = new THREE.Matrix4().set(
77
+ 1, 0, 0, 0,
78
+ 0, -1, 0, 0,
79
+ 0, 0, 1, 0,
80
+ 0, 0, 0, 1,
81
+ );
82
+
83
+ export function applyShadowClipAdjust(matrix, renderer) {
84
+ return matrix.premultiply(
85
+ renderer.coordinateSystem === THREE.WebGPUCoordinateSystem
86
+ ? shadowClipAdjustWebGPU
87
+ : shadowClipAdjustGL,
88
+ );
89
+ }
@@ -0,0 +1,70 @@
1
+ // TSL port of src/shaders/chunks/stylized-cloud-shadow.glsl — scrolling
2
+ // procedural cloud shadows for outdoor stylized scenes. A cheap 3-octave
3
+ // value-noise fbm (same rotation/lacunarity family as the sky's cloud fbm)
4
+ // sampled in world XZ and drifted by a velocity. Pure functions — callers
5
+ // pass their own uniforms, so grass, tree canopies, terrain, and any future
6
+ // surface share one implementation without colliding uniform names.
7
+
8
+ import {
9
+ clamp,
10
+ dot,
11
+ float,
12
+ floor,
13
+ Fn,
14
+ fract,
15
+ If,
16
+ mix,
17
+ smoothstep,
18
+ vec2,
19
+ vec3,
20
+ } from 'three/tsl';
21
+
22
+ export const cloudShadowHash = /*@__PURE__*/ Fn(([p]) => {
23
+ const p3 = fract(vec3(p.x, p.y, p.x).mul(0.1031)).toVar();
24
+ p3.addAssign(dot(p3, p3.yzx.add(33.33)));
25
+ return fract(p3.x.add(p3.y).mul(p3.z));
26
+ });
27
+
28
+ export const cloudShadowNoise = /*@__PURE__*/ Fn(([p]) => {
29
+ const i = floor(p);
30
+ const f = fract(p);
31
+ const u = f.mul(f).mul(f.mul(-2.0).add(3.0));
32
+ const a = cloudShadowHash(i);
33
+ const b = cloudShadowHash(i.add(vec2(1.0, 0.0)));
34
+ const c = cloudShadowHash(i.add(vec2(0.0, 1.0)));
35
+ const d = cloudShadowHash(i.add(vec2(1.0, 1.0)));
36
+ return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
37
+ });
38
+
39
+ /**
40
+ * GLSL stylizedCloudShadow(worldXZ, time, strength, coverage, scale,
41
+ * velocity): sunlight visibility factor in [1 - strength, 1]. strength below
42
+ * 0.001 short-circuits to fully lit (same If shape as the GLSL early return,
43
+ * so the fbm costs nothing while disabled).
44
+ */
45
+ export const stylizedCloudShadow = /*@__PURE__*/ Fn(([worldXZ, time, strength, coverage, scale, velocity]) => {
46
+ const visibility = float(1.0).toVar();
47
+ If(strength.greaterThanEqual(0.001), () => {
48
+ const uv = worldXZ.mul(scale).add(velocity.mul(time)).toVar();
49
+ const value = float(0.0).toVar();
50
+ let amplitude = 0.5;
51
+ let total = 0.0;
52
+ for (let i = 0; i < 3; i += 1) {
53
+ value.addAssign(cloudShadowNoise(uv).mul(amplitude));
54
+ total += amplitude;
55
+ // GLSL: uv = mat2(0.8, 0.6, -0.6, 0.8) * uv * 2.02 + vec2(17.13, 9.77),
56
+ // written out component-wise (columns (0.8, 0.6) and (-0.6, 0.8)) —
57
+ // TSL matN() scalar constructors are row-major (docs/tsl-conventions.md).
58
+ uv.assign(vec2(
59
+ uv.x.mul(0.8).add(uv.y.mul(-0.6)),
60
+ uv.x.mul(0.6).add(uv.y.mul(0.8)),
61
+ ).mul(2.02).add(vec2(17.13, 9.77)));
62
+ amplitude *= 0.5;
63
+ }
64
+ value.divAssign(Math.max(total, 1e-4));
65
+ const threshold = mix(0.72, 0.4, clamp(coverage, 0.0, 1.0));
66
+ const cloud = smoothstep(threshold, threshold.add(0.17), value);
67
+ visibility.assign(cloud.mul(clamp(strength, 0.0, 1.0)).oneMinus());
68
+ });
69
+ return visibility;
70
+ });