@call-me-sensei/toonlab 0.2.0 → 0.3.0

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 (205) hide show
  1. package/AGENTS.md +127 -4
  2. package/ATTRIBUTION.md +41 -3
  3. package/README.md +234 -34
  4. package/docs/characters.md +144 -0
  5. package/docs/debug-panel.md +126 -0
  6. package/docs/docs.css +589 -0
  7. package/docs/environment.md +186 -0
  8. package/docs/getting-started.md +180 -0
  9. package/docs/index.html +14 -0
  10. package/docs/lab-architecture.md +100 -0
  11. package/docs/lighting.md +825 -0
  12. package/docs/main.jsx +739 -0
  13. package/docs/mcp.md +97 -0
  14. package/docs/post-processing.md +98 -0
  15. package/docs/settings-reference.md +1878 -0
  16. package/docs/shader-constants.md +77 -0
  17. package/docs/sky.md +182 -0
  18. package/docs/style-labs.md +309 -0
  19. package/docs/texture-lab.md +135 -0
  20. package/docs/toon-shading.md +183 -0
  21. package/docs/tsl-conventions.md +167 -0
  22. package/docs/vegetation-sky.md +275 -0
  23. package/docs/water.md +430 -0
  24. package/docs/weather.md +200 -0
  25. package/docs/world-scale.md +111 -0
  26. package/mcp/server.mjs +610 -0
  27. package/mcp/style-lab-tools.mjs +371 -0
  28. package/mcp/vite-plugin.mjs +174 -0
  29. package/mcp/workspace.mjs +397 -0
  30. package/package.json +64 -5
  31. package/src/ambientfx/INTEGRATION.md +164 -0
  32. package/src/ambientfx/ambientFxPresets.js +83 -0
  33. package/src/ambientfx/ambientFxSettings.js +368 -0
  34. package/src/ambientfx/emitters.js +169 -0
  35. package/src/ambientfx/index.js +5 -0
  36. package/src/ambientfx/particleBackbone.js +493 -0
  37. package/src/ambientfx/stylizedAmbientFx.js +450 -0
  38. package/src/assetlib/ambientcg.js +117 -0
  39. package/src/assetlib/assetRef.js +91 -0
  40. package/src/assetlib/importedEntry.js +40 -0
  41. package/src/assetlib/index.js +23 -0
  42. package/src/assetlib/kaykit.js +192 -0
  43. package/src/assetlib/kaykitStaticIndex.js +700 -0
  44. package/src/assetlib/loadImported.js +143 -0
  45. package/src/assetlib/opensource3d.js +113 -0
  46. package/src/assetlib/polyhaven.js +182 -0
  47. package/src/assetlib/polypizza.js +115 -0
  48. package/src/assetlib/smithsonian.js +214 -0
  49. package/src/assetlib/sources.js +279 -0
  50. package/src/assetlib/zip.js +58 -0
  51. package/src/biome/biomeGenerator.js +385 -0
  52. package/src/biome/biomeRuntime.js +299 -0
  53. package/src/biome/index.js +2 -0
  54. package/src/buildinggen/buildingAsset.js +44 -0
  55. package/src/buildinggen/buildingGrammar.js +311 -0
  56. package/src/buildinggen/buildingMesh.js +451 -0
  57. package/src/buildinggen/buildingPresets.js +46 -0
  58. package/src/buildinggen/buildingRecipe.js +100 -0
  59. package/src/buildinggen/buildingSettings.js +238 -0
  60. package/src/buildinggen/index.js +6 -0
  61. package/src/camera/cameraDirector.js +157 -0
  62. package/src/camera/cameraGenerator.js +367 -0
  63. package/src/camera/cameraRig.js +570 -0
  64. package/src/camera/cameraSettings.js +236 -0
  65. package/src/camera/index.js +7 -0
  66. package/src/catalog/builtinEntries.js +245 -0
  67. package/src/catalog/catalog.js +210 -0
  68. package/src/catalog/index.js +3 -0
  69. package/src/catalog/manifest.js +84 -0
  70. package/src/core/generation.js +529 -0
  71. package/src/environment/environmentRigs.js +21 -1
  72. package/src/environment/environmentSettings.js +4 -0
  73. package/src/environment/environmentSunShadowPass.js +8 -0
  74. package/src/fauna/INTEGRATION.md +174 -0
  75. package/src/fauna/boids.js +861 -0
  76. package/src/fauna/faunaBodies.js +492 -0
  77. package/src/fauna/faunaPresets.js +52 -0
  78. package/src/fauna/faunaSettings.js +525 -0
  79. package/src/fauna/index.js +5 -0
  80. package/src/fauna/stylizedFauna.js +395 -0
  81. package/src/game-feel/gameFeelGenerator.js +402 -0
  82. package/src/game-feel/gameFeelRuntime.js +549 -0
  83. package/src/game-feel/index.js +2 -0
  84. package/src/index.js +17 -6
  85. package/src/lighting/colorIntensity.js +177 -0
  86. package/src/lighting/index.js +161 -0
  87. package/src/lighting/lightDescriptors.js +249 -0
  88. package/src/lighting/lightingCapabilities.js +79 -0
  89. package/src/lighting/lightingDocuments.js +247 -0
  90. package/src/lighting/lightingFixtures.js +446 -0
  91. package/src/lighting/lightingGenerator.js +449 -0
  92. package/src/lighting/lightingPresets.js +319 -0
  93. package/src/lighting/lightingRuntime.js +723 -0
  94. package/src/lighting/lightingStyle.js +386 -0
  95. package/src/lighting/lightingSystem.js +774 -0
  96. package/src/lighting/unrealExport.js +186 -0
  97. package/src/lighting/utils.js +87 -0
  98. package/src/motion/index.js +5 -0
  99. package/src/motion/motionClip.js +441 -0
  100. package/src/motion/motionController.js +628 -0
  101. package/src/motion/motionDocuments.js +225 -0
  102. package/src/motion/motionGraph.js +307 -0
  103. package/src/motion/motionSettings.js +222 -0
  104. package/src/pathgen/index.js +7 -0
  105. package/src/pathgen/pathBridge.js +232 -0
  106. package/src/pathgen/pathPresets.js +35 -0
  107. package/src/pathgen/pathRibbon.js +410 -0
  108. package/src/pathgen/pathRouter.js +380 -0
  109. package/src/pathgen/pathSettings.js +335 -0
  110. package/src/pathgen/pathTextures.js +123 -0
  111. package/src/pathgen/stylizedPaths.js +453 -0
  112. package/src/post/index.js +1 -0
  113. package/src/post/postGenerator.js +177 -0
  114. package/src/post/postProcessing.js +41 -0
  115. package/src/propgen/generatorsWave1.js +379 -0
  116. package/src/propgen/generatorsWave2.js +462 -0
  117. package/src/propgen/index.js +5 -0
  118. package/src/propgen/propAsset.js +323 -0
  119. package/src/propgen/propParts.js +170 -0
  120. package/src/propgen/propPlacement.js +459 -0
  121. package/src/propgen/propPresets.js +82 -0
  122. package/src/propgen/propSettings.js +395 -0
  123. package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
  124. package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
  125. package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
  126. package/src/shaders-tsl/chunks/water-waves.js +90 -11
  127. package/src/shaders-tsl/environment.js +15 -1
  128. package/src/shaders-tsl/flower.js +279 -30
  129. package/src/shaders-tsl/grass.js +60 -33
  130. package/src/shaders-tsl/sky.js +125 -31
  131. package/src/shaders-tsl/tree-leaf.js +61 -25
  132. package/src/shaders-tsl/water-breaker.js +7 -4
  133. package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
  134. package/src/shaders-tsl/water.js +439 -49
  135. package/src/shaders-tsl/woody-surface.js +154 -0
  136. package/src/sky/sceneOverrideLayers.js +10 -0
  137. package/src/sky/skyQuality.js +26 -0
  138. package/src/sky/stylizedSky.js +753 -45
  139. package/src/soundscape/index.js +4 -0
  140. package/src/soundscape/soundscapeGenerator.js +179 -0
  141. package/src/soundscape/soundscapeRuntime.js +806 -0
  142. package/src/soundscape/soundscapeSettings.js +292 -0
  143. package/src/styles/index.js +13 -0
  144. package/src/styles/styleBundle.js +325 -0
  145. package/src/stylizedTerrain.js +32 -2
  146. package/src/stylizedWorld.js +423 -20
  147. package/src/texgen/evaluateTexture.js +675 -0
  148. package/src/texgen/index.js +60 -0
  149. package/src/texgen/noise2.js +210 -0
  150. package/src/texgen/textureAi.js +436 -0
  151. package/src/texgen/textureGenerators.js +516 -0
  152. package/src/texgen/texturePresets.js +490 -0
  153. package/src/texgen/textureSettings.js +342 -0
  154. package/src/texgen/textureThree.js +59 -0
  155. package/src/vegetation/flowerSpecies.js +15 -3
  156. package/src/vegetation/grassPalettes.js +153 -0
  157. package/src/vegetation/index.js +6 -0
  158. package/src/vegetation/stylizedBush.js +2 -0
  159. package/src/vegetation/stylizedFlower.js +82 -0
  160. package/src/vegetation/stylizedFlowers.js +48 -7
  161. package/src/vegetation/stylizedForest.js +29 -1
  162. package/src/vegetation/stylizedGrass.js +291 -56
  163. package/src/vegetation/stylizedTree.js +38 -2
  164. package/src/vegetation/stylizedTreeFoliage.js +2 -1
  165. package/src/vegetation/vegetationShaders.js +1110 -0
  166. package/src/vfxgen/INTEGRATION.md +145 -0
  167. package/src/vfxgen/core/burstBackbone.js +380 -0
  168. package/src/vfxgen/core/projectileCore.js +92 -0
  169. package/src/vfxgen/core/spriteShapes.js +98 -0
  170. package/src/vfxgen/core/trailRibbon.js +272 -0
  171. package/src/vfxgen/core/vfxRandom.js +29 -0
  172. package/src/vfxgen/effects/emitHelpers.js +37 -0
  173. package/src/vfxgen/effects/magicEffects.js +162 -0
  174. package/src/vfxgen/effects/movementEffects.js +87 -0
  175. package/src/vfxgen/effects/weaponEffects.js +118 -0
  176. package/src/vfxgen/index.js +18 -0
  177. package/src/vfxgen/moves/moveController.js +146 -0
  178. package/src/vfxgen/moves/moveLibrary.js +335 -0
  179. package/src/vfxgen/vfxPresets.js +98 -0
  180. package/src/vfxgen/vfxSettings.js +384 -0
  181. package/src/vfxgen/vfxSystem.js +449 -0
  182. package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
  183. package/src/villagegen/index.js +4 -0
  184. package/src/villagegen/stylizedVillage.js +490 -0
  185. package/src/villagegen/villageArchetypes.js +160 -0
  186. package/src/villagegen/villageNames.js +40 -0
  187. package/src/villagegen/villageSites.js +105 -0
  188. package/src/water/sceneOverrideLayers.js +23 -0
  189. package/src/water/water.js +5 -0
  190. package/src/water/waterBreakerSystem.js +15 -1
  191. package/src/water/waterCurrentField.js +447 -0
  192. package/src/water/waterMaterial.js +12 -0
  193. package/src/water/waterNearshorePhase.js +320 -0
  194. package/src/water/waterScenePasses.js +83 -30
  195. package/src/water/waterSettings.js +325 -13
  196. package/src/water/waterShoreMaterial.js +322 -0
  197. package/src/water/waterShoreStateField.js +605 -0
  198. package/src/water/waterSurface.js +797 -28
  199. package/src/weather/index.js +6 -0
  200. package/src/weather/weatherPrecipitation.js +221 -0
  201. package/src/weather/weatherPresets.js +258 -0
  202. package/src/weather/weatherSettings.js +269 -0
  203. package/src/weather/weatherSystem.js +871 -0
  204. package/src/worldMinimap.js +62 -0
  205. package/src/worldPresets.js +4 -1
