@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,475 @@
1
+ import * as THREE from 'three';
2
+
3
+ import {
4
+ applyWaterSettingsToMaterial,
5
+ createWaterMaterial,
6
+ setWaterDebugMode,
7
+ updateWaterMaterialCamera,
8
+ } from './waterMaterial.js';
9
+ import { WaterBreakerSystem } from './waterBreakerSystem.js';
10
+ import { WaterInteractionManager } from './waterInteraction.js';
11
+ import { WaterRippleSimulation } from './waterRippleSimulation.js';
12
+ import { WaterScenePasses } from './waterScenePasses.js';
13
+ import { WaterSplashSystem } from './waterSplashSystem.js';
14
+ import {
15
+ createWaterSettings,
16
+ sampleGerstnerHeight,
17
+ } from './waterSettings.js';
18
+
19
+ const worldPositionScratch = new THREE.Vector3();
20
+ const localScratch = new THREE.Vector3();
21
+ const breakerSampleScratch = { weight: 0, crestY: 0, flowX: 0, flowZ: 0 };
22
+ const followScratch = new THREE.Vector3();
23
+
24
+ // All-in-one stylized water body: surface mesh + material, GPU ripple
25
+ // simulation, splash particles, interaction tracking, and the scene passes
26
+ // that feed refraction/reflection. One update() call per frame drives
27
+ // everything:
28
+ //
29
+ // const water = new WaterSurface({ width: 18, depth: 22, preset: 'lake' });
30
+ // water.position.y = 0.36;
31
+ // scene.add(water);
32
+ // ...
33
+ // water.update(renderer, scene, camera, delta); // before renderer.render
34
+ //
35
+ // The surface must stay axis-aligned (translation only); rotating the water
36
+ // plane is not supported by the interaction/reflection math.
37
+ export class WaterSurface extends THREE.Mesh {
38
+ constructor({
39
+ width = 20,
40
+ depth = 20,
41
+ segmentsPerMeter = 5,
42
+ maxSegments = 380,
43
+ simulation = {},
44
+ splashes = {},
45
+ passes = {},
46
+ interaction = {},
47
+ follow = null,
48
+ bedHeight = null,
49
+ ...settingsOptions
50
+ } = {}) {
51
+ const settings = createWaterSettings(settingsOptions);
52
+ const segmentsX = Math.min(maxSegments, Math.max(16, Math.round(width * segmentsPerMeter)));
53
+ const segmentsZ = Math.min(maxSegments, Math.max(16, Math.round(depth * segmentsPerMeter)));
54
+ const geometry = new THREE.PlaneGeometry(width, depth, segmentsX, segmentsZ);
55
+ geometry.rotateX(-Math.PI / 2);
56
+
57
+ // `shoaling` is the WATER_SHOALING signal for the TSL factory branch
58
+ // (graph shape must be known at creation); createWaterSettings drops the
59
+ // key, so the classic ShaderMaterial path is untouched by it.
60
+ super(geometry, createWaterMaterial(
61
+ typeof bedHeight === 'function' ? { ...settings, shoaling: true } : settings,
62
+ ));
63
+ this.name = 'WaterSurface';
64
+ this.width = width;
65
+ this.depth = depth;
66
+ this.settings = this.material.userData.waterSettings;
67
+ this.time = 0;
68
+ this.followTarget = follow;
69
+ // Displaced waves escape the flat-plane bounds; skip culling entirely.
70
+ this.frustumCulled = false;
71
+ // Scene shadows from rocks, trees, and the character darken the surface.
72
+ this.receiveShadow = true;
73
+
74
+ // Optional terrain sampler (x, z) => world-space bed height. Enables
75
+ // shoaling: waves flatten in shallow water instead of clipping through
76
+ // shores/islands. Vertex bed heights bake in update() and re-bake
77
+ // whenever the surface's world XZ moves (>5cm) from where it was baked —
78
+ // immune to first-frame ordering races where the mesh isn't positioned
79
+ // yet. The terrain itself is assumed static.
80
+ this.bedHeightSampler = typeof bedHeight === 'function' ? bedHeight : null;
81
+ this.shoalingBakedX = NaN;
82
+ this.shoalingBakedZ = NaN;
83
+ // Dedicated plunging-breaker shells along the break line; created lazily
84
+ // in update() when settings.breakerAmount > 0 and a bed sampler exists.
85
+ this.breakers = null;
86
+ this.breakerBuilt = { x: NaN, y: NaN, z: NaN, energy: NaN };
87
+ if (this.bedHeightSampler) {
88
+ this.material.defines = { ...this.material.defines, WATER_SHOALING: 1 };
89
+ }
90
+
91
+ this.simulationEnabled = simulation !== false;
92
+ this.ripples = this.simulationEnabled
93
+ ? new WaterRippleSimulation({
94
+ resolution: simulation.resolution ?? 256,
95
+ worldWidth: Math.min(simulation.worldSize ?? width, width),
96
+ worldDepth: Math.min(simulation.worldSize ?? depth, depth),
97
+ })
98
+ : null;
99
+
100
+ this.splashesEnabled = splashes !== false;
101
+ this.splashSystem = this.splashesEnabled
102
+ ? new WaterSplashSystem({ ...(splashes || {}), settings: this.settings })
103
+ : null;
104
+ if (this.splashSystem) {
105
+ this.add(this.splashSystem);
106
+ // Foam rings evaluate the same Gerstner stack to drape over the swell.
107
+ this.splashSystem.attachWaveUniforms(this.material);
108
+ }
109
+
110
+ this.passesEnabled = passes !== false;
111
+ this.passes = this.passesEnabled ? new WaterScenePasses(passes || {}) : null;
112
+
113
+ this.interactions = new WaterInteractionManager(this, interaction || {});
114
+
115
+ this.syncSimulationParameters();
116
+ }
117
+
118
+ get gerstnerWaves() {
119
+ return this.material.userData.gerstnerWaves ?? [];
120
+ }
121
+
122
+ syncSimulationParameters() {
123
+ this.ripples?.setParameters({
124
+ rippleDamping: this.settings.rippleDamping,
125
+ ripplePropagation: this.settings.ripplePropagation,
126
+ rippleFoamDecay: this.settings.rippleFoamDecay,
127
+ rippleFoamGain: this.settings.rippleFoamGain,
128
+ });
129
+ }
130
+
131
+ // Merges option overrides into the current settings (pass { preset } to
132
+ // switch presets while keeping explicit overrides you re-supply).
133
+ applySettings(options = {}) {
134
+ applyWaterSettingsToMaterial(this.material, { ...this.settings, ...options });
135
+ this.settings = this.material.userData.waterSettings;
136
+ this.splashSystem?.applySettings(this.settings);
137
+ this.syncSimulationParameters();
138
+ return this.settings;
139
+ }
140
+
141
+ // Loads a preset from scratch (unlike applySettings, prior overrides drop).
142
+ setPreset(name, overrides = {}) {
143
+ applyWaterSettingsToMaterial(this.material, { preset: name, ...overrides });
144
+ this.settings = this.material.userData.waterSettings;
145
+ this.splashSystem?.applySettings(this.settings);
146
+ this.syncSimulationParameters();
147
+ return this.settings;
148
+ }
149
+
150
+ setDebugMode(mode) {
151
+ setWaterDebugMode(this.material, mode);
152
+ return this;
153
+ }
154
+
155
+ // Drifting procedural cloud shadows over the surface — same field as the
156
+ // grass/tree/terrain helpers so a passing cloud dims the scene together.
157
+ // strength 0 (the default) disables. velocity is uv-space drift per second.
158
+ setCloudShadow({ strength, coverage, scale, velocity } = {}) {
159
+ const uniforms = this.material.uniforms;
160
+ if (Number.isFinite(strength)) uniforms.uCloudShadowStrength.value = strength;
161
+ if (Number.isFinite(coverage)) uniforms.uCloudShadowCoverage.value = coverage;
162
+ if (Number.isFinite(scale)) uniforms.uCloudShadowScale.value = scale;
163
+ if (velocity) {
164
+ uniforms.uCloudShadowVelocity.value.set(
165
+ velocity[0] ?? velocity.x ?? 0, velocity[1] ?? velocity.y ?? 0);
166
+ }
167
+ return this;
168
+ }
169
+
170
+ setFollowTarget(target) {
171
+ this.followTarget = target;
172
+ return this;
173
+ }
174
+
175
+ get waveEnergy() {
176
+ return this.material.userData.waterWaveEnergy ?? 0.3;
177
+ }
178
+
179
+ // World-space wave height of the rendered Gerstner surface at (x, z).
180
+ // Interactive ripples live on the GPU and are not included; for buoyancy
181
+ // the analytic swell is the part that matters.
182
+ getHeightAt(x, z) {
183
+ this.getWorldPosition(worldPositionScratch);
184
+ const surfaceY = worldPositionScratch.y;
185
+ return surfaceY + this.relativeSurfaceHeightAt(x, z, surfaceY);
186
+ }
187
+
188
+ // Surface height relative to the rest level — the exact CPU mirror of the
189
+ // vertex shader's shoaling/breaking/swash displacement, so buoyancy, swim
190
+ // floats, and splash anchors sit on the rendered surface everywhere,
191
+ // including the surf zone and the run-up film on the beach.
192
+ relativeSurfaceHeightAt(x, z, surfaceY) {
193
+ if (!this.bedHeightSampler) {
194
+ return sampleGerstnerHeight(this.gerstnerWaves, x, z, this.time);
195
+ }
196
+ const settings = this.settings;
197
+ const bed = this.bedHeightSampler(x, z);
198
+ const restDepth = surfaceY - bed;
199
+ const energy = Math.max(this.waveEnergy, 1e-3);
200
+ const range = Math.max(settings.shoalingDepth ?? 1.4, energy * 2.2, 1e-3);
201
+ // Mirror of the vertex shader's shallow-water chop filter.
202
+ const chopWeight = THREE.MathUtils.lerp(
203
+ 0.15, 1, THREE.MathUtils.smoothstep(restDepth, range * 0.3, range * 1.4));
204
+ const raw = sampleGerstnerHeight(this.gerstnerWaves, x, z, this.time, chopWeight);
205
+ const deepFactor = THREE.MathUtils.smoothstep(restDepth, 0, range);
206
+ const rearUp = (1 - THREE.MathUtils.smoothstep(restDepth, range * 0.45, range * 1.5)) *
207
+ THREE.MathUtils.smoothstep(restDepth, 0.05, 0.35);
208
+ const shoal = THREE.MathUtils.lerp(settings.shorelineWaves ?? 0.35, 1, deepFactor) *
209
+ (1 + 0.3 * rearUp);
210
+ const targetY = raw * shoal;
211
+ const capY = 0.72 * Math.max(restDepth, 0);
212
+ const brokenY = Math.max(
213
+ Math.min(targetY, capY) + Math.max(targetY - capY, 0) * 0.1,
214
+ -Math.max(restDepth, 0) + 0.04);
215
+ const wavePhase = THREE.MathUtils.clamp(raw / energy, -1, 1);
216
+ const runup = (settings.shorelineRunup ?? 0.6) * energy * Math.max(wavePhase, 0);
217
+ const film = THREE.MathUtils.clamp(restDepth + runup, -0.2, 0.05);
218
+ const filmY = (bed - surfaceY) + film;
219
+ const beach = 1 - THREE.MathUtils.smoothstep(restDepth, -0.02, 0.06);
220
+ const base = THREE.MathUtils.lerp(brokenY, filmY, beach);
221
+
222
+ // Breakers are physical: blend the traveling shell face over the base
223
+ // surface so buoyant objects (and the swimmer) ride up the wave as it
224
+ // passes and drop behind it. The blend keeps the base where the shell
225
+ // envelope fades out, so there is never a height discontinuity.
226
+ if (this.breakers) {
227
+ const shell = this.breakers.sampleAt(x, z, breakerSampleScratch);
228
+ if (shell.weight > 0.001 && shell.crestY > base) {
229
+ return base + (shell.crestY - base) * shell.weight;
230
+ }
231
+ }
232
+ return base;
233
+ }
234
+
235
+ // Horizontal water push velocity (m/s, world space) at (x, z) — currently
236
+ // the shoreward surge of a passing breaker: strongest in the whitewater
237
+ // just after the wave breaks, modest on the unbroken face. Zero elsewhere.
238
+ // Feed it to floating bodies and swimmers so surf actually carries them.
239
+ getFlowAt(x, z, out = new THREE.Vector2()) {
240
+ out.set(0, 0);
241
+ if (this.breakers) {
242
+ const shell = this.breakers.sampleAt(x, z, breakerSampleScratch);
243
+ out.set(shell.flowX, shell.flowZ);
244
+ }
245
+ return out;
246
+ }
247
+
248
+ // 0..1 local wave motion scale — how much of the open-water swell survives
249
+ // at (x, z). Splash sheets use it to damp their wave-conforming drape in
250
+ // the shallows and on the beach film.
251
+ waveScaleAt(x, z, surfaceY) {
252
+ if (!this.bedHeightSampler) return 1;
253
+ const restDepth = surfaceY - this.bedHeightSampler(x, z);
254
+ const range = Math.max(
255
+ this.settings.shoalingDepth ?? 1.4, Math.max(this.waveEnergy, 1e-3) * 2.2, 1e-3);
256
+ const deepFactor = THREE.MathUtils.smoothstep(restDepth, 0, range);
257
+ const beach = 1 - THREE.MathUtils.smoothstep(restDepth, -0.02, 0.06);
258
+ return THREE.MathUtils.lerp(this.settings.shorelineWaves ?? 0.35, 1, deepFactor) *
259
+ (1 - beach);
260
+ }
261
+
262
+ // Samples the terrain under every vertex and stores it as the aBedHeight
263
+ // attribute the vertex shader reads. update() re-runs this automatically
264
+ // when the surface's world XZ has moved since the last bake.
265
+ bakeShoalingDepths() {
266
+ if (!this.bedHeightSampler) return this;
267
+ this.updateWorldMatrix(true, false);
268
+ const positionAttr = this.geometry.attributes.position;
269
+ const existing = this.geometry.attributes.aBedHeight;
270
+ const bedHeights = existing?.count === positionAttr.count
271
+ ? existing.array
272
+ : new Float32Array(positionAttr.count);
273
+ for (let i = 0; i < positionAttr.count; i += 1) {
274
+ localScratch.fromBufferAttribute(positionAttr, i).applyMatrix4(this.matrixWorld);
275
+ bedHeights[i] = this.bedHeightSampler(localScratch.x, localScratch.z);
276
+ }
277
+ if (existing?.array === bedHeights) {
278
+ existing.needsUpdate = true;
279
+ } else {
280
+ this.geometry.setAttribute('aBedHeight', new THREE.BufferAttribute(bedHeights, 1));
281
+ }
282
+ this.getWorldPosition(worldPositionScratch);
283
+ this.shoalingBakedX = worldPositionScratch.x;
284
+ this.shoalingBakedZ = worldPositionScratch.z;
285
+ return this;
286
+ }
287
+
288
+ // Lifecycle for the breaker shells: creates/disposes with the
289
+ // breakerAmount setting and rebuilds the break line whenever the surface
290
+ // moves or the swell energy shifts enough to relocate the collapse depth.
291
+ updateBreakers() {
292
+ const enabled = this.settings.breakerEnabled !== false &&
293
+ this.bedHeightSampler && (this.settings.breakerAmount ?? 0) > 0.001;
294
+ if (!enabled) {
295
+ if (this.breakers) {
296
+ this.remove(this.breakers);
297
+ this.breakers.dispose();
298
+ this.breakers = null;
299
+ this.breakerBuilt.x = NaN;
300
+ }
301
+ return;
302
+ }
303
+ if (!this.breakers) {
304
+ this.breakers = new WaterBreakerSystem();
305
+ this.breakers.attachWaveUniforms(this.material);
306
+ this.add(this.breakers);
307
+ }
308
+ this.getWorldPosition(worldPositionScratch);
309
+ const energy = this.waveEnergy;
310
+ const built = this.breakerBuilt;
311
+ const unchanged =
312
+ Math.abs(worldPositionScratch.x - built.x) <= 0.05 &&
313
+ Math.abs(worldPositionScratch.y - built.y) <= 0.02 &&
314
+ Math.abs(worldPositionScratch.z - built.z) <= 0.05 &&
315
+ Math.abs(energy - built.energy) <= 0.03;
316
+ if (!unchanged) {
317
+ this.breakers.rebuild({
318
+ bedSampler: this.bedHeightSampler,
319
+ originX: worldPositionScratch.x,
320
+ originZ: worldPositionScratch.z,
321
+ surfaceY: worldPositionScratch.y,
322
+ width: this.width,
323
+ depth: this.depth,
324
+ settings: this.settings,
325
+ waveEnergy: energy,
326
+ waves: this.gerstnerWaves,
327
+ });
328
+ built.x = worldPositionScratch.x;
329
+ built.y = worldPositionScratch.y;
330
+ built.z = worldPositionScratch.z;
331
+ built.energy = energy;
332
+ }
333
+ this.breakers.configure(this.settings, energy, this.gerstnerWaves);
334
+ this.breakers.update(this.time);
335
+ }
336
+
337
+ containsPoint(x, z, margin = 0) {
338
+ this.getWorldPosition(worldPositionScratch);
339
+ return Math.abs(x - worldPositionScratch.x) <= this.width * 0.5 - margin &&
340
+ Math.abs(z - worldPositionScratch.z) <= this.depth * 0.5 - margin;
341
+ }
342
+
343
+ worldToLocalPoint(worldPoint, out = localScratch) {
344
+ this.getWorldPosition(worldPositionScratch);
345
+ return out.set(
346
+ (worldPoint.x ?? 0) - worldPositionScratch.x,
347
+ (worldPoint.y ?? worldPositionScratch.y) - worldPositionScratch.y,
348
+ (worldPoint.z ?? 0) - worldPositionScratch.z,
349
+ );
350
+ }
351
+
352
+ // Composite splash at a world position: particles + a downward impulse and
353
+ // an outward ring wave in the ripple simulation.
354
+ splash(worldPoint, { strength = 1, radius = 0.5 } = {}) {
355
+ const local = this.worldToLocalPoint(worldPoint);
356
+ local.y = this.relativeSurfaceHeightAt(
357
+ worldPoint.x ?? 0, worldPoint.z ?? 0, worldPositionScratch.y);
358
+ this.splashSystem?.emitSplash(local, { strength });
359
+ if (this.ripples) {
360
+ const impulse = strength * this.settings.rippleStrength;
361
+ this.ripples.addImpulse(worldPoint.x, worldPoint.z, {
362
+ radius: radius,
363
+ strength: -1.1 * impulse,
364
+ });
365
+ this.ripples.addRingImpulse(worldPoint.x, worldPoint.z, {
366
+ radius: radius * 1.1,
367
+ strength: 0.55 * impulse,
368
+ });
369
+ }
370
+ return this;
371
+ }
372
+
373
+ // Plain ripple impulse (no particles) — wakes, rain, gentle touches.
374
+ addRipple(worldPoint, { radius = 0.4, strength = 0.4 } = {}) {
375
+ this.ripples?.addImpulse(worldPoint.x, worldPoint.z, {
376
+ radius,
377
+ strength: strength * this.settings.rippleStrength,
378
+ });
379
+ return this;
380
+ }
381
+
382
+ // Small droplet burst without rings/crown — bow spray on fast movers.
383
+ sprayAt(worldPoint, { count = 4, strength = 0.4 } = {}) {
384
+ if (!this.splashSystem) return this;
385
+ const local = this.worldToLocalPoint(worldPoint);
386
+ local.y = Math.max(local.y, 0);
387
+ this.splashSystem.emitDroplets(local, { count, strength });
388
+ return this;
389
+ }
390
+
391
+ // source: Object3D | (outVector3) => position | { getPosition }.
392
+ addInteractor(source, options = {}) {
393
+ return this.interactions.add(source, options);
394
+ }
395
+
396
+ removeInteractor(id) {
397
+ this.interactions.remove(id);
398
+ }
399
+
400
+ // Call once per frame before rendering the scene.
401
+ update(renderer, scene, camera, delta) {
402
+ const clampedDelta = Math.min(Math.max(delta ?? 0.016, 0), 0.1);
403
+ this.time += clampedDelta;
404
+
405
+ if (this.bedHeightSampler) {
406
+ this.getWorldPosition(worldPositionScratch);
407
+ const movedX = Math.abs(worldPositionScratch.x - this.shoalingBakedX);
408
+ const movedZ = Math.abs(worldPositionScratch.z - this.shoalingBakedZ);
409
+ if (!(movedX <= 0.05 && movedZ <= 0.05)) this.bakeShoalingDepths();
410
+ }
411
+ this.updateBreakers();
412
+
413
+ if (this.ripples) {
414
+ this.getWorldPosition(worldPositionScratch);
415
+ if (this.followTarget) {
416
+ const target = typeof this.followTarget === 'function'
417
+ ? this.followTarget(followScratch)
418
+ : this.followTarget.getWorldPosition?.(followScratch) ?? this.followTarget;
419
+ if (target && Number.isFinite(target.x)) this.ripples.setCenter(target.x, target.z);
420
+ } else {
421
+ this.ripples.setCenter(worldPositionScratch.x, worldPositionScratch.z);
422
+ }
423
+ this.ripples.update(renderer, clampedDelta);
424
+ }
425
+
426
+ this.interactions.update(clampedDelta);
427
+ if (this.splashSystem) {
428
+ // Crowns and foam rings hug the animated surface: rewrite their anchor
429
+ // heights from the same displacement math the mesh uses, so they never
430
+ // hover above a trough or drown under a crest. The wave-scale sampler
431
+ // lets the sheet shader drape ring geometry over the local swell.
432
+ this.getWorldPosition(worldPositionScratch);
433
+ const surfaceX = worldPositionScratch.x;
434
+ const surfaceY = worldPositionScratch.y;
435
+ const surfaceZ = worldPositionScratch.z;
436
+ this.splashSystem.updateSurfaceHeights(
437
+ (localX, localZ) =>
438
+ this.relativeSurfaceHeightAt(localX + surfaceX, localZ + surfaceZ, surfaceY),
439
+ (localX, localZ) =>
440
+ this.waveScaleAt(localX + surfaceX, localZ + surfaceZ, surfaceY),
441
+ );
442
+ this.splashSystem.update(this.time, renderer);
443
+ }
444
+
445
+ const uniforms = this.material.uniforms;
446
+ uniforms.uTime.value = this.time;
447
+ updateWaterMaterialCamera(this.material, renderer, camera);
448
+ this.getWorldPosition(worldPositionScratch);
449
+ uniforms.uCameraBelow.value =
450
+ camera.getWorldPosition(followScratch).y < worldPositionScratch.y ? 1 : 0;
451
+ if (this.ripples) {
452
+ uniforms.uRippleMap.value = this.ripples.texture;
453
+ uniforms.uUseRippleMap.value = 1;
454
+ this.ripples.getRegion(uniforms.uRippleRegion.value);
455
+ uniforms.uRippleTexel.value.copy(this.ripples.texelSize);
456
+ } else {
457
+ uniforms.uUseRippleMap.value = 0;
458
+ }
459
+
460
+ if (this.passes) {
461
+ this.passes.render(renderer, scene, camera, this);
462
+ this.passes.bindToMaterial(this.material);
463
+ }
464
+ return this;
465
+ }
466
+
467
+ dispose() {
468
+ this.geometry.dispose();
469
+ this.material.dispose();
470
+ this.ripples?.dispose();
471
+ this.splashSystem?.dispose();
472
+ this.breakers?.dispose();
473
+ this.passes?.dispose();
474
+ }
475
+ }
@@ -0,0 +1,83 @@
1
+ import * as THREE from 'three';
2
+
3
+ import { createWaterKelpNodeMaterial } from '../shaders-tsl/water-kelp.js';
4
+
5
+ function setSrgbColor(color, rgb) {
6
+ color.setRGB(rgb[0], rgb[1], rgb[2], THREE.SRGBColorSpace);
7
+ }
8
+
9
+ // Instanced underwater kelp blades that sway with the water flow — a scale
10
+ // and motion reference for underwater camera work. Fully procedural.
11
+ //
12
+ // const kelp = new WaterKelpField({
13
+ // placements: positions.map((p) => ({ x: p.x, y: bedHeight(p), z: p.z })),
14
+ // });
15
+ // scene.add(kelp);
16
+ // kelp.update(delta); // each frame
17
+ // kelp.setFlow(settings.flowDirection, settings.flowSpeed);
18
+ export class WaterKelpField extends THREE.Mesh {
19
+ constructor({
20
+ placements = [],
21
+ heightRange = [0.7, 1.6],
22
+ widthRange = [0.1, 0.2],
23
+ swayAmplitude = 0.24,
24
+ kelpColor = [0.32, 0.7, 0.42],
25
+ kelpShadeColor = [0.1, 0.36, 0.28],
26
+ shadowStrength = 0.55,
27
+ } = {}) {
28
+ const count = Math.max(placements.length, 1);
29
+ const blade = new THREE.PlaneGeometry(1, 1, 1, 6);
30
+ blade.translate(0, 0.5, 0);
31
+
32
+ const geometry = new THREE.InstancedBufferGeometry();
33
+ geometry.index = blade.index;
34
+ geometry.setAttribute('position', blade.attributes.position);
35
+ geometry.setAttribute('uv', blade.attributes.uv);
36
+
37
+ const origins = new Float32Array(count * 3);
38
+ const infos = new Float32Array(count * 4);
39
+ placements.forEach((placement, i) => {
40
+ origins[i * 3] = placement.x ?? 0;
41
+ origins[i * 3 + 1] = placement.y ?? 0;
42
+ origins[i * 3 + 2] = placement.z ?? 0;
43
+ infos[i * 4] = placement.height ??
44
+ THREE.MathUtils.lerp(heightRange[0], heightRange[1], Math.random());
45
+ infos[i * 4 + 1] = Math.random();
46
+ infos[i * 4 + 2] = placement.width ??
47
+ THREE.MathUtils.lerp(widthRange[0], widthRange[1], Math.random());
48
+ infos[i * 4 + 3] = Math.random() * Math.PI * 2;
49
+ });
50
+ geometry.setAttribute('iOrigin', new THREE.InstancedBufferAttribute(origins, 3));
51
+ geometry.setAttribute('iInfo', new THREE.InstancedBufferAttribute(infos, 4));
52
+ geometry.instanceCount = placements.length;
53
+ geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(), 1e5);
54
+
55
+ // getShadowMask() comes from the shared sun-shadow pass on the node
56
+ // backends (see water-kelp.js).
57
+ const material = createWaterKelpNodeMaterial({ shadowStrength, swayAmplitude });
58
+ setSrgbColor(material.uniforms.uKelpColor.value, kelpColor);
59
+ setSrgbColor(material.uniforms.uKelpShadeColor.value, kelpShadeColor);
60
+
61
+ super(geometry, material);
62
+ this.name = 'WaterKelpField';
63
+ this.frustumCulled = false;
64
+ this.receiveShadow = true;
65
+ }
66
+
67
+ setFlow(flowDirection, flowSpeed) {
68
+ const direction = this.material.uniforms.uFlowDirection.value;
69
+ direction.set(flowDirection?.[0] ?? flowDirection?.x ?? 1, flowDirection?.[1] ?? flowDirection?.y ?? 0);
70
+ if (Number.isFinite(flowSpeed)) this.material.uniforms.uFlowSpeed.value = flowSpeed;
71
+ return this;
72
+ }
73
+
74
+ update(delta) {
75
+ this.material.uniforms.uTime.value += Math.min(Math.max(delta ?? 0.016, 0), 0.1);
76
+ return this;
77
+ }
78
+
79
+ dispose() {
80
+ this.geometry.dispose();
81
+ this.material.dispose();
82
+ }
83
+ }