@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,193 @@
1
+ import * as THREE from 'three';
2
+
3
+ const workingPosition = new THREE.Vector3();
4
+
5
+ // Tracks objects interacting with a WaterSurface and converts their motion
6
+ // into ripples, wakes, and splashes:
7
+ // - fast downward crossing of the surface -> entry splash + ripple ring
8
+ // - fast upward exit -> smaller exit splash
9
+ // - horizontal movement while touching the water -> wake ripples + foam
10
+ //
11
+ // Interactor sources can be a THREE.Object3D or a function/object producing a
12
+ // world position, so physics bodies (e.g. rapier) hook in without adapters.
13
+ export class WaterInteractionManager {
14
+ constructor(surface, {
15
+ wakeInterval = 0.07,
16
+ entrySplashSpeed = 1.4,
17
+ exitSplashSpeed = 2.2,
18
+ minWakeSpeed = 0.12,
19
+ } = {}) {
20
+ this.surface = surface;
21
+ this.wakeInterval = wakeInterval;
22
+ this.entrySplashSpeed = entrySplashSpeed;
23
+ this.exitSplashSpeed = exitSplashSpeed;
24
+ this.minWakeSpeed = minWakeSpeed;
25
+ this.interactors = new Map();
26
+ this.nextId = 1;
27
+ }
28
+
29
+ resolvePosition(source, out) {
30
+ if (typeof source === 'function') {
31
+ const result = source(out);
32
+ if (result && result !== out) out.set(result.x ?? 0, result.y ?? 0, result.z ?? 0);
33
+ return out;
34
+ }
35
+ if (source?.isObject3D) return source.getWorldPosition(out);
36
+ if (typeof source?.getPosition === 'function') {
37
+ const result = source.getPosition(out);
38
+ if (result && result !== out) out.set(result.x ?? 0, result.y ?? 0, result.z ?? 0);
39
+ return out;
40
+ }
41
+ if (source && Number.isFinite(source.x)) return out.set(source.x, source.y ?? 0, source.z ?? 0);
42
+ return out.set(0, -1e6, 0);
43
+ }
44
+
45
+ // Options: radius (m), splashStrength / wakeStrength multipliers, splashes /
46
+ // wakes toggles, onSplash(event) callback.
47
+ add(source, options = {}) {
48
+ const id = options.id ?? `interactor-${this.nextId += 1}`;
49
+ this.interactors.set(id, {
50
+ source,
51
+ radius: options.radius ?? 0.35,
52
+ // Vertical extent above the tracked point. For tall bodies (characters,
53
+ // boats) this makes "touching" mean the waterline passes through the
54
+ // body, so chest-deep wading wakes but a fully submerged body is silent.
55
+ height: options.height ?? 0,
56
+ splashStrength: options.splashStrength ?? 1,
57
+ wakeStrength: options.wakeStrength ?? 1,
58
+ splashes: options.splashes ?? true,
59
+ wakes: options.wakes ?? true,
60
+ onSplash: options.onSplash ?? null,
61
+ previousPosition: new THREE.Vector3(0, -1e6, 0),
62
+ wasTouching: false,
63
+ wasAboveSurface: true,
64
+ wasBelowSurface: false,
65
+ wakeTimer: 0,
66
+ initialized: false,
67
+ });
68
+ return id;
69
+ }
70
+
71
+ remove(id) {
72
+ this.interactors.delete(id);
73
+ }
74
+
75
+ clear() {
76
+ this.interactors.clear();
77
+ }
78
+
79
+ update(delta) {
80
+ if (delta <= 0) return;
81
+ for (const interactor of this.interactors.values()) {
82
+ this.updateInteractor(interactor, delta);
83
+ }
84
+ }
85
+
86
+ updateInteractor(interactor, delta) {
87
+ const position = this.resolvePosition(interactor.source, workingPosition);
88
+ if (position.y < -1e5) return;
89
+
90
+ if (!interactor.initialized) {
91
+ interactor.previousPosition.copy(position);
92
+ interactor.initialized = true;
93
+ interactor.wasTouching = false;
94
+ interactor.wasAboveSurface = true;
95
+ interactor.wasBelowSurface = false;
96
+ return;
97
+ }
98
+
99
+ const surface = this.surface;
100
+ const inArea = surface.containsPoint(position.x, position.z);
101
+ const waterHeight = surface.getHeightAt(position.x, position.z);
102
+ const height = typeof interactor.height === 'function' ? interactor.height() : interactor.height;
103
+ const top = position.y + Math.max(height, interactor.radius);
104
+ const bottom = position.y - interactor.radius;
105
+ // Surface effects only happen where the waterline actually passes through
106
+ // the interactor volume. A body running along the seabed (fully below) or
107
+ // flying above leaves the surface untouched.
108
+ const touching = inArea && waterHeight >= bottom - 0.05 && waterHeight <= top + 0.05;
109
+ const aboveSurface = bottom > waterHeight;
110
+ const belowSurface = top < waterHeight;
111
+
112
+ const velocityY = (position.y - interactor.previousPosition.y) / delta;
113
+ const planarSpeed = Math.hypot(
114
+ position.x - interactor.previousPosition.x,
115
+ position.z - interactor.previousPosition.z) / delta;
116
+
117
+ // Crossing detection uses the previous frame's relation to the surface so
118
+ // a fast fall that skips past the waterline band in one step still counts.
119
+ const enteredFromAbove = interactor.wasAboveSurface && (touching || belowSurface);
120
+ const exitedUpward = (interactor.wasTouching || interactor.wasBelowSurface) && aboveSurface;
121
+
122
+ if (inArea && enteredFromAbove && interactor.splashes &&
123
+ velocityY < -this.entrySplashSpeed) {
124
+ const strength = Math.min(2.4, 0.4 + Math.abs(velocityY) * 0.22) *
125
+ interactor.splashStrength * (0.6 + interactor.radius);
126
+ surface.splash(position, { strength, radius: interactor.radius * 2.2 });
127
+ interactor.onSplash?.({ type: 'enter', position: position.clone(), strength });
128
+ } else if (inArea && exitedUpward && interactor.splashes &&
129
+ velocityY > this.exitSplashSpeed) {
130
+ const strength = Math.min(1.4, 0.3 + velocityY * 0.1) * interactor.splashStrength;
131
+ surface.splash(position, { strength: strength * 0.7, radius: interactor.radius * 1.6 });
132
+ interactor.onSplash?.({ type: 'exit', position: position.clone(), strength });
133
+ }
134
+
135
+ if (interactor.wakes && interactor.wasTouching && belowSurface && velocityY < -0.3) {
136
+ surface.addRipple(position, { radius: interactor.radius * 1.4, strength: -0.4 * interactor.wakeStrength });
137
+ } else if (interactor.wakes && interactor.wasBelowSurface && touching && velocityY > 0.3) {
138
+ surface.addRipple(position, { radius: interactor.radius * 1.4, strength: 0.35 * interactor.wakeStrength });
139
+ }
140
+
141
+ if (touching && interactor.wakes) {
142
+ interactor.wakeTimer -= delta;
143
+ if (planarSpeed > this.minWakeSpeed && interactor.wakeTimer <= 0) {
144
+ const speedFactor = Math.min(planarSpeed, 4);
145
+ // A body pushing through water makes a V-wake, not concentric rings:
146
+ // a rising bow push just ahead, and a pair of depressions at the
147
+ // flanks slightly behind. The propagating pair draws the trailing V.
148
+ const stepX = position.x - interactor.previousPosition.x;
149
+ const stepZ = position.z - interactor.previousPosition.z;
150
+ const stepLength = Math.hypot(stepX, stepZ) || 1;
151
+ const dirX = stepX / stepLength;
152
+ const dirZ = stepZ / stepLength;
153
+ const sideX = -dirZ;
154
+ const sideZ = dirX;
155
+ const radius = interactor.radius;
156
+ const wakeStrength = speedFactor * interactor.wakeStrength;
157
+ surface.addRipple({
158
+ x: position.x + dirX * radius * 1.15,
159
+ z: position.z + dirZ * radius * 1.15,
160
+ }, { radius: radius * (0.9 + speedFactor * 0.1), strength: 0.16 * wakeStrength });
161
+ const backX = position.x - dirX * radius * 0.5;
162
+ const backZ = position.z - dirZ * radius * 0.5;
163
+ surface.addRipple({
164
+ x: backX + sideX * radius,
165
+ z: backZ + sideZ * radius,
166
+ }, { radius: radius * 0.85, strength: -0.12 * wakeStrength });
167
+ surface.addRipple({
168
+ x: backX - sideX * radius,
169
+ z: backZ - sideZ * radius,
170
+ }, { radius: radius * 0.85, strength: -0.12 * wakeStrength });
171
+ // Fast movers kick up a little spray off the bow.
172
+ if (planarSpeed > 1.8 && interactor.splashes) {
173
+ surface.sprayAt({
174
+ x: position.x + dirX * radius,
175
+ y: position.y,
176
+ z: position.z + dirZ * radius,
177
+ }, {
178
+ count: Math.round(2 + speedFactor),
179
+ strength: speedFactor * 0.22 * interactor.splashStrength,
180
+ });
181
+ }
182
+ interactor.wakeTimer = this.wakeInterval / Math.max(planarSpeed, 1);
183
+ }
184
+ } else {
185
+ interactor.wakeTimer = 0;
186
+ }
187
+
188
+ interactor.wasTouching = touching;
189
+ interactor.wasAboveSurface = aboveSurface;
190
+ interactor.wasBelowSurface = belowSurface;
191
+ interactor.previousPosition.copy(position);
192
+ }
193
+ }
@@ -0,0 +1,263 @@
1
+ import * as THREE from 'three';
2
+
3
+ import { createWaterNodeMaterial } from '../shaders-tsl/water.js';
4
+ import {
5
+ buildGerstnerWaves,
6
+ createWaterSettings,
7
+ WATER_DEBUG_MODES,
8
+ WATER_GERSTNER_WAVE_COUNT,
9
+ } from './waterSettings.js';
10
+
11
+ // Quality tiers gate the most expensive fragment features:
12
+ // low — no caustics/sparkles, 2-octave detail noise
13
+ // medium — caustics + sparkles, no chromatic caustics, 3-octave detail
14
+ // high — chromatic caustics, 4-octave detail
15
+ const QUALITY_DEFINES = Object.freeze({
16
+ low: Object.freeze({ WATER_QUALITY: 0, WATER_DETAIL_OCTAVES: 2, WATER_FOAM_OCTAVES: 2 }),
17
+ medium: Object.freeze({ WATER_QUALITY: 1, WATER_DETAIL_OCTAVES: 3, WATER_FOAM_OCTAVES: 3 }),
18
+ high: Object.freeze({ WATER_QUALITY: 2, WATER_DETAIL_OCTAVES: 4, WATER_FOAM_OCTAVES: 3 }),
19
+ });
20
+
21
+ /**
22
+ * The named water quality tiers, keyed 'low' | 'medium' | 'high'. Each tier
23
+ * maps to the shader defines it produces:
24
+ * `{ WATER_QUALITY, WATER_DETAIL_OCTAVES, WATER_FOAM_OCTAVES }` — see
25
+ * resolveWaterQualityDefines for the feature gates per WATER_QUALITY level.
26
+ * Frozen; use a custom `{ detailOctaves, foamOctaves, qualityLevel }` quality
27
+ * object to deviate from the named tiers.
28
+ */
29
+ export const WATER_QUALITY_TIERS = QUALITY_DEFINES;
30
+
31
+ function clampOctaves(value, fallback) {
32
+ const number = Number(value);
33
+ if (!Number.isFinite(number)) return fallback;
34
+ return Math.min(8, Math.max(1, Math.round(number)));
35
+ }
36
+
37
+ /**
38
+ * Resolves a quality request into the water shader defines
39
+ * `{ WATER_QUALITY, WATER_DETAIL_OCTAVES, WATER_FOAM_OCTAVES }`.
40
+ *
41
+ * Accepts either a named tier ('low' | 'medium' | 'high', see
42
+ * WATER_QUALITY_TIERS) or a custom tier object:
43
+ *
44
+ * { detailOctaves: 5, foamOctaves: 4, qualityLevel: 'high' }
45
+ *
46
+ * - `qualityLevel` picks the feature gate (0/'low' = no caustics or sparkles,
47
+ * 1/'medium' = caustics + sparkles, 2/'high' = chromatic caustics) and the
48
+ * octave fallbacks; it defaults to 'high'.
49
+ * - `detailOctaves` / `foamOctaves` override the noise octave counts
50
+ * (clamped to 1..8 integers).
51
+ *
52
+ * Unknown or missing values fall back to the 'high' tier, matching the
53
+ * previous behavior.
54
+ *
55
+ * @param {string|{detailOctaves?: number, foamOctaves?: number, qualityLevel?: string|number}} quality
56
+ * @returns {{WATER_QUALITY: number, WATER_DETAIL_OCTAVES: number, WATER_FOAM_OCTAVES: number}}
57
+ */
58
+ export function resolveWaterQualityDefines(quality) {
59
+ if (quality && typeof quality === 'object') {
60
+ const level = quality.qualityLevel;
61
+ const numericLevel = Number(level);
62
+ const tier = Number.isFinite(numericLevel)
63
+ ? Object.values(QUALITY_DEFINES)
64
+ .find((entry) => entry.WATER_QUALITY === Math.min(2, Math.max(0, Math.round(numericLevel))))
65
+ : QUALITY_DEFINES[String(level ?? 'high').toLowerCase()];
66
+ const base = tier ?? QUALITY_DEFINES.high;
67
+ return Object.freeze({
68
+ WATER_QUALITY: base.WATER_QUALITY,
69
+ WATER_DETAIL_OCTAVES: clampOctaves(quality.detailOctaves, base.WATER_DETAIL_OCTAVES),
70
+ WATER_FOAM_OCTAVES: clampOctaves(quality.foamOctaves, base.WATER_FOAM_OCTAVES),
71
+ });
72
+ }
73
+ return QUALITY_DEFINES[String(quality ?? '').toLowerCase()] ?? QUALITY_DEFINES.high;
74
+ }
75
+
76
+ // createWaterSettings normalizes `quality` to a named tier string, so a
77
+ // custom tier object must be pulled from the raw options before it is lost.
78
+ function requestedWaterQuality(options, settings) {
79
+ const raw = options && typeof options === 'object' ? options.quality : undefined;
80
+ return raw && typeof raw === 'object' ? raw : settings.quality;
81
+ }
82
+
83
+ // Water colors are authored as sRGB values in settings/presets; uniforms are
84
+ // uploaded in linear working space and converted back at the end of the
85
+ // fragment shader, matching the pipeline used by built-in materials.
86
+ function setSrgbColorUniform(uniform, value) {
87
+ if (!uniform) return;
88
+ const [r, g, b] = Array.isArray(value) ? value : [value?.r ?? 1, value?.g ?? 1, value?.b ?? 1];
89
+ if (!uniform.value?.isColor) uniform.value = new THREE.Color();
90
+ uniform.value.setRGB(r, g, b, THREE.SRGBColorSpace);
91
+ }
92
+
93
+ function setVector2Uniform(uniform, value) {
94
+ if (!uniform) return;
95
+ if (!uniform.value?.isVector2) uniform.value = new THREE.Vector2();
96
+ uniform.value.set(value?.[0] ?? value?.x ?? 1, value?.[1] ?? value?.y ?? 0);
97
+ }
98
+
99
+ function setDirection3Uniform(uniform, value) {
100
+ if (!uniform) return;
101
+ if (!uniform.value?.isVector3) uniform.value = new THREE.Vector3();
102
+ uniform.value
103
+ .set(value?.[0] ?? value?.x ?? 0, value?.[1] ?? value?.y ?? 1, value?.[2] ?? value?.z ?? 0)
104
+ .normalize();
105
+ }
106
+
107
+ function writeWaveUniforms(material, waves) {
108
+ const wavesA = material.uniforms.uWavesA.value;
109
+ const wavesB = material.uniforms.uWavesB.value;
110
+ for (let i = 0; i < WATER_GERSTNER_WAVE_COUNT; i += 1) {
111
+ const wave = waves[i];
112
+ wavesA[i].set(wave.dirX, wave.dirZ, wave.omega, wave.waveNumber);
113
+ wavesB[i].set(wave.amplitude, wave.phase, wave.steepness, wave.crestWeight);
114
+ }
115
+ }
116
+
117
+ export function resolveWaterDebugMode(mode) {
118
+ if (typeof mode === 'number' && Number.isFinite(mode)) return Math.max(0, Math.floor(mode));
119
+ return WATER_DEBUG_MODES[String(mode ?? 'off').toLowerCase()] ?? 0;
120
+ }
121
+
122
+ export function setWaterDebugMode(material, mode) {
123
+ if (material?.uniforms?.uDebugMode) {
124
+ material.uniforms.uDebugMode.value = resolveWaterDebugMode(mode);
125
+ }
126
+ return material;
127
+ }
128
+
129
+ export function applyWaterSettingsToMaterial(material, options = {}) {
130
+ const uniforms = material?.uniforms;
131
+ if (!uniforms) return material;
132
+ const settings = createWaterSettings(options);
133
+ const waves = buildGerstnerWaves(settings);
134
+
135
+ writeWaveUniforms(material, waves);
136
+
137
+ uniforms.uDetailNormalStrength.value = settings.detailNormalStrength;
138
+ uniforms.uDetailScale.value = settings.detailScale;
139
+ setVector2Uniform(uniforms.uFlowDirection, settings.flowDirection);
140
+ uniforms.uFlowSpeed.value = settings.flowSpeed;
141
+
142
+ setSrgbColorUniform(uniforms.uShallowColor, settings.shallowColor);
143
+ setSrgbColorUniform(uniforms.uMidColor, settings.midColor);
144
+ setSrgbColorUniform(uniforms.uDeepColor, settings.deepColor);
145
+ uniforms.uDepthFadeDistance.value = settings.depthFadeDistance;
146
+ uniforms.uDeepFadeDistance.value = settings.deepFadeDistance;
147
+ uniforms.uOpacity.value = settings.opacity;
148
+ uniforms.uRefractionStrength.value = settings.refractionStrength;
149
+ uniforms.uCausticsStrength.value = settings.causticsStrength;
150
+ uniforms.uCausticsScale.value = settings.causticsScale;
151
+ uniforms.uCausticsSpeed.value = settings.causticsSpeed;
152
+
153
+ setSrgbColorUniform(uniforms.uFoamColor, settings.foamColor);
154
+ uniforms.uFoamAmount.value = settings.foamAmount;
155
+ uniforms.uFoamContactDistance.value = settings.foamContactDistance;
156
+ uniforms.uFoamLineSpacing.value = settings.foamLineSpacing;
157
+ uniforms.uFoamNoiseScale.value = settings.foamNoiseScale;
158
+ uniforms.uWhitecapAmount.value = settings.whitecapAmount;
159
+ uniforms.uRippleFoamStrength.value = settings.rippleFoamStrength;
160
+
161
+ setDirection3Uniform(uniforms.uSunDirection, settings.sunDirection);
162
+ setSrgbColorUniform(uniforms.uSunColor, settings.sunColor);
163
+ uniforms.uSpecularStrength.value = settings.specularStrength;
164
+ uniforms.uSpecularShininess.value = settings.specularShininess;
165
+ uniforms.uSpecularStretch.value = settings.specularStretch;
166
+ uniforms.uSparkleStrength.value = settings.sparkleStrength;
167
+ uniforms.uSparkleScale.value = settings.sparkleScale;
168
+ uniforms.uSparkleSpeed.value = settings.sparkleSpeed;
169
+ uniforms.uSunGlowStrength.value = settings.sunGlowStrength;
170
+ uniforms.uSceneShadowStrength.value = settings.sceneShadowStrength;
171
+ uniforms.uFresnelStrength.value = settings.fresnelStrength;
172
+ uniforms.uFresnelPower.value = settings.fresnelPower;
173
+ uniforms.uFresnelBias.value = settings.fresnelBias;
174
+ uniforms.uReflectionSoftness.value = settings.reflectionSoftness;
175
+ setSrgbColorUniform(uniforms.uFresnelColor, settings.fresnelColor);
176
+ setSrgbColorUniform(uniforms.uSkyZenithColor, settings.skyZenithColor);
177
+ setSrgbColorUniform(uniforms.uSkyHorizonColor, settings.skyHorizonColor);
178
+ uniforms.uReflectionStrength.value = settings.reflectionStrength;
179
+ uniforms.uReflectionDistortion.value = settings.reflectionDistortion;
180
+
181
+ uniforms.uRippleHeightScale.value = settings.rippleHeightScale;
182
+ uniforms.uShoalingDepth.value = settings.shoalingDepth;
183
+ uniforms.uShorelineWaves.value = settings.shorelineWaves;
184
+ uniforms.uShorelineRunup.value = settings.shorelineRunup;
185
+ // Total swell energy — normalizes breaker foam and scales swash run-up.
186
+ const waveEnergy = waves.reduce((sum, wave) => sum + Math.abs(wave.amplitude), 0);
187
+ uniforms.uWaveEnergy.value = waveEnergy;
188
+ material.userData.waterWaveEnergy = waveEnergy;
189
+
190
+ const qualityDefines = resolveWaterQualityDefines(requestedWaterQuality(options, settings));
191
+ const currentDefines = material.defines ?? {};
192
+ if (currentDefines.WATER_QUALITY !== qualityDefines.WATER_QUALITY
193
+ || currentDefines.WATER_DETAIL_OCTAVES !== qualityDefines.WATER_DETAIL_OCTAVES
194
+ || currentDefines.WATER_FOAM_OCTAVES !== qualityDefines.WATER_FOAM_OCTAVES) {
195
+ material.defines = { ...material.defines, ...qualityDefines };
196
+ material.needsUpdate = true;
197
+ }
198
+
199
+ material.userData.waterMaterial = true;
200
+ material.userData.waterSettings = settings;
201
+ material.userData.gerstnerWaves = waves;
202
+ return material;
203
+ }
204
+
205
+ export function createWaterMaterial(options = {}) {
206
+ const settings = createWaterSettings(options);
207
+ const requestedQuality = requestedWaterQuality(options, settings);
208
+ const qualityDefines = resolveWaterQualityDefines(requestedQuality);
209
+
210
+ // Node-backend material: same uniform-name surface, quality defines baked
211
+ // as graph flags (a later quality change needs a new material on this
212
+ // backend). `options.shoaling` is the WATER_SHOALING define analog —
213
+ // WaterSurface passes it when a bed sampler bakes aBedHeight.
214
+ const material = createWaterNodeMaterial({
215
+ waveCount: WATER_GERSTNER_WAVE_COUNT,
216
+ qualityLevel: qualityDefines.WATER_QUALITY,
217
+ detailOctaves: qualityDefines.WATER_DETAIL_OCTAVES,
218
+ foamOctaves: qualityDefines.WATER_FOAM_OCTAVES,
219
+ shoaling: Boolean(options.shoaling),
220
+ });
221
+ return applyWaterSettingsToMaterial(
222
+ material,
223
+ typeof requestedQuality === 'object' ? { ...settings, quality: requestedQuality } : settings,
224
+ );
225
+ }
226
+
227
+ const invViewProjScratch = new THREE.Matrix4();
228
+
229
+ // Node backends: the water fragment reconstructs world positions with the
230
+ // literal GLSL math — screenUv/rawDepth * 2 - 1 into uInvViewProjMatrix — so
231
+ // the backend differences are composed into the matrix on the CPU
232
+ // (docs/tsl-conventions.md #6): screenUV is top-left-origin on both node
233
+ // backends (y-negate), and WebGPU NDC z is [0,1] (the stored [0,1] window
234
+ // depth IS the WebGPU NDC z, so z' maps 2d-1 back to d). These are the
235
+ // inverses of pass-depth-color's shadowClipAdjust* matrices.
236
+ const ndcAdjustWebGPU = new THREE.Matrix4().set(
237
+ 1, 0, 0, 0,
238
+ 0, -1, 0, 0,
239
+ 0, 0, 0.5, 0.5,
240
+ 0, 0, 0, 1,
241
+ );
242
+ const ndcAdjustGL = new THREE.Matrix4().set(
243
+ 1, 0, 0, 0,
244
+ 0, -1, 0, 0,
245
+ 0, 0, 1, 0,
246
+ 0, 0, 0, 1,
247
+ );
248
+
249
+ // Per-frame camera/screen uniform refresh. WaterSurface calls this for you;
250
+ // it is exported for projects driving the material directly.
251
+ export function updateWaterMaterialCamera(material, renderer, camera) {
252
+ const uniforms = material?.uniforms;
253
+ if (!uniforms) return material;
254
+ renderer.getDrawingBufferSize(uniforms.uResolution.value);
255
+ uniforms.uCameraNear.value = camera.near;
256
+ uniforms.uCameraFar.value = camera.far;
257
+ invViewProjScratch.multiplyMatrices(camera.matrixWorld, camera.projectionMatrixInverse);
258
+ invViewProjScratch.multiply(
259
+ renderer.coordinateSystem === THREE.WebGPUCoordinateSystem ? ndcAdjustWebGPU : ndcAdjustGL,
260
+ );
261
+ uniforms.uInvViewProjMatrix.value.copy(invViewProjScratch);
262
+ return material;
263
+ }
@@ -0,0 +1,136 @@
1
+ import * as THREE from 'three';
2
+
3
+ import { createWaterRainNodeMaterial } from '../shaders-tsl/water-rain.js';
4
+
5
+ // Node-backend rain: WGSL has no gl_PointSize, so the streak POINTS become
6
+ // instanced billboard quads (dust-motes pattern; see water-rain.js). Same
7
+ // public API as the classic Points-based WaterRain — the WaterRain
8
+ // constructor returns an instance of this class on the TSL backend.
9
+ class WaterRainNodeMesh extends THREE.Mesh {
10
+ constructor({
11
+ count = 2400,
12
+ areaSize = 30,
13
+ fallHeight = 16,
14
+ speed = 16,
15
+ streakLength = 0.42,
16
+ wind = [2.2, 0.8],
17
+ color = [0.8, 0.88, 0.95],
18
+ opacity = 0.34,
19
+ } = {}) {
20
+ const quad = new THREE.PlaneGeometry(1, 1);
21
+ const geometry = new THREE.InstancedBufferGeometry();
22
+ geometry.index = quad.index;
23
+ geometry.setAttribute('position', quad.attributes.position);
24
+ geometry.setAttribute('uv', quad.attributes.uv);
25
+ const seeds = new Float32Array(count * 4);
26
+ for (let i = 0; i < count; i += 1) {
27
+ seeds[i * 4] = Math.random();
28
+ seeds[i * 4 + 1] = Math.random();
29
+ seeds[i * 4 + 2] = Math.random();
30
+ seeds[i * 4 + 3] = Math.random();
31
+ }
32
+ geometry.setAttribute('aSeed', new THREE.InstancedBufferAttribute(seeds, 4));
33
+ geometry.instanceCount = count;
34
+ geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(), 1e5);
35
+
36
+ const material = createWaterRainNodeMaterial({
37
+ areaSize, color, fallHeight, opacity, speed, streakLength, wind,
38
+ });
39
+
40
+ super(geometry, material);
41
+ this.name = 'WaterRain';
42
+ this.frustumCulled = false;
43
+ this.renderOrder = 40;
44
+ this.count = count;
45
+ this.baseOpacity = opacity;
46
+ this.intensity = 1;
47
+ // Rain is an overlay effect: keep it out of the water's scene passes.
48
+ this.userData.waterExclude = true;
49
+ }
50
+
51
+ setIntensity(intensity) {
52
+ this.intensity = THREE.MathUtils.clamp(intensity, 0, 1);
53
+ this.visible = this.intensity > 0.005;
54
+ this.material.uniforms.uOpacity.value = this.baseOpacity * (0.4 + 0.6 * this.intensity);
55
+ // Instanced quads: the draw-count scaling happens through instanceCount
56
+ // (setDrawRange would trim the base quad, not the particle pool).
57
+ this.geometry.instanceCount = Math.floor(this.count * this.intensity);
58
+ return this;
59
+ }
60
+
61
+ update(delta, camera, renderer, waterLevel = 0) {
62
+ const uniforms = this.material.uniforms;
63
+ uniforms.uTime.value += Math.min(Math.max(delta ?? 0.016, 0), 0.1);
64
+ if (camera) {
65
+ const position = camera.getWorldPosition(new THREE.Vector3());
66
+ uniforms.uCenter.value.set(position.x, waterLevel, position.z);
67
+ }
68
+ if (renderer) {
69
+ uniforms.uPixelRatio.value = renderer.getPixelRatio();
70
+ uniforms.uPointScale.value = (renderer.domElement?.clientHeight || 540) * 0.9;
71
+ }
72
+ return this;
73
+ }
74
+
75
+ dispose() {
76
+ this.geometry.dispose();
77
+ this.material.dispose();
78
+ }
79
+ }
80
+
81
+ // Procedural rain: GPU-looping streak particles in a volume that follows a
82
+ // center point (usually the camera over the water). No texture assets and no
83
+ // per-frame buffer writes; intensity scales draw count and opacity.
84
+ //
85
+ // const rain = new WaterRain({ count: 2600 });
86
+ // scene.add(rain);
87
+ // rain.setIntensity(0.9);
88
+ // rain.update(delta, camera, renderer, waterLevel); // each frame
89
+ //
90
+ // Pair it with WaterSurface.addRipple(...) dimples for rain-pocked water.
91
+ export class WaterRain extends THREE.Points {
92
+ constructor({
93
+ count = 2400,
94
+ areaSize = 30,
95
+ fallHeight = 16,
96
+ speed = 16,
97
+ streakLength = 0.42,
98
+ wind = [2.2, 0.8],
99
+ color = [0.8, 0.88, 0.95],
100
+ opacity = 0.34,
101
+ } = {}) {
102
+ // Derived constructors may skip super() when they return an object: the
103
+ // instanced-quad implementation keeps the same public API surface, so call
104
+ // sites stay `new WaterRain(...)`.
105
+ return new WaterRainNodeMesh({
106
+ areaSize, color, count, fallHeight, opacity, speed, streakLength, wind,
107
+ });
108
+ }
109
+
110
+ setIntensity(intensity) {
111
+ this.intensity = THREE.MathUtils.clamp(intensity, 0, 1);
112
+ this.visible = this.intensity > 0.005;
113
+ this.material.uniforms.uOpacity.value = this.baseOpacity * (0.4 + 0.6 * this.intensity);
114
+ this.geometry.setDrawRange(0, Math.floor(this.count * this.intensity));
115
+ return this;
116
+ }
117
+
118
+ update(delta, camera, renderer, waterLevel = 0) {
119
+ const uniforms = this.material.uniforms;
120
+ uniforms.uTime.value += Math.min(Math.max(delta ?? 0.016, 0), 0.1);
121
+ if (camera) {
122
+ const position = camera.getWorldPosition(new THREE.Vector3());
123
+ uniforms.uCenter.value.set(position.x, waterLevel, position.z);
124
+ }
125
+ if (renderer) {
126
+ uniforms.uPixelRatio.value = renderer.getPixelRatio();
127
+ uniforms.uPointScale.value = (renderer.domElement?.clientHeight || 540) * 0.9;
128
+ }
129
+ return this;
130
+ }
131
+
132
+ dispose() {
133
+ this.geometry.dispose();
134
+ this.material.dispose();
135
+ }
136
+ }