@@ -0,0 +1,605 @@
1
+ import * as THREE from 'three';
2
+
3
+ import { createWaterShoreStateSimulationNodeMaterial } from
4
+ '../shaders-tsl/water-shore-state-simulation.js';
5
+ import {
6
+ samplePrimarySwellSequence,
7
+ sampleSwashDistance,
8
+ sampleSwashEventShape,
9
+ shapeSwashProgress,
10
+ } from './waterSettings.js';
11
+
12
+ const FIXED_TIMESTEP = 1 / 30;
13
+ const MAX_SUBSTEPS_PER_FRAME = 2;
14
+
15
+ const DEFAULT_PARAMETERS = Object.freeze({
16
+ moistureWetTime: 0.12,
17
+ moistureDryTime: 120,
18
+ filmWetTime: 0.08,
19
+ filmDryTime: 2,
20
+ foamWetLifetime: 6,
21
+ foamDryLifetime: 2,
22
+ residueLifetime: 12,
23
+ foamGain: 7,
24
+ foamAdvection: 1,
25
+ foamDiffusion: 0.012,
26
+ // Keep both source octaves above the 23 cm state-atlas Nyquist footprint.
27
+ // Broader cells survive advection as readable torn rafts instead of
28
+ // aliasing into small, cut-out scallops at the moving lip.
29
+ noiseScale: 0.32,
30
+ baseCurrent: Object.freeze([0, 0]),
31
+ });
32
+
33
+ const NUMERIC_PARAMETER_UNIFORMS = Object.freeze({
34
+ moistureWetTime: 'uMoistureWetTime',
35
+ moistureDryTime: 'uMoistureDryTime',
36
+ filmWetTime: 'uFilmWetTime',
37
+ filmDryTime: 'uFilmDryTime',
38
+ foamWetLifetime: 'uFoamWetLifetime',
39
+ foamDryLifetime: 'uFoamDryLifetime',
40
+ residueLifetime: 'uResidueLifetime',
41
+ foamGain: 'uFoamGain',
42
+ foamAdvection: 'uFoamAdvection',
43
+ foamDiffusion: 'uFoamDiffusion',
44
+ noiseScale: 'uNoiseScale',
45
+ });
46
+
47
+ function finiteOr(value, fallback) {
48
+ const number = Number(value);
49
+ return Number.isFinite(number) ? number : fallback;
50
+ }
51
+
52
+ function normalizeResolution(resolution) {
53
+ let x;
54
+ let y;
55
+ if (Number.isFinite(resolution)) {
56
+ x = resolution;
57
+ y = resolution;
58
+ } else if (Array.isArray(resolution)) {
59
+ [x, y] = resolution;
60
+ } else {
61
+ x = resolution?.x ?? resolution?.width;
62
+ y = resolution?.y ?? resolution?.height;
63
+ }
64
+ return {
65
+ x: THREE.MathUtils.clamp(Math.round(finiteOr(x, 512)), 16, 2048),
66
+ y: THREE.MathUtils.clamp(Math.round(finiteOr(y, 128)), 16, 2048),
67
+ };
68
+ }
69
+
70
+ // Constructor regions use full width/depth. A Vector4 is accepted as the
71
+ // shader-facing (centerX, centerZ, halfWidth, halfDepth) representation.
72
+ function normalizeRegion(region) {
73
+ if (region?.isVector4) {
74
+ return {
75
+ centerX: finiteOr(region.x, 0),
76
+ centerZ: finiteOr(region.y, 0),
77
+ width: Math.max(finiteOr(region.z, 16) * 2, 0.1),
78
+ depth: Math.max(finiteOr(region.w, 8) * 2, 0.1),
79
+ };
80
+ }
81
+ if (Array.isArray(region)) {
82
+ return {
83
+ centerX: finiteOr(region[0], 0),
84
+ centerZ: finiteOr(region[1], 0),
85
+ width: Math.max(finiteOr(region[2], 32), 0.1),
86
+ depth: Math.max(finiteOr(region[3], 16), 0.1),
87
+ };
88
+ }
89
+ if (region?.min && region?.max) {
90
+ const minX = finiteOr(region.min.x, -16);
91
+ const minZ = finiteOr(region.min.y ?? region.min.z, -8);
92
+ const maxX = finiteOr(region.max.x, 16);
93
+ const maxZ = finiteOr(region.max.y ?? region.max.z, 8);
94
+ return {
95
+ centerX: (minX + maxX) * 0.5,
96
+ centerZ: (minZ + maxZ) * 0.5,
97
+ width: Math.max(maxX - minX, 0.1),
98
+ depth: Math.max(maxZ - minZ, 0.1),
99
+ };
100
+ }
101
+ const center = region?.center;
102
+ const size = region?.size;
103
+ return {
104
+ centerX: finiteOr(region?.centerX ?? center?.x ?? region?.x, 0),
105
+ centerZ: finiteOr(region?.centerZ ?? center?.z ?? center?.y ?? region?.z, 0),
106
+ width: Math.max(finiteOr(
107
+ region?.width ?? size?.x ?? (Number.isFinite(region?.halfWidth) ? region.halfWidth * 2 : undefined),
108
+ 32,
109
+ ), 0.1),
110
+ depth: Math.max(finiteOr(
111
+ region?.depth ?? size?.z ?? size?.y ??
112
+ (Number.isFinite(region?.halfDepth) ? region.halfDepth * 2 : undefined),
113
+ 16,
114
+ ), 0.1),
115
+ };
116
+ }
117
+
118
+ function vector2Components(value, fallbackX = 0, fallbackY = 0) {
119
+ if (Array.isArray(value)) {
120
+ return [finiteOr(value[0], fallbackX), finiteOr(value[1], fallbackY)];
121
+ }
122
+ return [
123
+ finiteOr(value?.x ?? value?.[0], fallbackX),
124
+ finiteOr(value?.y ?? value?.z ?? value?.[1], fallbackY),
125
+ ];
126
+ }
127
+
128
+ function eventHash(value) {
129
+ let x = ((value * 0.1031) % 1 + 1) % 1;
130
+ x *= x + 33.33;
131
+ x *= x + x;
132
+ return ((x % 1) + 1) % 1;
133
+ }
134
+
135
+ function eventNoiseOffset(index, out) {
136
+ return out.set(eventHash(index + 17.17), eventHash(index + 83.91));
137
+ }
138
+
139
+ function waveEnergyFrom(waves) {
140
+ return (waves ?? []).reduce(
141
+ (sum, wave) => sum + Math.abs(finiteOr(wave?.amplitude, 0)),
142
+ 0,
143
+ );
144
+ }
145
+
146
+ function sampleProgressAt(waves, time) {
147
+ return shapeSwashProgress(samplePrimarySwellSequence(waves, time).cycle);
148
+ }
149
+
150
+ function buildFallbackSwashFrame(waves, time, runupDistance) {
151
+ const sequence = samplePrimarySwellSequence(waves, time);
152
+ const progress = shapeSwashProgress(sequence.cycle);
153
+ const derivativeStep = 1 / 120;
154
+ const progressSpeed = (
155
+ sampleProgressAt(waves, time + derivativeStep) -
156
+ sampleProgressAt(waves, time - derivativeStep)
157
+ ) / (derivativeStep * 2);
158
+ const explicit = runupDistance > 0.01;
159
+ const edgeDistance = explicit ? sampleSwashDistance(waves, time, runupDistance) : 0;
160
+ const edgeDistanceSpeed = explicit ? (
161
+ sampleSwashDistance(waves, time + derivativeStep, runupDistance) -
162
+ sampleSwashDistance(waves, time - derivativeStep, runupDistance)
163
+ ) / (derivativeStep * 2) : 0;
164
+ return {
165
+ cycle: sequence.cycle,
166
+ cycleSpeed: Math.max(finiteOr(waves?.[0]?.omega, 0), 0) / (Math.PI * 2),
167
+ edgeShape: sampleSwashEventShape(sequence.index),
168
+ eventIndex: sequence.index,
169
+ progress,
170
+ progressSpeed,
171
+ edgeDistance,
172
+ edgeDistanceSpeed,
173
+ };
174
+ }
175
+
176
+ /**
177
+ * World-anchored persistent wetness/foam field for a shoreline band.
178
+ *
179
+ * The region is independent from WaterRippleSimulation's camera-following
180
+ * window. Example:
181
+ *
182
+ * new WaterShoreStateField({
183
+ * region: { centerX: 0, centerZ: 0, width: 180, depth: 32 },
184
+ * resolution: { x: 512, y: 96 },
185
+ * bedHeight: heightAt,
186
+ * });
187
+ */
188
+ export class WaterShoreStateField {
189
+ constructor({
190
+ region = {},
191
+ resolution = { x: 512, y: 128 },
192
+ bedHeight = null,
193
+ bedHeightSampler = bedHeight,
194
+ currentField = null,
195
+ parameters = {},
196
+ } = {}) {
197
+ const normalizedRegion = normalizeRegion(region);
198
+ const normalizedResolution = normalizeResolution(resolution);
199
+ this.centerX = normalizedRegion.centerX;
200
+ this.centerZ = normalizedRegion.centerZ;
201
+ this.worldWidth = normalizedRegion.width;
202
+ this.worldDepth = normalizedRegion.depth;
203
+ this.resolutionX = normalizedResolution.x;
204
+ this.resolutionY = normalizedResolution.y;
205
+ this.region = new THREE.Vector4(
206
+ this.centerX,
207
+ this.centerZ,
208
+ this.worldWidth * 0.5,
209
+ this.worldDepth * 0.5,
210
+ );
211
+
212
+ this.parameters = {
213
+ ...DEFAULT_PARAMETERS,
214
+ baseCurrent: [...DEFAULT_PARAMETERS.baseCurrent],
215
+ };
216
+ this.targets = [this.createTarget(), this.createTarget()];
217
+ this.readIndex = 0;
218
+ this.timeAccumulator = 0;
219
+ this.needsClear = true;
220
+ this.disposed = false;
221
+ this.bedHeightSampler = null;
222
+ this.bedTexture = null;
223
+ this.bedHeightData = null;
224
+ this.bedValidityData = null;
225
+ this.bedTextureData = null;
226
+ this.currentField = null;
227
+ this.eventOffsetScratch = new THREE.Vector2();
228
+
229
+ this.material = createWaterShoreStateSimulationNodeMaterial({
230
+ resolutionX: this.resolutionX,
231
+ resolutionY: this.resolutionY,
232
+ });
233
+ this.material.uniforms.uRegion.value.copy(this.region);
234
+
235
+ this.fullscreenScene = new THREE.Scene();
236
+ this.fullscreenCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
237
+ this.quadGeometry = new THREE.PlaneGeometry(2, 2);
238
+ this.quad = new THREE.Mesh(this.quadGeometry, this.material);
239
+ this.quad.frustumCulled = false;
240
+ this.fullscreenScene.add(this.quad);
241
+
242
+ this.setParameters(parameters);
243
+ this.setCurrentField(currentField);
244
+ this.setBedHeightSampler(bedHeightSampler);
245
+ }
246
+
247
+ createTarget() {
248
+ const target = new THREE.WebGLRenderTarget(this.resolutionX, this.resolutionY, {
249
+ type: THREE.HalfFloatType,
250
+ format: THREE.RGBAFormat,
251
+ minFilter: THREE.LinearFilter,
252
+ magFilter: THREE.LinearFilter,
253
+ wrapS: THREE.ClampToEdgeWrapping,
254
+ wrapT: THREE.ClampToEdgeWrapping,
255
+ depthBuffer: false,
256
+ stencilBuffer: false,
257
+ generateMipmaps: false,
258
+ });
259
+ target.texture.name = 'waterShoreState';
260
+ target.texture.colorSpace = THREE.NoColorSpace;
261
+ return target;
262
+ }
263
+
264
+ get texture() {
265
+ return this.targets[this.readIndex].texture;
266
+ }
267
+
268
+ get texelSize() {
269
+ return this.material.uniforms.uTexel.value;
270
+ }
271
+
272
+ getRegion(out = new THREE.Vector4()) {
273
+ return out.copy(this.region);
274
+ }
275
+
276
+ setParameters(parameters = {}) {
277
+ for (const key of Object.keys(NUMERIC_PARAMETER_UNIFORMS)) {
278
+ if (!Number.isFinite(Number(parameters[key]))) continue;
279
+ const isTime = key.endsWith('Time') || key.endsWith('Lifetime');
280
+ const minimum = isTime || key === 'noiseScale' ? 1e-3 : 0;
281
+ this.parameters[key] = Math.max(Number(parameters[key]), minimum);
282
+ }
283
+ if (parameters.baseCurrent !== undefined) {
284
+ const [x, z] = vector2Components(parameters.baseCurrent);
285
+ this.parameters.baseCurrent = [x, z];
286
+ }
287
+
288
+ const uniforms = this.material?.uniforms;
289
+ if (uniforms) {
290
+ for (const [key, uniformName] of Object.entries(NUMERIC_PARAMETER_UNIFORMS)) {
291
+ uniforms[uniformName].value = this.parameters[key];
292
+ }
293
+ uniforms.uBaseCurrent.value.set(...this.parameters.baseCurrent);
294
+ }
295
+ return this;
296
+ }
297
+
298
+ // Binds the compact RGBA8 velocity atlas produced by WaterCurrentField.
299
+ // The field remains caller-owned; this class only reads its texture and
300
+ // mirrors region/encoding uniforms before each update.
301
+ setCurrentField(currentField) {
302
+ if (currentField != null && currentField.isWaterCurrentField !== true) {
303
+ throw new TypeError('WaterShoreStateField currentField must be a WaterCurrentField or null.');
304
+ }
305
+ this.currentField = currentField ?? null;
306
+ this.syncCurrentFieldUniforms();
307
+ return this;
308
+ }
309
+
310
+ syncCurrentFieldUniforms() {
311
+ const uniforms = this.material?.uniforms;
312
+ if (!uniforms) return this;
313
+ if (!this.currentField) {
314
+ uniforms.uUseCurrentMap.value = 0;
315
+ return this;
316
+ }
317
+ uniforms.uCurrentMap.value = this.currentField.texture;
318
+ uniforms.uUseCurrentMap.value = 1;
319
+ uniforms.uCurrentMaxSpeed.value = this.currentField.maxSpeed;
320
+ uniforms.uCurrentStrength.value = this.currentField.strength;
321
+ this.currentField.getRegion(uniforms.uCurrentRegion.value);
322
+ return this;
323
+ }
324
+
325
+ createBedTexture(sampler) {
326
+ const width = this.resolutionX;
327
+ const height = this.resolutionY;
328
+ const count = width * height;
329
+ const heights = this.bedHeightData?.length === count
330
+ ? this.bedHeightData
331
+ : new Float32Array(count);
332
+ const valid = this.bedValidityData?.length === count
333
+ ? this.bedValidityData
334
+ : new Uint8Array(count);
335
+ heights.fill(0);
336
+ valid.fill(0);
337
+ this.bedHeightData = heights;
338
+ this.bedValidityData = valid;
339
+ const worldStepX = this.worldWidth / width;
340
+ const worldStepZ = this.worldDepth / height;
341
+ const minX = this.centerX - this.worldWidth * 0.5;
342
+ const minZ = this.centerZ - this.worldDepth * 0.5;
343
+
344
+ if (sampler) {
345
+ for (let y = 0; y < height; y += 1) {
346
+ const worldZ = minZ + (y + 0.5) * worldStepZ;
347
+ for (let x = 0; x < width; x += 1) {
348
+ const worldX = minX + (x + 0.5) * worldStepX;
349
+ const index = y * width + x;
350
+ const value = Number(sampler(worldX, worldZ));
351
+ if (Number.isFinite(value)) {
352
+ heights[index] = value;
353
+ valid[index] = 1;
354
+ }
355
+ }
356
+ }
357
+ }
358
+
359
+ const data = this.bedTextureData?.length === count * 4
360
+ ? this.bedTextureData
361
+ : new Float32Array(count * 4);
362
+ data.fill(0);
363
+ this.bedTextureData = data;
364
+ for (let y = 0; y < height; y += 1) {
365
+ const row = y * width;
366
+ const downRow = Math.max(y - 1, 0) * width;
367
+ const upRow = Math.min(y + 1, height - 1) * width;
368
+ for (let x = 0; x < width; x += 1) {
369
+ const index = row + x;
370
+ const destination = index * 4;
371
+ if (!valid[index]) continue;
372
+ const center = heights[index];
373
+ const leftIndex = row + Math.max(x - 1, 0);
374
+ const rightIndex = row + Math.min(x + 1, width - 1);
375
+ const downIndex = downRow + x;
376
+ const upIndex = upRow + x;
377
+ const leftValid = valid[leftIndex] === 1;
378
+ const rightValid = valid[rightIndex] === 1;
379
+ const downValid = valid[downIndex] === 1;
380
+ const upValid = valid[upIndex] === 1;
381
+ const gradientX = leftValid && rightValid
382
+ ? (heights[rightIndex] - heights[leftIndex]) /
383
+ (worldStepX * (x > 0 && x < width - 1 ? 2 : 1))
384
+ : rightValid ? (heights[rightIndex] - center) / worldStepX
385
+ : leftValid ? (center - heights[leftIndex]) / worldStepX : 0;
386
+ const gradientZ = downValid && upValid
387
+ ? (heights[upIndex] - heights[downIndex]) /
388
+ (worldStepZ * (y > 0 && y < height - 1 ? 2 : 1))
389
+ : upValid ? (heights[upIndex] - center) / worldStepZ
390
+ : downValid ? (center - heights[downIndex]) / worldStepZ : 0;
391
+ data[destination] = center;
392
+ data[destination + 1] = gradientX;
393
+ data[destination + 2] = gradientZ;
394
+ data[destination + 3] = 1;
395
+ }
396
+ }
397
+
398
+ // Preserve one texture object across terrain switches. Updating its CPU
399
+ // data in-place avoids both the multi-megabyte retired-texture queue and
400
+ // the WebGPU lifetime race caused by disposing a texture still referenced
401
+ // by the current command encoder.
402
+ const texture = this.bedTexture ?? new THREE.DataTexture(
403
+ data, width, height, THREE.RGBAFormat, THREE.FloatType,
404
+ );
405
+ texture.image.data = data;
406
+ texture.image.width = width;
407
+ texture.image.height = height;
408
+ texture.name = 'waterShoreBedField';
409
+ texture.wrapS = THREE.ClampToEdgeWrapping;
410
+ texture.wrapT = THREE.ClampToEdgeWrapping;
411
+ // This atlas has exactly the state-pass resolution and is sampled at its
412
+ // texel centers. Nearest filtering avoids requiring float-linear texture
413
+ // support on the forced-WebGL2 fallback without changing the result.
414
+ texture.minFilter = THREE.NearestFilter;
415
+ texture.magFilter = THREE.NearestFilter;
416
+ texture.generateMipmaps = false;
417
+ texture.flipY = false;
418
+ texture.colorSpace = THREE.NoColorSpace;
419
+ texture.needsUpdate = true;
420
+ return texture;
421
+ }
422
+
423
+ // A bed replacement is a topology change: rebuild the static atlas and
424
+ // clear temporal state on the next renderer update. Water-level changes do
425
+ // not call this and therefore preserve tide marks.
426
+ setBedHeightSampler(bedHeightSampler) {
427
+ if (bedHeightSampler != null && typeof bedHeightSampler !== 'function') {
428
+ throw new TypeError('WaterShoreStateField bedHeightSampler must be a function or null.');
429
+ }
430
+ const nextTexture = this.createBedTexture(bedHeightSampler ?? null);
431
+ this.bedTexture = nextTexture;
432
+ this.bedHeightSampler = bedHeightSampler ?? null;
433
+ this.material.uniforms.uBedMap.value = nextTexture;
434
+ this.readIndex = 0;
435
+ this.timeAccumulator = 0;
436
+ this.needsClear = true;
437
+ return this;
438
+ }
439
+
440
+ clearTargets(renderer) {
441
+ const previousTarget = renderer.getRenderTarget();
442
+ const previousClearColor = renderer.getClearColor(new THREE.Color());
443
+ const previousClearAlpha = renderer.getClearAlpha();
444
+ renderer.setClearColor(0x000000, 0);
445
+ for (const target of this.targets) {
446
+ renderer.setRenderTarget(target);
447
+ renderer.clear(true, false, false);
448
+ }
449
+ renderer.setRenderTarget(previousTarget);
450
+ renderer.setClearColor(previousClearColor, previousClearAlpha);
451
+ this.readIndex = 0;
452
+ this.needsClear = false;
453
+ }
454
+
455
+ writeStaticUniforms({ waterLevel, settings = {}, waveEnergy } = {}) {
456
+ this.syncCurrentFieldUniforms();
457
+ const uniforms = this.material.uniforms;
458
+ const resolvedEnergy = Number.isFinite(Number(waveEnergy))
459
+ ? Math.max(Number(waveEnergy), 0)
460
+ : waveEnergyFrom(settings.waves);
461
+ const explicitRunup = finiteOr(settings.runupDistance, 0) > 0.01;
462
+ const shorelineRunup = Math.max(finiteOr(settings.shorelineRunup, 0.6), 0);
463
+ uniforms.uWaterLevel.value = finiteOr(waterLevel, 0);
464
+ uniforms.uExplicitRunup.value = explicitRunup ? 1 : 0;
465
+ uniforms.uShorelineRunup.value = shorelineRunup;
466
+ uniforms.uWaveEnergy.value = resolvedEnergy;
467
+ uniforms.uSwashEnabled.value = this.bedHeightSampler &&
468
+ (explicitRunup || shorelineRunup * resolvedEnergy > 1e-4) ? 1 : 0;
469
+ // Appearance gain is applied once in the visible water material. Feeding
470
+ // it into temporal injection as well squared the setting and saturated a
471
+ // multi-metre white carpet after only a few updates.
472
+ uniforms.uFoamAmount.value = 1;
473
+ const authoredNoiseScale = Math.max(finiteOr(settings.foamNoiseScale, 0.6), 0.01);
474
+ uniforms.uNoiseScale.value = this.parameters.noiseScale * THREE.MathUtils.clamp(
475
+ authoredNoiseScale / 0.6,
476
+ 0.35,
477
+ 3,
478
+ );
479
+ }
480
+
481
+ writeFrameUniforms({
482
+ time,
483
+ waves = [],
484
+ settings = {},
485
+ swashFrame = null,
486
+ }, sampleTime) {
487
+ const runupDistance = Math.max(finiteOr(settings.runupDistance, 0), 0);
488
+ const fallback = buildFallbackSwashFrame(waves, sampleTime, runupDistance);
489
+ const timeOffset = sampleTime - finiteOr(time, sampleTime);
490
+ const progressSpeed = finiteOr(swashFrame?.progressSpeed, fallback.progressSpeed);
491
+ const edgeDistanceSpeed = finiteOr(
492
+ swashFrame?.edgeDistanceSpeed ?? swashFrame?.edgeSpeed,
493
+ fallback.edgeDistanceSpeed,
494
+ );
495
+ const progress = swashFrame
496
+ ? THREE.MathUtils.clamp(
497
+ finiteOr(swashFrame.progress, fallback.progress) + progressSpeed * timeOffset,
498
+ 0,
499
+ 1,
500
+ )
501
+ : fallback.progress;
502
+ const edgeDistance = swashFrame
503
+ ? finiteOr(swashFrame.edgeDistance, fallback.edgeDistance) + edgeDistanceSpeed * timeOffset
504
+ : fallback.edgeDistance;
505
+ const eventIndex = Math.floor(finiteOr(swashFrame?.eventIndex, fallback.eventIndex));
506
+ const cycleSpeed = finiteOr(swashFrame?.cycleSpeed, fallback.cycleSpeed);
507
+ const cycle = (
508
+ finiteOr(swashFrame?.cycle, fallback.cycle) + cycleSpeed * timeOffset
509
+ ) % 1;
510
+ const edgeShape = swashFrame?.edgeShape ?? fallback.edgeShape;
511
+
512
+ const primary = waves?.[0];
513
+ const authoredDirection = swashFrame?.primaryDirection ??
514
+ (primary ? [primary.dirX, primary.dirZ] : settings.waveDirection);
515
+ const [directionX, directionZ] = vector2Components(authoredDirection, 1, 0);
516
+ const directionLength = Math.hypot(directionX, directionZ) || 1;
517
+ const uniforms = this.material.uniforms;
518
+ uniforms.uTime.value = sampleTime;
519
+ uniforms.uSwashCycle.value = cycle < 0 ? cycle + 1 : cycle;
520
+ uniforms.uSwashCycleSpeed.value = cycleSpeed;
521
+ uniforms.uSwashProgress.value = progress;
522
+ uniforms.uSwashProgressSpeed.value = progressSpeed;
523
+ uniforms.uSwashEdgeDistance.value = edgeDistance;
524
+ uniforms.uSwashEdgeSpeed.value = edgeDistanceSpeed;
525
+ uniforms.uPrimaryDirection.value.set(
526
+ directionX / directionLength,
527
+ directionZ / directionLength,
528
+ );
529
+ uniforms.uSwashEdgeShape.value.set(
530
+ finiteOr(edgeShape?.phase, 0),
531
+ finiteOr(edgeShape?.frequency, 0.1),
532
+ finiteOr(edgeShape?.amplitude, 0),
533
+ );
534
+ if (swashFrame?.noiseOffset !== undefined) {
535
+ const [x, y] = vector2Components(swashFrame.noiseOffset);
536
+ uniforms.uEventNoiseOffset.value.set(x, y);
537
+ } else {
538
+ eventNoiseOffset(eventIndex, this.eventOffsetScratch);
539
+ uniforms.uEventNoiseOffset.value.copy(this.eventOffsetScratch);
540
+ }
541
+ }
542
+
543
+ step(renderer) {
544
+ const uniforms = this.material.uniforms;
545
+ uniforms.uPrevState.value = this.targets[this.readIndex].texture;
546
+ const writeTarget = this.targets[1 - this.readIndex];
547
+ renderer.setRenderTarget(writeTarget);
548
+ renderer.render(this.fullscreenScene, this.fullscreenCamera);
549
+ this.readIndex = 1 - this.readIndex;
550
+ }
551
+
552
+ /**
553
+ * Advances the field. `swashFrame` is optional; when omitted, the class
554
+ * derives it from the same public waterSettings helpers used by WaterSurface.
555
+ */
556
+ update(renderer, {
557
+ time = 0,
558
+ waterLevel = 0,
559
+ waves = [],
560
+ settings = {},
561
+ waveEnergy = undefined,
562
+ swashFrame = null,
563
+ } = {}, delta = 0) {
564
+ if (this.disposed) return this;
565
+ if (!renderer) throw new TypeError('WaterShoreStateField.update requires a renderer.');
566
+ if (this.needsClear) this.clearTargets(renderer);
567
+
568
+ this.timeAccumulator += Math.min(Math.max(finiteOr(delta, 0), 0), 0.1);
569
+ let substeps = Math.floor(this.timeAccumulator / FIXED_TIMESTEP);
570
+ if (substeps <= 0) return this;
571
+ substeps = Math.min(substeps, MAX_SUBSTEPS_PER_FRAME);
572
+ this.timeAccumulator = Math.max(
573
+ 0,
574
+ Math.min(this.timeAccumulator - substeps * FIXED_TIMESTEP, FIXED_TIMESTEP),
575
+ );
576
+
577
+ this.writeStaticUniforms({ waterLevel, settings: { ...settings, waves }, waveEnergy });
578
+ this.material.uniforms.uDelta.value = FIXED_TIMESTEP;
579
+
580
+ const previousTarget = renderer.getRenderTarget();
581
+ const previousXrEnabled = renderer.xr?.enabled;
582
+ if (renderer.xr) renderer.xr.enabled = false;
583
+ try {
584
+ for (let i = 0; i < substeps; i += 1) {
585
+ const sampleTime = finiteOr(time, 0) - (substeps - 1 - i) * FIXED_TIMESTEP;
586
+ this.writeFrameUniforms({ time, waves, settings, swashFrame }, sampleTime);
587
+ this.step(renderer);
588
+ }
589
+ } finally {
590
+ renderer.setRenderTarget(previousTarget);
591
+ if (renderer.xr && previousXrEnabled !== undefined) renderer.xr.enabled = previousXrEnabled;
592
+ }
593
+ return this;
594
+ }
595
+
596
+ dispose() {
597
+ if (this.disposed) return;
598
+ this.disposed = true;
599
+ for (const target of this.targets) target.dispose();
600
+ this.bedTexture?.dispose();
601
+ this.material.dispose();
602
+ this.quadGeometry.dispose();
603
+ this.fullscreenScene.remove(this.quad);
604
+ }
605
+ }