@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,298 @@
1
+ // TSL port of src/shaders/waterSplashDroplets.*.glsl + waterSplashSheets.*.glsl
2
+ // — the splash droplet points and the spray-crown / foam-ring sheets.
3
+ //
4
+ // WGSL has no gl_PointSize, so the droplet POINTS become instanced billboard
5
+ // quads whose clip-space offset reproduces the GLSL pixel math exactly — the
6
+ // dust-motes pattern from environmentRigs (docs/tsl-conventions.md). The quad
7
+ // uv is flipped to gl_PointCoord orientation (y grows downward) so the
8
+ // off-center highlight lands identically.
9
+ //
10
+ // Sheets keep their instanced-quad geometry; the material factory can adopt
11
+ // the owning surface material's uWavesA/uWavesB uniform NODES (`waves`
12
+ // option) — the TSL equivalent of WaterSplashSystem.attachWaveUniforms's
13
+ // by-reference sharing, which rebuilds this material before first render.
14
+
15
+ import * as THREE from 'three';
16
+ import {
17
+ abs,
18
+ atan,
19
+ attribute,
20
+ cameraPosition,
21
+ cameraProjectionMatrix,
22
+ cameraViewMatrix,
23
+ Discard,
24
+ float,
25
+ Fn,
26
+ fract,
27
+ If,
28
+ length,
29
+ max,
30
+ min,
31
+ mix,
32
+ modelWorldMatrix,
33
+ normalize,
34
+ positionGeometry,
35
+ pow,
36
+ sin,
37
+ smoothstep,
38
+ step,
39
+ uniform,
40
+ uv,
41
+ varying,
42
+ vec2,
43
+ vec3,
44
+ vec4,
45
+ viewportSize,
46
+ } from 'three/tsl';
47
+ import { NodeMaterial } from 'three/webgpu';
48
+
49
+ import { waterFbm, waterToonStep, waterValueNoise } from './chunks/water-common.js';
50
+ import { createWaterWavesChunk } from './chunks/water-waves.js';
51
+ import { resolveWaterArrayUniformNode } from './water.js';
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Droplets — ballistic particles, billboard quads standing in for points.
55
+ // ---------------------------------------------------------------------------
56
+
57
+ export function createWaterSplashDropletsNodeMaterial() {
58
+ const u = {
59
+ uTime: uniform(0),
60
+ uPixelRatio: uniform(1),
61
+ uPointScale: uniform(540),
62
+ uDropletColor: uniform(new THREE.Color(1, 1, 1)),
63
+ uHighlightColor: uniform(new THREE.Color(1, 1, 1)),
64
+ };
65
+
66
+ const material = new NodeMaterial();
67
+ material.name = 'WaterSplashDroplets';
68
+ material.lights = false;
69
+ material.fog = false;
70
+ material.transparent = true;
71
+ material.depthWrite = false;
72
+
73
+ const vLife = varying(float(), 'vDropletLife');
74
+ const vSeed = varying(float(), 'vDropletSeed');
75
+
76
+ material.vertexNode = Fn(() => {
77
+ const aSpawnOrigin = attribute('aSpawnOrigin', 'vec3');
78
+ const aVelocity = attribute('aVelocity', 'vec3');
79
+ // aInfo = (spawnTime, life, size, seed)
80
+ const aInfo = attribute('aInfo', 'vec4');
81
+
82
+ const age = u.uTime.sub(aInfo.x).toVar();
83
+ const life = max(aInfo.y, 1e-3).toVar();
84
+ const t = age.div(life).toVar();
85
+ vLife.assign(t);
86
+ vSeed.assign(aInfo.w);
87
+
88
+ // Dead particles collapse off-clip exactly like the GLSL early return.
89
+ const clipPosition = vec4(8.0, 8.0, 8.0, 1.0).toVar();
90
+ If(t.greaterThan(0.0).and(t.lessThan(1.0)), () => {
91
+ const localPosition = aSpawnOrigin.add(aVelocity.mul(age)).toVar();
92
+ localPosition.y.subAssign(age.mul(age).mul(4.905));
93
+
94
+ // Sink out once a droplet falls back through the surface (local y = 0).
95
+ const submergeFade = smoothstep(-0.3, -0.06, localPosition.y);
96
+
97
+ const mvPosition = cameraViewMatrix.mul(modelWorldMatrix).mul(vec4(localPosition, 1.0)).toVar();
98
+ const sizeCurve = smoothstep(0.0, 0.1, t).mul(smoothstep(0.5, 1.0, t).mul(0.55).oneMinus());
99
+ // gl_PointSize equivalent, in device pixels.
100
+ const pixels = aInfo.z.mul(sizeCurve).mul(submergeFade).mul(u.uPointScale).mul(u.uPixelRatio)
101
+ .div(max(mvPosition.z.negate(), 0.1)).toVar();
102
+ clipPosition.assign(cameraProjectionMatrix.mul(mvPosition));
103
+ // plane(1,1) local xy spans [-0.5, 0.5] → full quad = `pixels` px, the
104
+ // same square gl_PointSize would rasterize (dust-motes pattern).
105
+ clipPosition.xy.addAssign(
106
+ positionGeometry.xy.mul(pixels).mul(float(2.0).div(viewportSize)).mul(clipPosition.w),
107
+ );
108
+ });
109
+ return clipPosition;
110
+ })();
111
+
112
+ material.fragmentNode = Fn(() => {
113
+ // gl_PointCoord replacement: quad uv with y flipped (PointCoord y grows
114
+ // downward), remapped to [-1, 1].
115
+ const point = vec2(uv().x, uv().y.oneMinus()).mul(2.0).sub(1.0).toVar();
116
+ const radius = length(point).toVar();
117
+ Discard(radius.greaterThan(1.0));
118
+
119
+ const body = smoothstep(0.72, 1.0, radius).oneMinus();
120
+ // Bright white core with a cool tinted edge; highlight spot sits up-left.
121
+ const color = mix(u.uHighlightColor, u.uDropletColor, smoothstep(0.3, 0.95, radius)).toVar();
122
+ const highlight = smoothstep(0.1, 0.5, length(point.sub(vec2(-0.32, -0.34)))).oneMinus();
123
+ color.assign(mix(color, vec3(1.0), step(0.5, highlight).mul(0.6)));
124
+
125
+ const fade = smoothstep(0.58, 1.0, vLife).oneMinus();
126
+ const alpha = body.mul(fade).mul(fract(vSeed.mul(7.31)).mul(0.18).add(0.82)).toVar();
127
+ Discard(alpha.lessThan(0.02));
128
+
129
+ return vec4(color, alpha);
130
+ })();
131
+
132
+ material.uniforms = u;
133
+ material.userData.isToonNodeMaterial = true;
134
+ return material;
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Sheets — spray crown (kind 0, cylindrically billboarded) and foam ring
139
+ // (kind 1, hugging the surface).
140
+ // ---------------------------------------------------------------------------
141
+
142
+ /**
143
+ * options.waves null | { wavesA, wavesB, waveCount } — the owning surface
144
+ * material's wave uniform nodes (WATER_SHEET_WAVES analog).
145
+ * options.previous uniform-node map of a previous sheet material; uniforms
146
+ * are reused so values persist across the rebuild.
147
+ */
148
+ export function createWaterSplashSheetsNodeMaterial({ waves = null, previous = null } = {}) {
149
+ const u = {
150
+ uTime: uniform(0),
151
+ uSprayColor: uniform(new THREE.Color(1, 1, 1)),
152
+ uSprayShadeColor: uniform(new THREE.Color(0.7, 0.9, 1)),
153
+ };
154
+ if (previous) {
155
+ for (const name of Object.keys(u)) {
156
+ if (previous[name]) u[name] = previous[name];
157
+ }
158
+ }
159
+
160
+ const waveChunk = waves
161
+ ? createWaterWavesChunk({
162
+ wavesA: resolveWaterArrayUniformNode(waves.wavesA),
163
+ wavesB: resolveWaterArrayUniformNode(waves.wavesB),
164
+ waveCount: waves.waveCount,
165
+ })
166
+ : null;
167
+
168
+ const material = new NodeMaterial();
169
+ material.name = 'WaterSplashSheets';
170
+ material.lights = false;
171
+ material.fog = false;
172
+ material.transparent = true;
173
+ material.depthWrite = false;
174
+ material.side = THREE.DoubleSide;
175
+
176
+ const vLocal = varying(vec2(), 'vSheetLocal');
177
+ const vLife = varying(float(), 'vSheetLife');
178
+ const vSeed = varying(float(), 'vSheetSeed');
179
+ const vKind = varying(float(), 'vSheetKind');
180
+
181
+ material.vertexNode = Fn(() => {
182
+ const iOrigin = attribute('iOrigin', 'vec3');
183
+ // iInfo = (spawnTime, life, scale, seed)
184
+ const iInfo = attribute('iInfo', 'vec4');
185
+ const iKind = attribute('iKind', 'float');
186
+ const iSurface = attribute('iSurface', 'float');
187
+
188
+ const age = u.uTime.sub(iInfo.x).toVar();
189
+ const life = max(iInfo.y, 1e-3).toVar();
190
+ const t = age.div(life).toVar();
191
+ vLife.assign(t);
192
+ vSeed.assign(iInfo.w);
193
+ vKind.assign(iKind);
194
+ vLocal.assign(vec2(0.0));
195
+
196
+ const clipPosition = vec4(8.0, 8.0, 8.0, 1.0).toVar();
197
+ If(t.greaterThan(0.0).and(t.lessThan(1.0)), () => {
198
+ const scale = iInfo.z.toVar();
199
+ const originWorld = modelWorldMatrix.mul(vec4(iOrigin, 1.0)).toVar();
200
+ const worldOffset = vec3(0.0).toVar();
201
+
202
+ If(iKind.lessThan(0.5), () => {
203
+ // Spray crown: anchored at the waterline, cylindrically billboarded.
204
+ vLocal.assign(vec2(positionGeometry.x.mul(2.0), positionGeometry.y.add(0.5)));
205
+ const toCamera = cameraPosition.sub(originWorld.xyz);
206
+ const right = normalize(vec3(toCamera.z.negate(), 0.0, toCamera.x).add(vec3(1e-5, 0.0, 0.0)));
207
+ const pop = sin(min(t.mul(1.25), 1.0).mul(3.14159265));
208
+ const width = scale.mul(smoothstep(0.0, 0.7, t).mul(0.66).add(0.58));
209
+ const height = scale.mul(pop.mul(0.92).add(0.38));
210
+ worldOffset.assign(right.mul(vLocal.x.mul(0.5).mul(width)).add(vec3(0.0, vLocal.y.mul(height), 0.0)));
211
+ }).Else(() => {
212
+ // Foam ring: hugs the surface; the ring radius animates in the fragment.
213
+ vLocal.assign(vec2(positionGeometry.x, positionGeometry.y).mul(2.0));
214
+ worldOffset.assign(vec3(vLocal.x, 0.0, vLocal.y.negate()).mul(scale).mul(1.25).add(vec3(0.0, 0.02, 0.0)));
215
+ });
216
+
217
+ const worldPosition = vec4(originWorld).toVar();
218
+ worldPosition.xyz.addAssign(worldOffset);
219
+
220
+ if (waveChunk) {
221
+ If(iKind.greaterThan(0.5), () => {
222
+ // Drape the ring over the swell: offset each vertex by the wave
223
+ // height difference from the (CPU-anchored) center, scaled by the
224
+ // local wave motion so rings flatten in the surf zone.
225
+ const waveDelta = waveChunk.gerstnerDisplacement(worldPosition.xz, u.uTime).y
226
+ .sub(waveChunk.gerstnerDisplacement(originWorld.xz, u.uTime).y);
227
+ worldPosition.y.addAssign(waveDelta.mul(iSurface));
228
+ });
229
+ }
230
+
231
+ clipPosition.assign(cameraProjectionMatrix.mul(cameraViewMatrix).mul(worldPosition));
232
+ });
233
+ return clipPosition;
234
+ })();
235
+
236
+ material.fragmentNode = Fn(() => {
237
+ const alpha = float(0.0).toVar();
238
+ const color = vec3(u.uSprayColor).toVar();
239
+
240
+ If(vKind.lessThan(0.5), () => {
241
+ // Spray crown: a fan of noisy vertical streaks that erode away upward.
242
+ const x = vLocal.x.toVar();
243
+ const y = vLocal.y.toVar();
244
+ const noise = waterFbm(vec2(x.mul(3.1).add(vSeed.mul(17.0)), y.mul(2.2).sub(vLife.mul(3.4))), 3).toVar();
245
+ const streaks = waterValueNoise(vec2(x.mul(7.5).add(vSeed.mul(29.0)), y.mul(1.1).sub(vLife.mul(2.2)))).toVar();
246
+ const profile = x.mul(x).mul(0.72).oneMinus().mul(noise.sub(0.35).mul(0.62).add(0.5)).toVar();
247
+ const body = smoothstep(profile, profile.sub(0.3), y)
248
+ .mul(smoothstep(1.0, 0.55, abs(x)))
249
+ .mul(smoothstep(0.28, 0.62, streaks.add(y.oneMinus().mul(0.25))));
250
+
251
+ const speckle = step(0.84, waterValueNoise(
252
+ vec2(x.mul(10.0), y.mul(8.0).sub(vLife.mul(7.0))).add(vSeed.mul(23.0)),
253
+ ));
254
+ const crownEdge = smoothstep(profile.sub(0.34), profile.sub(0.06), y);
255
+
256
+ const fadeIn = smoothstep(0.0, 0.08, vLife);
257
+ const fadeOut = smoothstep(0.45, 0.95, vLife).oneMinus();
258
+ alpha.assign(clamp01(max(body, speckle.mul(crownEdge))).mul(fadeIn).mul(fadeOut));
259
+
260
+ const shade = waterToonStep(0.5, 0.12, noise.add(y.mul(0.5)));
261
+ color.assign(mix(u.uSprayShadeColor, u.uSprayColor, shade));
262
+ }).Else(() => {
263
+ // Foam ring: expanding annulus that thins and dissolves into dashes.
264
+ const radius = length(vLocal).toVar();
265
+ const angle = atan(vLocal.y, vLocal.x).toVar();
266
+ const ringRadius = mix(0.14, 0.94, pow(vLife.oneMinus(), 2.3).oneMinus()).toVar();
267
+ const ringWidth = mix(0.3, 0.05, vLife).toVar();
268
+ const band = smoothstep(0.0, ringWidth, abs(radius.sub(ringRadius))).oneMinus().toVar();
269
+
270
+ const breakup = waterValueNoise(
271
+ vec2(angle.mul(2.4), radius.mul(2.0)).mul(2.2).add(vSeed.mul(11.0)).add(vLife.mul(1.2)),
272
+ );
273
+ band.mulAssign(smoothstep(0.28, 0.5, breakup.add(vLife.oneMinus().mul(0.4))));
274
+
275
+ alpha.assign(waterToonStep(0.3, 0.18, band).mul(smoothstep(0.55, 1.0, vLife).oneMinus()).mul(0.85));
276
+ color.assign(mix(u.uSprayShadeColor, u.uSprayColor, waterToonStep(0.5, 0.2, band)));
277
+ });
278
+
279
+ Discard(alpha.lessThan(0.02));
280
+ return vec4(color, alpha);
281
+ })();
282
+
283
+ material.uniforms = u;
284
+ material.userData.isToonNodeMaterial = true;
285
+ if (waves) {
286
+ // Mirror the classic post-attach uniform surface: the shared wave entries
287
+ // appear under their GLSL names (same objects as the owning surface).
288
+ u.uWavesA = waves.wavesA;
289
+ u.uWavesB = waves.wavesB;
290
+ material.defines = { WATER_WAVE_COUNT: waves.waveCount, WATER_SHEET_WAVES: 1 };
291
+ }
292
+ return material;
293
+ }
294
+
295
+ // clamp(x, 0, 1) helper local to the crown body (keeps the GLSL clamp shape).
296
+ function clamp01(node) {
297
+ return node.clamp(0.0, 1.0);
298
+ }