@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,871 @@
1
+ import * as THREE from 'three';
2
+ import { Fn, uniform, vec4 } from 'three/tsl';
3
+ import { NodeMaterial } from 'three/webgpu';
4
+
5
+ import { setEnvironmentCloudShadow } from '../environment/environmentShaderMaterials.js';
6
+ import { environmentSharedUniformNodes } from '../shaders-tsl/environment.js';
7
+ import { createWeatherPresetDocument, resolveWeatherSettings } from './weatherPresets.js';
8
+ import {
9
+ DEFAULT_WEATHER_SETTINGS,
10
+ createWeatherSettings,
11
+ interpolateWeatherSettings,
12
+ mergeWeatherSettings,
13
+ } from './weatherSettings.js';
14
+ import { WeatherPrecipitation } from './weatherPrecipitation.js';
15
+ import { SKY_SCENE_OVERRIDE_PRIORITIES } from '../sky/sceneOverrideLayers.js';
16
+ import { WATER_SCENE_OVERRIDE_PRIORITIES } from '../water/sceneOverrideLayers.js';
17
+
18
+ const scratchCenter = new THREE.Vector3();
19
+ const scratchCamera = new THREE.Vector3();
20
+ const scratchRipple = new THREE.Vector3();
21
+
22
+ function resolveTarget(value) {
23
+ return typeof value === 'function' ? value() : value;
24
+ }
25
+
26
+ function copySettings(settings) {
27
+ return createWeatherSettings(settings);
28
+ }
29
+
30
+ function finiteValue(value) {
31
+ if (value === null || value === undefined || value === '') return null;
32
+ const number = Number(value);
33
+ return Number.isFinite(number) ? number : null;
34
+ }
35
+
36
+ function vectorValue(value, size, { srgb = false } = {}) {
37
+ if (value?.isColor) {
38
+ const color = value.clone();
39
+ if (srgb) color.convertLinearToSRGB();
40
+ return [color.r, color.g, color.b].slice(0, size);
41
+ }
42
+ if (Array.isArray(value)) {
43
+ const channels = value.slice(0, size).map(Number);
44
+ return channels.length === size && channels.every(Number.isFinite) ? channels : null;
45
+ }
46
+ if (value && typeof value === 'object') {
47
+ const keys = size === 2 ? ['x', 'y'] : ['x', 'y', 'z'];
48
+ const channels = keys.map((key, index) => Number(value[key] ?? value[['r', 'g', 'b'][index]]));
49
+ return channels.every(Number.isFinite) ? channels : null;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ function representativeTarget(target) {
55
+ return target?.variantTrees?.[0] ?? target;
56
+ }
57
+
58
+ function targetUniformMaps(target) {
59
+ const source = representativeTarget(target);
60
+ return [
61
+ source?.material?.uniforms,
62
+ source?.canopyMesh?.material?.uniforms,
63
+ source?.trunkMesh?.material?.uniforms,
64
+ ].filter(Boolean);
65
+ }
66
+
67
+ function targetSettingsSources(target) {
68
+ const source = representativeTarget(target);
69
+ return [
70
+ source?.settings?.shared,
71
+ source?.settings?.foliage,
72
+ source?.settings,
73
+ ].filter((value) => value && typeof value === 'object');
74
+ }
75
+
76
+ function readUniform(target, name) {
77
+ for (const uniforms of targetUniformMaps(target)) {
78
+ if (uniforms[name]?.value !== undefined) return uniforms[name].value;
79
+ }
80
+ return undefined;
81
+ }
82
+
83
+ function readSetting(target, key) {
84
+ for (const settings of targetSettingsSources(target)) {
85
+ if (settings[key] !== undefined) return settings[key];
86
+ }
87
+ return undefined;
88
+ }
89
+
90
+ function firstVector(...candidates) {
91
+ for (const [value, size, options] of candidates) {
92
+ const resolved = vectorValue(value, size, options);
93
+ if (resolved) return resolved;
94
+ }
95
+ return null;
96
+ }
97
+
98
+ function firstNumber(...values) {
99
+ for (const value of values) {
100
+ const resolved = finiteValue(value);
101
+ if (resolved !== null) return resolved;
102
+ }
103
+ return null;
104
+ }
105
+
106
+ function snapshotWeatherTarget(target) {
107
+ if (!target || (typeof target !== 'object' && typeof target !== 'function')) return null;
108
+ const statsWind = target.stats?.wind ?? null;
109
+ const wind = {
110
+ direction: firstVector(
111
+ [readUniform(target, 'uWindDirection'), 2],
112
+ [statsWind?.direction, 2],
113
+ [readSetting(target, 'windDirection'), 2],
114
+ ),
115
+ gustFrequency: firstNumber(readUniform(target, 'uGustFrequency'), readSetting(target, 'gustFrequency')),
116
+ gustSpeed: firstNumber(readUniform(target, 'uGustSpeed'), readSetting(target, 'gustSpeed')),
117
+ speed: firstNumber(readUniform(target, 'uWindSpeed'), statsWind?.speed, readSetting(target, 'windSpeed')),
118
+ strength: firstNumber(readUniform(target, 'uWindStrength'), statsWind?.strength, readSetting(target, 'windStrength')),
119
+ };
120
+ const cloudShadow = {
121
+ coverage: firstNumber(readUniform(target, 'uCloudShadowCoverage'), readSetting(target, 'cloudShadowCoverage')),
122
+ scale: firstNumber(readUniform(target, 'uCloudShadowScale'), readSetting(target, 'cloudShadowScale')),
123
+ strength: firstNumber(readUniform(target, 'uCloudShadowStrength'), readSetting(target, 'cloudShadowStrength')),
124
+ velocity: firstVector(
125
+ [readUniform(target, 'uCloudShadowVelocity'), 2],
126
+ [readSetting(target, 'cloudShadowVelocity'), 2],
127
+ ),
128
+ };
129
+ const surfaceWeather = {
130
+ snowCover: firstNumber(readUniform(target, 'uSnowCover')),
131
+ wetness: firstNumber(readUniform(target, 'uWetness')),
132
+ };
133
+ const sun = {
134
+ color: firstVector(
135
+ [readUniform(target, 'uSunColor'), 3, { srgb: true }],
136
+ [readSetting(target, 'sunColor'), 3],
137
+ ),
138
+ direction: firstVector(
139
+ [readUniform(target, 'uSunDirection'), 3],
140
+ [readSetting(target, 'sunDirection'), 3],
141
+ ),
142
+ sky: firstVector(
143
+ [readUniform(target, 'uSkyColor'), 3, { srgb: true }],
144
+ [readSetting(target, 'skyColor'), 3],
145
+ ),
146
+ };
147
+ return { cloudShadow, sun, surfaceWeather, wind };
148
+ }
149
+
150
+ function completeSunState(source, fallback = {}) {
151
+ const input = source && typeof source === 'object' ? source : {};
152
+ const color = firstVector([input.color, 3], [fallback.color, 3]);
153
+ const direction = firstVector([input.direction, 3], [fallback.direction, 3]);
154
+ const sky = firstVector([input.sky, 3], [fallback.sky, 3]);
155
+ return color && direction && sky ? { color, direction, sky } : null;
156
+ }
157
+
158
+ function definedEntries(source) {
159
+ return Object.fromEntries(Object.entries(source).filter(([, value]) => value !== null && value !== undefined));
160
+ }
161
+
162
+ function seededRandom(seed) {
163
+ let state = (Math.trunc(seed) || 1) >>> 0;
164
+ return () => {
165
+ state = (state + 0x6d2b79f5) >>> 0;
166
+ let value = state;
167
+ value = Math.imul(value ^ (value >>> 15), value | 1);
168
+ value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
169
+ return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
170
+ };
171
+ }
172
+
173
+ function weatheredColor(source, tint, darkening, desaturation) {
174
+ const channels = Array.isArray(source) ? source : [source.r, source.g, source.b];
175
+ const result = channels.map((value, index) => value * (tint[index] ?? 1) * (1 - darkening));
176
+ const gray = result[0] * 0.299 + result[1] * 0.587 + result[2] * 0.114;
177
+ return result.map((value) => value + (gray - value) * desaturation);
178
+ }
179
+
180
+ function snapshotSunRig(sunRig) {
181
+ const light = sunRig?.light;
182
+ if (!light) return null;
183
+ const opacity = (mesh) => mesh?.material?.uniforms?.opacity?.value ?? null;
184
+ return {
185
+ beamOpacity: opacity(sunRig.beam),
186
+ color: light.color.clone(),
187
+ diskOpacity: opacity(sunRig.disk),
188
+ intensity: light.intensity,
189
+ shaftOpacity: opacity(sunRig.shaft),
190
+ spillOpacity: opacity(sunRig.spill),
191
+ };
192
+ }
193
+
194
+ function targetWorldPosition(target, output) {
195
+ if (!target) return null;
196
+ if (typeof target.getWorldPosition === 'function') return target.getWorldPosition(output);
197
+ if (target.position) return output.copy(target.position);
198
+ if (Number.isFinite(target.x)) return output.set(target.x, target.y ?? 0, target.z ?? 0);
199
+ return null;
200
+ }
201
+
202
+ function createLightningFlash() {
203
+ const uniforms = {
204
+ uColor: uniform(new THREE.Color(0xb8ccff)),
205
+ uIntensity: uniform(0),
206
+ };
207
+ const material = new NodeMaterial();
208
+ material.name = 'WeatherLightningFlash';
209
+ material.transparent = true;
210
+ material.depthTest = false;
211
+ material.depthWrite = false;
212
+ material.blending = THREE.AdditiveBlending;
213
+ material.fog = false;
214
+ material.lights = false;
215
+ material.side = THREE.BackSide;
216
+ material.fragmentNode = Fn(() => vec4(uniforms.uColor, uniforms.uIntensity))();
217
+ material.uniforms = uniforms;
218
+
219
+ const mesh = new THREE.Mesh(new THREE.SphereGeometry(420, 20, 12), material);
220
+ mesh.name = 'Weather lightning flash';
221
+ mesh.frustumCulled = false;
222
+ mesh.renderOrder = 100;
223
+ mesh.intensity = 0;
224
+ mesh.userData.environmentShaderExclude = true;
225
+ mesh.userData.waterExclude = true;
226
+ return mesh;
227
+ }
228
+
229
+ /**
230
+ * Cross-system weather coordinator. It constructs precipitation/lightning and
231
+ * temporarily owns its named runtime layers; every other response goes through
232
+ * public adapters whose captured baseline is restored on disposal.
233
+ */
234
+ export class WeatherSystem extends THREE.EventDispatcher {
235
+ constructor({
236
+ ambientFx = null,
237
+ camera = null,
238
+ cloudShadowBaseline = null,
239
+ environmentRoot = null,
240
+ fauna = null,
241
+ flowers = null,
242
+ followTarget = null,
243
+ forest = null,
244
+ getSun = null,
245
+ grass = null,
246
+ groundHeightAt = null,
247
+ lighting = null,
248
+ onLightning = null,
249
+ onSurfaceChange = null,
250
+ onThunder = null,
251
+ precipitationFloorY = 0,
252
+ preset = 'call_me_sensei',
253
+ renderer = null,
254
+ scene = null,
255
+ seed = 1,
256
+ setCloudShadow = null,
257
+ setSun = null,
258
+ settings = {},
259
+ sky = null,
260
+ surfaceBaseline = null,
261
+ sunRig = null,
262
+ water = null,
263
+ } = {}) {
264
+ super();
265
+ this.scene = scene;
266
+ this.renderer = renderer;
267
+ this.camera = camera;
268
+ this.followTarget = followTarget;
269
+ this.environmentRoot = environmentRoot;
270
+ this.targets = { ambientFx, fauna, flowers, forest, grass, lighting, sky, sunRig, water };
271
+ this.groundHeightAt = typeof groundHeightAt === 'function' ? groundHeightAt : null;
272
+ this.precipitationFloorY = Number(precipitationFloorY) || 0;
273
+ this.setCloudShadowAdapter = typeof setCloudShadow === 'function' ? setCloudShadow : null;
274
+ this.getSunAdapter = typeof getSun === 'function' ? getSun : null;
275
+ this.setSunAdapter = typeof setSun === 'function' ? setSun : null;
276
+ this.onSurfaceChange = typeof onSurfaceChange === 'function' ? onSurfaceChange : null;
277
+ this.random = seededRandom(seed);
278
+ this.root = new THREE.Group();
279
+ this.root.name = 'WeatherSystem';
280
+ this.root.userData.environmentShaderExclude = true;
281
+ this.root.userData.waterExclude = true;
282
+
283
+ this.currentPreset = preset;
284
+ this.settings = resolveWeatherSettings(preset, settings);
285
+ this.targetSettings = copySettings(this.settings);
286
+ this.transition = null;
287
+ this._dirty = true;
288
+ this._rippleAccumulator = 0;
289
+ this._lightningCountdown = 0;
290
+ this._flashRemaining = 0;
291
+ this._thunderQueue = [];
292
+ this._disposed = false;
293
+ this._targetBaselines = new WeakMap();
294
+
295
+ const precipitation = new WeatherPrecipitation({
296
+ maxParticles: this.settings.precipitation.maxParticles,
297
+ seed,
298
+ settings: this.settings.precipitation,
299
+ });
300
+ this.precipitation = precipitation;
301
+ this.root.add(precipitation);
302
+
303
+ // A camera-enclosing additive flash avoids mutating the scene-light
304
+ // layout during a WebGPU submit. The public name stays `lightningLight`
305
+ // for compatibility with the original coordinator API.
306
+ this.lightningLight = createLightningFlash();
307
+ this.root.add(this.lightningLight);
308
+
309
+ this._originalFog = scene?.fog ?? null;
310
+ this._baseFog = this._originalFog?.isFog
311
+ ? { color: this._originalFog.color.clone(), far: this._originalFog.far, near: this._originalFog.near }
312
+ : { color: new THREE.Color(0.72, 0.83, 0.94), far: 900, near: 180 };
313
+ this._weatherFog = this._originalFog?.isFog
314
+ ? this._originalFog
315
+ : new THREE.Fog(this._baseFog.color.clone(), this._baseFog.near, this._baseFog.far);
316
+
317
+ const skyTarget = resolveTarget(sky);
318
+ this._baseSky = skyTarget?.settings ? structuredClone(skyTarget.settings) : null;
319
+ this._skyOverrideLayer = Symbol('ToonLab WeatherSystem sky layer');
320
+ this._baseSun = snapshotSunRig(resolveTarget(sunRig));
321
+ const waterTarget = resolveTarget(water);
322
+ this._baseWaterWaveIntensity = Number(waterTarget?.settings?.waveIntensity) || 0;
323
+ this._waterOverrideLayer = Symbol('ToonLab WeatherSystem water layer');
324
+ this._baseEnvironmentCloudShadow = {
325
+ coverage: environmentSharedUniformNodes.cloudShadowCoverage.value,
326
+ scale: environmentSharedUniformNodes.cloudShadowScale.value,
327
+ strength: environmentSharedUniformNodes.cloudShadowStrength.value,
328
+ velocity: vectorValue(environmentSharedUniformNodes.cloudShadowVelocity.value, 2),
329
+ };
330
+ this._baseCloudShadow = cloudShadowBaseline && typeof cloudShadowBaseline === 'object'
331
+ ? structuredClone(cloudShadowBaseline)
332
+ : (this.setCloudShadowAdapter ? { strength: 0 } : structuredClone(this._baseEnvironmentCloudShadow));
333
+ this._baseSurfaceState = {
334
+ ...DEFAULT_WEATHER_SETTINGS.surface,
335
+ ...(surfaceBaseline && typeof surfaceBaseline === 'object' ? structuredClone(surfaceBaseline) : {}),
336
+ };
337
+ this._ambientLights = new Map();
338
+ scene?.traverse?.((object) => {
339
+ if (object.isAmbientLight || object.isHemisphereLight) this._ambientLights.set(object, object.intensity);
340
+ });
341
+
342
+ for (const target of this._currentTransientTargets()) this._rememberTarget(target);
343
+ let adapterSun = null;
344
+ try {
345
+ adapterSun = this.getSunAdapter?.() ?? null;
346
+ } catch {
347
+ // A host getter is optional; target/Sky settings below remain a safe fallback.
348
+ }
349
+ const firstTargetSun = this._currentTransientTargets()
350
+ .map((target) => this._targetBaselines.get(target)?.sun)
351
+ .find((value) => value?.color && value?.direction && value?.sky);
352
+ this._baseSceneSun = completeSunState(adapterSun, firstTargetSun ?? {
353
+ color: this._baseSky?.sunColor,
354
+ direction: this._baseSky?.sunDirection,
355
+ sky: this._baseSky?.zenithColor,
356
+ });
357
+
358
+ if (onLightning) this.addEventListener('lightning', onLightning);
359
+ if (onThunder) this.addEventListener('thunder', onThunder);
360
+ scene?.add?.(this.root);
361
+ this._applyFrame(this.settings);
362
+ }
363
+
364
+ _currentTransientTargets() {
365
+ return [...new Set([
366
+ resolveTarget(this.targets.ambientFx),
367
+ resolveTarget(this.targets.fauna),
368
+ resolveTarget(this.targets.flowers),
369
+ resolveTarget(this.targets.forest),
370
+ resolveTarget(this.targets.grass),
371
+ resolveTarget(this.targets.water),
372
+ ].filter(Boolean))];
373
+ }
374
+
375
+ _rememberTarget(target) {
376
+ if (target && !this._targetBaselines.has(target)) {
377
+ this._targetBaselines.set(target, snapshotWeatherTarget(target));
378
+ }
379
+ return target;
380
+ }
381
+
382
+ _applyStandaloneSun(atmosphere) {
383
+ if (!this._baseSceneSun) return;
384
+ const sceneSun = {
385
+ color: this._baseSceneSun.color.map((value, index) =>
386
+ value * (atmosphere.sunTint[index] ?? 1)),
387
+ direction: this._baseSceneSun.direction.slice(),
388
+ sky: weatheredColor(
389
+ this._baseSceneSun.sky,
390
+ atmosphere.skyTint,
391
+ atmosphere.skyDarkening,
392
+ atmosphere.skyDesaturation,
393
+ ),
394
+ };
395
+ if (this.setSunAdapter) {
396
+ this.setSunAdapter(sceneSun);
397
+ return;
398
+ }
399
+ for (const target of this._currentTransientTargets()) {
400
+ this._rememberTarget(target)?.setSun?.(sceneSun);
401
+ }
402
+ }
403
+
404
+ _restoreTransientTargets({ restoreSun = true } = {}) {
405
+ const ambientFx = resolveTarget(this.targets.ambientFx);
406
+ for (const target of this._currentTransientTargets()) {
407
+ const baseline = this._targetBaselines.get(target) ?? snapshotWeatherTarget(target);
408
+ if (target.setWind) {
409
+ const wind = definedEntries(baseline?.wind ?? {});
410
+ if (target === ambientFx) {
411
+ target.setWind(Object.keys(wind).length > 0 ? {
412
+ ...(wind.direction ? { windDirection: wind.direction } : {}),
413
+ ...(wind.speed !== undefined ? { windSpeed: wind.speed } : {}),
414
+ ...(wind.strength !== undefined ? { windStrength: wind.strength } : {}),
415
+ } : { windStrength: 0 });
416
+ } else {
417
+ target.setWind(Object.keys(wind).length > 0 ? wind : { strength: 0 });
418
+ }
419
+ }
420
+ if (target.setSurfaceWeather) {
421
+ const surfaceWeather = definedEntries(baseline?.surfaceWeather ?? {});
422
+ target.setSurfaceWeather(Object.keys(surfaceWeather).length > 0
423
+ ? surfaceWeather
424
+ : { snowCover: 0, wetness: 0 });
425
+ }
426
+ if (target.setCloudShadow) {
427
+ const cloudShadow = definedEntries(baseline?.cloudShadow ?? {});
428
+ target.setCloudShadow(Object.keys(cloudShadow).length > 0
429
+ ? cloudShadow
430
+ : { strength: 0 });
431
+ }
432
+ if (restoreSun && !this.setSunAdapter && target.setSun) {
433
+ const sun = completeSunState(baseline?.sun, this._baseSceneSun ?? {});
434
+ if (sun) target.setSun(sun);
435
+ }
436
+ }
437
+ }
438
+
439
+ get state() {
440
+ return {
441
+ preset: this.currentPreset,
442
+ settings: copySettings(this.settings),
443
+ transitioning: Boolean(this.transition),
444
+ transitionProgress: this.transition
445
+ ? Math.min(this.transition.elapsed / this.transition.duration, 1)
446
+ : 1,
447
+ };
448
+ }
449
+
450
+ /** Active one-writer Lighting coordinator, if the host attached one. */
451
+ get lightingSystem() {
452
+ return resolveTarget(this.targets.lighting);
453
+ }
454
+
455
+ /** True after teardown; useful to coordinators resolving detach ownership. */
456
+ get disposed() {
457
+ return this._disposed;
458
+ }
459
+
460
+ /** Pre-Weather scene sun captured from the optional world adapter. */
461
+ get sunBaseline() {
462
+ return this._baseSceneSun ? structuredClone(this._baseSceneSun) : null;
463
+ }
464
+
465
+ /** Pre-Weather physical sun-rig state, serialized for ownership handoff. */
466
+ get sunRigBaseline() {
467
+ if (!this._baseSun) return null;
468
+ return {
469
+ beamOpacity: this._baseSun.beamOpacity,
470
+ color: this._baseSun.color.toArray(),
471
+ diskOpacity: this._baseSun.diskOpacity,
472
+ intensity: this._baseSun.intensity,
473
+ shaftOpacity: this._baseSun.shaftOpacity,
474
+ spillOpacity: this._baseSun.spillOpacity,
475
+ };
476
+ }
477
+
478
+ /**
479
+ * Hands sun, ambient, and fog-color ownership to Lighting. Weather then
480
+ * supplies modulation only; removing Lighting restores the direct fallback.
481
+ */
482
+ setLightingSystem(lighting = null) {
483
+ if (this._disposed) return this;
484
+ const previous = this.lightingSystem;
485
+ if (previous === lighting) return this;
486
+ previous?.setWeatherModulation?.();
487
+ if (lighting) {
488
+ const sunRig = resolveTarget(this.targets.sunRig);
489
+ if (sunRig?.light && this._baseSun) {
490
+ sunRig.setState?.({ color: this._baseSun.color, intensity: this._baseSun.intensity });
491
+ }
492
+ for (const [light, intensity] of this._ambientLights) light.intensity = intensity;
493
+ if (this._originalFog?.isFog) this._originalFog.color.copy(this._baseFog.color);
494
+ }
495
+ this.targets.lighting = lighting;
496
+ this._dirty = true;
497
+ this._applyFrame(this.settings);
498
+ return this;
499
+ }
500
+
501
+ setPreset(name, overrides = {}) {
502
+ if (this._disposed) return this;
503
+ this.currentPreset = name;
504
+ this.settings = resolveWeatherSettings(name, overrides);
505
+ this.targetSettings = copySettings(this.settings);
506
+ this.transition = null;
507
+ this._dirty = true;
508
+ this._applyFrame(this.settings);
509
+ return this;
510
+ }
511
+
512
+ applySettings(overrides = {}, { duration = 0 } = {}) {
513
+ if (this._disposed) return this;
514
+ const target = mergeWeatherSettings(this.settings, overrides);
515
+ if (duration > 0) return this.transitionTo(target, { duration });
516
+ this.currentPreset = null;
517
+ this.settings = target;
518
+ this.targetSettings = copySettings(target);
519
+ this.transition = null;
520
+ this._dirty = true;
521
+ this._applyFrame(this.settings);
522
+ return this;
523
+ }
524
+
525
+ transitionTo(presetOrSettings, { duration = 4, overrides = {} } = {}) {
526
+ if (this._disposed) return this;
527
+ const target = typeof presetOrSettings === 'string'
528
+ ? resolveWeatherSettings(presetOrSettings, overrides)
529
+ : mergeWeatherSettings(createWeatherSettings(presetOrSettings), overrides);
530
+ this.currentPreset = typeof presetOrSettings === 'string' ? presetOrSettings : null;
531
+ this.targetSettings = target;
532
+ const seconds = Math.max(Number(duration) || 0, 0);
533
+ if (seconds === 0) {
534
+ this.settings = copySettings(target);
535
+ this.transition = null;
536
+ this._applyFrame(this.settings);
537
+ return this;
538
+ }
539
+ this.transition = { duration: seconds, elapsed: 0, from: copySettings(this.settings), to: copySettings(target) };
540
+ return this;
541
+ }
542
+
543
+ setWeather(presetOrSettings, options) {
544
+ return this.transitionTo(presetOrSettings, options);
545
+ }
546
+
547
+ refresh() {
548
+ if (this._disposed) return this;
549
+ this._dirty = true;
550
+ this._applyFrame(this.settings);
551
+ return this;
552
+ }
553
+
554
+ _applyFrame(settings) {
555
+ const atmosphere = settings.atmosphere;
556
+ const wind = settings.wind;
557
+ const lighting = this.lightingSystem;
558
+ for (const target of this._currentTransientTargets()) this._rememberTarget(target);
559
+ const sky = resolveTarget(this.targets.sky);
560
+ if (sky?.setSceneOverrideLayer) {
561
+ sky.setSceneOverrideLayer(this._skyOverrideLayer, (base) => ({
562
+ cloudColor: weatheredColor(base.cloudColor, atmosphere.skyTint, atmosphere.skyDarkening * 0.45, atmosphere.skyDesaturation),
563
+ cloudCoverage: atmosphere.cloudCoverage,
564
+ cloudShadeColor: weatheredColor(base.cloudShadeColor, atmosphere.skyTint, atmosphere.skyDarkening * 0.7, atmosphere.skyDesaturation),
565
+ cloudSpeed: base.cloudSpeed * atmosphere.cloudSpeed,
566
+ groundColor: weatheredColor(base.groundColor, atmosphere.skyTint, atmosphere.skyDarkening, atmosphere.skyDesaturation),
567
+ horizonColor: weatheredColor(base.horizonColor, atmosphere.skyTint, atmosphere.skyDarkening * 0.7, atmosphere.skyDesaturation),
568
+ zenithColor: weatheredColor(base.zenithColor, atmosphere.skyTint, atmosphere.skyDarkening, atmosphere.skyDesaturation),
569
+ }), { priority: SKY_SCENE_OVERRIDE_PRIORITIES.weather });
570
+ } else if ((sky?.setSceneOverrides || sky?.applySettings) && this._baseSky) {
571
+ const overrides = {
572
+ cloudColor: weatheredColor(this._baseSky.cloudColor, atmosphere.skyTint, atmosphere.skyDarkening * 0.45, atmosphere.skyDesaturation),
573
+ cloudCoverage: atmosphere.cloudCoverage,
574
+ cloudShadeColor: weatheredColor(this._baseSky.cloudShadeColor, atmosphere.skyTint, atmosphere.skyDarkening * 0.7, atmosphere.skyDesaturation),
575
+ cloudSpeed: this._baseSky.cloudSpeed * atmosphere.cloudSpeed,
576
+ groundColor: weatheredColor(this._baseSky.groundColor, atmosphere.skyTint, atmosphere.skyDarkening, atmosphere.skyDesaturation),
577
+ horizonColor: weatheredColor(this._baseSky.horizonColor, atmosphere.skyTint, atmosphere.skyDarkening * 0.7, atmosphere.skyDesaturation),
578
+ zenithColor: weatheredColor(this._baseSky.zenithColor, atmosphere.skyTint, atmosphere.skyDarkening, atmosphere.skyDesaturation),
579
+ };
580
+ if (sky.setSceneOverrides) sky.setSceneOverrides(overrides);
581
+ else sky.applySettings(overrides);
582
+ }
583
+
584
+ const sunRig = resolveTarget(this.targets.sunRig);
585
+ if (lighting?.setWeatherModulation) {
586
+ lighting.setWeatherModulation({
587
+ ambientScale: atmosphere.ambientIntensity,
588
+ fogColorOverride: atmosphere.fogColor,
589
+ sunColorTint: atmosphere.sunTint,
590
+ sunIntensityScale: atmosphere.sunIntensity,
591
+ });
592
+ } else if (sunRig?.light && this._baseSun) {
593
+ const color = this._baseSun.color.clone();
594
+ color.multiply(new THREE.Color(...atmosphere.sunTint));
595
+ sunRig.setState?.({
596
+ beamOpacity: this._baseSun.beamOpacity === null ? undefined : this._baseSun.beamOpacity * atmosphere.sunIntensity,
597
+ color,
598
+ diskOpacity: this._baseSun.diskOpacity === null ? undefined : this._baseSun.diskOpacity * atmosphere.sunIntensity,
599
+ intensity: this._baseSun.intensity * atmosphere.sunIntensity,
600
+ shaftOpacity: this._baseSun.shaftOpacity === null ? undefined : this._baseSun.shaftOpacity * atmosphere.sunIntensity,
601
+ spillOpacity: this._baseSun.spillOpacity === null ? undefined : this._baseSun.spillOpacity * atmosphere.sunIntensity,
602
+ });
603
+ }
604
+ if (!lighting?.setWeatherModulation) {
605
+ this._applyStandaloneSun(atmosphere);
606
+ for (const [light, intensity] of this._ambientLights) {
607
+ light.intensity = intensity * atmosphere.ambientIntensity;
608
+ }
609
+ }
610
+
611
+ if (this.scene) {
612
+ this.scene.fog = this._weatherFog;
613
+ if (!lighting?.setWeatherModulation) {
614
+ const color = atmosphere.fogColor
615
+ ? new THREE.Color(...atmosphere.fogColor)
616
+ : this._baseFog.color;
617
+ this._weatherFog.color.copy(color);
618
+ }
619
+ this._weatherFog.near = this._baseFog.near / atmosphere.fogRangeScale;
620
+ this._weatherFog.far = this._baseFog.far / atmosphere.fogRangeScale;
621
+ }
622
+
623
+ const cloudShadow = {
624
+ coverage: atmosphere.cloudShadowCoverage,
625
+ scale: atmosphere.cloudShadowScale,
626
+ strength: atmosphere.cloudShadowStrength,
627
+ velocity: [
628
+ wind.direction[0] * wind.speed * 0.012,
629
+ wind.direction[1] * wind.speed * 0.012,
630
+ ],
631
+ };
632
+ if (this.setCloudShadowAdapter) this.setCloudShadowAdapter(cloudShadow);
633
+ else {
634
+ setEnvironmentCloudShadow(cloudShadow);
635
+ resolveTarget(this.targets.water)?.setCloudShadow?.(cloudShadow);
636
+ resolveTarget(this.targets.grass)?.setCloudShadow?.(cloudShadow);
637
+ resolveTarget(this.targets.flowers)?.setCloudShadow?.(cloudShadow);
638
+ resolveTarget(this.targets.forest)?.setCloudShadow?.(cloudShadow);
639
+ resolveTarget(this.targets.fauna)?.setCloudShadow?.(cloudShadow);
640
+ }
641
+
642
+ resolveTarget(this.targets.grass)?.setWind?.({
643
+ direction: wind.direction,
644
+ gustFrequency: wind.gustFrequency,
645
+ gustSpeed: wind.gustSpeed,
646
+ speed: wind.speed,
647
+ strength: wind.strength,
648
+ });
649
+ resolveTarget(this.targets.flowers)?.setWind?.({
650
+ direction: wind.direction,
651
+ speed: wind.speed,
652
+ strength: wind.strength,
653
+ });
654
+ resolveTarget(this.targets.forest)?.setWind?.({
655
+ direction: wind.direction,
656
+ speed: wind.speed,
657
+ strength: wind.strength,
658
+ });
659
+ resolveTarget(this.targets.ambientFx)?.setWind?.({
660
+ windDirection: wind.direction,
661
+ windSpeed: wind.speed,
662
+ windStrength: wind.strength,
663
+ });
664
+
665
+ const water = resolveTarget(this.targets.water);
666
+ if (water?.setSceneOverrideLayer) {
667
+ water.setSceneOverrideLayer(this._waterOverrideLayer, (base) => ({
668
+ waveIntensity: Math.min(base.waveIntensity + settings.surface.waterWaveBoost, 1),
669
+ }), { priority: WATER_SCENE_OVERRIDE_PRIORITIES.weather });
670
+ } else {
671
+ water?.applySettings?.({
672
+ waveIntensity: Math.min(this._baseWaterWaveIntensity + settings.surface.waterWaveBoost, 1),
673
+ });
674
+ }
675
+ const surfaceWeather = {
676
+ snowCover: settings.surface.snowCover,
677
+ wetness: settings.surface.wetness,
678
+ };
679
+ resolveTarget(this.targets.grass)?.setSurfaceWeather?.(surfaceWeather);
680
+ resolveTarget(this.targets.flowers)?.setSurfaceWeather?.(surfaceWeather);
681
+ resolveTarget(this.targets.forest)?.setSurfaceWeather?.(surfaceWeather);
682
+ this.precipitation.applySettings(settings.precipitation, wind);
683
+ this.lightningLight.material.uniforms.uColor.value.setRGB(
684
+ ...settings.lightning.color,
685
+ THREE.SRGBColorSpace,
686
+ );
687
+ this.root.userData.weatherSurface = { ...settings.surface };
688
+ this.onSurfaceChange?.({ ...settings.surface });
689
+ this.dispatchEvent({ settings: copySettings(settings), type: 'change' });
690
+ this._dirty = false;
691
+ }
692
+
693
+ _resolveCenter() {
694
+ const target = resolveTarget(this.followTarget);
695
+ const camera = resolveTarget(this.camera);
696
+ const targetPosition = targetWorldPosition(target, scratchCenter);
697
+ const cameraPosition = targetWorldPosition(camera, scratchCamera);
698
+ const position = targetPosition ?? cameraPosition ?? scratchCenter.set(0, 0, 0);
699
+ let floorY = targetPosition?.y ?? this.precipitationFloorY;
700
+ if (this.groundHeightAt) floorY = Number(this.groundHeightAt(position.x, position.z)) || floorY;
701
+ return scratchCenter.set(position.x, floorY, position.z);
702
+ }
703
+
704
+ _scheduleLightning() {
705
+ const rate = this.settings.lightning.strikesPerMinute;
706
+ if (!(rate > 0)) {
707
+ this._lightningCountdown = Infinity;
708
+ return;
709
+ }
710
+ this._lightningCountdown = (60 / rate) * (0.5 + this.random());
711
+ }
712
+
713
+ triggerLightning() {
714
+ if (this._disposed) return this;
715
+ const lightning = this.settings.lightning;
716
+ if (!lightning.enabled) return this;
717
+ const center = this._resolveCenter();
718
+ const angle = this.random() * Math.PI * 2;
719
+ const distance = 60 + this.random() * 240;
720
+ this.lightningLight.position.set(
721
+ center.x + Math.cos(angle) * distance,
722
+ center.y + 35 + this.random() * 80,
723
+ center.z + Math.sin(angle) * distance,
724
+ );
725
+ this._flashRemaining = lightning.duration;
726
+ const thunderDelay = Math.max(distance / 343, 0.08);
727
+ const event = { distance, position: this.lightningLight.position.clone(), thunderDelay, type: 'lightning' };
728
+ this.dispatchEvent(event);
729
+ this._thunderQueue.push({ distance, remaining: thunderDelay });
730
+ return this;
731
+ }
732
+
733
+ _updateLightning(delta) {
734
+ const lightning = this.settings.lightning;
735
+ if (lightning.enabled && lightning.strikesPerMinute > 0) {
736
+ if (!Number.isFinite(this._lightningCountdown) || this._lightningCountdown <= 0) this._scheduleLightning();
737
+ this._lightningCountdown -= delta;
738
+ if (this._lightningCountdown <= 0) {
739
+ this.triggerLightning();
740
+ this._scheduleLightning();
741
+ }
742
+ } else {
743
+ this._lightningCountdown = Infinity;
744
+ }
745
+
746
+ if (this._flashRemaining > 0) {
747
+ this._flashRemaining = Math.max(this._flashRemaining - delta, 0);
748
+ const phase = this._flashRemaining / Math.max(lightning.duration, 0.01);
749
+ const stutter = Math.sin(phase * 42) > 0.15 ? 1 : 0.22;
750
+ this.lightningLight.intensity = lightning.intensity * phase * phase * stutter;
751
+ } else {
752
+ this.lightningLight.intensity = 0;
753
+ }
754
+ this.lightningLight.material.uniforms.uIntensity.value = Math.min(
755
+ this.lightningLight.intensity * 0.075,
756
+ 0.72,
757
+ );
758
+
759
+ for (let index = this._thunderQueue.length - 1; index >= 0; index -= 1) {
760
+ const thunder = this._thunderQueue[index];
761
+ thunder.remaining -= delta;
762
+ if (thunder.remaining <= 0) {
763
+ this.dispatchEvent({ distance: thunder.distance, type: 'thunder' });
764
+ this._thunderQueue.splice(index, 1);
765
+ }
766
+ }
767
+ }
768
+
769
+ _updateWaterRipples(delta, center) {
770
+ const water = resolveTarget(this.targets.water);
771
+ const rate = this.settings.surface.waterRippleRate;
772
+ if (!water?.addRipple || !(rate > 0)) return;
773
+ this._rippleAccumulator += rate * delta;
774
+ let count = Math.min(Math.floor(this._rippleAccumulator), 5);
775
+ this._rippleAccumulator -= count;
776
+ const radius = this.settings.precipitation.areaSize * 0.42;
777
+ while (count > 0) {
778
+ count -= 1;
779
+ const angle = this.random() * Math.PI * 2;
780
+ const distance = Math.sqrt(this.random()) * radius;
781
+ scratchRipple.set(
782
+ center.x + Math.cos(angle) * distance,
783
+ water.position?.y ?? 0,
784
+ center.z + Math.sin(angle) * distance,
785
+ );
786
+ water.addRipple(scratchRipple, {
787
+ radius: 0.14 + this.random() * 0.2,
788
+ strength: 0.12 + this.settings.precipitation.intensity * 0.2,
789
+ });
790
+ }
791
+ }
792
+
793
+ update(delta = 0.016) {
794
+ if (this._disposed) return this;
795
+ const dt = Math.min(Math.max(Number(delta) || 0.016, 0), 0.1);
796
+ if (this.transition) {
797
+ this.transition.elapsed = Math.min(this.transition.elapsed + dt, this.transition.duration);
798
+ const raw = this.transition.elapsed / this.transition.duration;
799
+ const eased = raw * raw * (3 - 2 * raw);
800
+ this.settings = interpolateWeatherSettings(this.transition.from, this.transition.to, eased);
801
+ this._applyFrame(this.settings);
802
+ if (raw >= 1) {
803
+ this.settings = copySettings(this.transition.to);
804
+ this.transition = null;
805
+ }
806
+ } else if (this._dirty) {
807
+ this._applyFrame(this.settings);
808
+ }
809
+ const center = this._resolveCenter();
810
+ this.precipitation.update(dt, { center, renderer: resolveTarget(this.renderer) });
811
+ this._updateLightning(dt);
812
+ this._updateWaterRipples(dt, center);
813
+ return this;
814
+ }
815
+
816
+ toJSON({ id = this.currentPreset ?? 'custom-weather', label = 'Custom Weather' } = {}) {
817
+ return createWeatherPresetDocument(id, { label, settings: this.targetSettings });
818
+ }
819
+
820
+ dispose() {
821
+ if (this._disposed) return;
822
+ this._disposed = true;
823
+ this.precipitation.dispose();
824
+ this.lightningLight.geometry.dispose();
825
+ this.lightningLight.material.dispose();
826
+ this.root.parent?.remove(this.root);
827
+ const lighting = this.lightingSystem;
828
+ this.targets.lighting = null;
829
+ lighting?.setWeatherModulation?.();
830
+ if (this._originalFog?.isFog) {
831
+ if (!lighting?.setWeatherModulation) this._originalFog.color.copy(this._baseFog.color);
832
+ this._originalFog.near = this._baseFog.near;
833
+ this._originalFog.far = this._baseFog.far;
834
+ }
835
+ if (this.scene) this.scene.fog = this._originalFog;
836
+ if (!lighting?.setWeatherModulation) {
837
+ for (const [light, intensity] of this._ambientLights) light.intensity = intensity;
838
+ }
839
+ const sky = resolveTarget(this.targets.sky);
840
+ if (sky?.clearSceneOverrideLayer) {
841
+ sky.clearSceneOverrideLayer(this._skyOverrideLayer);
842
+ } else if (sky?.clearSceneOverrides) {
843
+ sky.clearSceneOverrides();
844
+ } else if (sky?.applySettings && this._baseSky) {
845
+ sky.applySettings(this._baseSky);
846
+ }
847
+ const sunRig = resolveTarget(this.targets.sunRig);
848
+ if (!lighting?.setWeatherModulation && sunRig?.light && this._baseSun) {
849
+ sunRig.setState?.({ color: this._baseSun.color, intensity: this._baseSun.intensity });
850
+ }
851
+ const water = resolveTarget(this.targets.water);
852
+ if (water?.clearSceneOverrideLayer) water.clearSceneOverrideLayer(this._waterOverrideLayer);
853
+ else water?.applySettings?.({ waveIntensity: this._baseWaterWaveIntensity });
854
+
855
+ // Weather owns these values only while alive. Restore exact live target
856
+ // baselines when they were inspectable; otherwise disable the effect
857
+ // explicitly instead of leaving the last storm frame behind.
858
+ if (this.setCloudShadowAdapter) this.setCloudShadowAdapter(structuredClone(this._baseCloudShadow));
859
+ else setEnvironmentCloudShadow(this._baseEnvironmentCloudShadow);
860
+ this._restoreTransientTargets({ restoreSun: !lighting?.setWeatherModulation });
861
+ if (!lighting?.setWeatherModulation && this.setSunAdapter && this._baseSceneSun) {
862
+ this.setSunAdapter(structuredClone(this._baseSceneSun));
863
+ }
864
+ this.root.userData.weatherSurface = structuredClone(this._baseSurfaceState);
865
+ this.onSurfaceChange?.(structuredClone(this._baseSurfaceState));
866
+ }
867
+ }
868
+
869
+ export function createWeatherSystem(options = {}) {
870
+ return new WeatherSystem(options);
871
+ }