three-base 2.0.142 → 2.0.145

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 (227) hide show
  1. package/DirectionalLight.d.ts +6 -6
  2. package/DirectionalLight.js +68 -68
  3. package/EventMap.d.ts +103 -103
  4. package/EventMap.js +17 -17
  5. package/EventMesh.d.ts +18 -18
  6. package/EventMesh.js +59 -59
  7. package/ExtrudeMesh.d.ts +112 -112
  8. package/ExtrudeMesh.js +342 -342
  9. package/FPScontrol.d.ts +511 -511
  10. package/FPScontrol.js +1799 -1799
  11. package/FPScontrol.ts +1855 -1855
  12. package/Fire/Fire.d.ts +28 -28
  13. package/Fire/Fire.js +80 -80
  14. package/Fire/FireGeometry.d.ts +9 -9
  15. package/Fire/FireGeometry.js +40 -40
  16. package/Fire/FireMaterial.d.ts +28 -28
  17. package/Fire/FireMaterial.js +32 -32
  18. package/Fire/index.d.ts +2 -2
  19. package/Fire/index.js +2 -2
  20. package/GLTFDracoLoader.d.ts +71 -71
  21. package/GLTFDracoLoader.js +266 -266
  22. package/GLTFDracoLoader.ts +1 -1
  23. package/Grass/Grass.d.ts +8 -8
  24. package/Grass/Grass.js +38 -38
  25. package/Grass/index.d.ts +2 -2
  26. package/Grass/index.js +2 -2
  27. package/Ground1/Ground1.d.ts +10 -10
  28. package/Ground1/Ground1.js +71 -71
  29. package/Ground1/index.d.ts +1 -1
  30. package/Ground1/index.js +1 -1
  31. package/MeshBase.d.ts +36 -36
  32. package/MeshBase.js +247 -247
  33. package/Mirror.d.ts +25 -25
  34. package/Mirror.js +143 -143
  35. package/MktMesh/MktMesh.d.ts +301 -291
  36. package/MktMesh/MktMesh.js +1064 -1045
  37. package/MktMesh/MktMesh.ts +32 -11
  38. package/MktMesh/assetUrl.d.ts +59 -35
  39. package/MktMesh/assetUrl.js +154 -69
  40. package/MktMesh/assetUrl.ts +155 -69
  41. package/MktMesh/mktMappingShader.d.ts +12 -12
  42. package/MktMesh/mktMappingShader.js +138 -138
  43. package/MktMesh/mktMappingShader.ts +139 -139
  44. package/MktMesh/mktShader.d.ts +263 -263
  45. package/MktMesh/mktShader.js +1236 -1236
  46. package/MktMesh/mktShader.ts +1344 -1344
  47. package/MktMesh/panoBitmapTexture.d.ts +25 -25
  48. package/MktMesh/panoBitmapTexture.js +52 -52
  49. package/MktMesh/panoBitmapTexture.ts +54 -54
  50. package/MktMesh/panoSlotBind.d.ts +29 -29
  51. package/MktMesh/panoSlotBind.js +106 -106
  52. package/MktMesh/panoSlotBind.ts +153 -153
  53. package/MktMesh/panoSlotRoster.d.ts +22 -22
  54. package/MktMesh/panoSlotRoster.js +169 -169
  55. package/MktMesh/panoSlotRoster.ts +168 -168
  56. package/MktMesh/panoTileStitcher.d.ts +119 -119
  57. package/MktMesh/panoTileStitcher.js +529 -529
  58. package/MktMesh/panoTileStitcher.ts +616 -616
  59. package/MktMesh/types.d.ts +75 -75
  60. package/MktMesh/types.js +1 -1
  61. package/MktMesh/wallCapTessellate.d.ts +33 -33
  62. package/MktMesh/wallCapTessellate.js +97 -97
  63. package/MktMesh/wallCapTessellate.ts +68 -68
  64. package/PipeMesh/PathGeometry.d.ts +44 -44
  65. package/PipeMesh/PathGeometry.js +307 -307
  66. package/PipeMesh/PathPoint.d.ts +17 -17
  67. package/PipeMesh/PathPoint.js +31 -31
  68. package/PipeMesh/PathPointList.d.ts +31 -31
  69. package/PipeMesh/PathPointList.js +209 -209
  70. package/PipeMesh/PathTubeGeometry.d.ts +25 -25
  71. package/PipeMesh/PathTubeGeometry.js +130 -130
  72. package/PipeMesh/PipeMesh.d.ts +25 -25
  73. package/PipeMesh/PipeMesh.js +63 -63
  74. package/PipeMesh/index.d.ts +1 -1
  75. package/PipeMesh/index.js +1 -1
  76. package/PlaneModal1/PlaneModal1.d.ts +34 -34
  77. package/PlaneModal1/PlaneModal1.js +109 -109
  78. package/PlaneModal1/index.d.ts +1 -1
  79. package/PlaneModal1/index.js +1 -1
  80. package/PlaneText.d.ts +27 -27
  81. package/PlaneText.js +35 -35
  82. package/PlaneWithHtml.d.ts +13 -13
  83. package/PlaneWithHtml.js +64 -64
  84. package/PointLight/PointLight.d.ts +13 -13
  85. package/PointLight/PointLight.js +64 -64
  86. package/PointLight/index.d.ts +1 -1
  87. package/PointLight/index.js +1 -1
  88. package/Sky/Sky.d.ts +9 -9
  89. package/Sky/Sky.js +22 -22
  90. package/Sky/index.d.ts +1 -1
  91. package/Sky/index.js +1 -1
  92. package/SpotLight.d.ts +6 -6
  93. package/SpotLight.js +72 -72
  94. package/SpriteModal1/SpriteModal1.d.ts +33 -33
  95. package/SpriteModal1/SpriteModal1.js +116 -116
  96. package/SpriteModal1/index.d.ts +1 -1
  97. package/SpriteModal1/index.js +1 -1
  98. package/SpriteText.d.ts +46 -46
  99. package/SpriteText.js +176 -176
  100. package/SpriteText2.d.ts +48 -48
  101. package/SpriteText2.js +223 -223
  102. package/SpriteText3.d.ts +86 -86
  103. package/SpriteText3.js +517 -517
  104. package/SpriteText3.ts +599 -599
  105. package/SpriteWithHtml.d.ts +9 -9
  106. package/SpriteWithHtml.js +44 -44
  107. package/Text3D/Text3D.d.ts +20 -20
  108. package/Text3D/Text3D.js +58 -58
  109. package/Text3D/fontController.d.ts +60 -60
  110. package/Text3D/fontController.js +166 -166
  111. package/Text3D/index.d.ts +1 -1
  112. package/Text3D/index.js +1 -1
  113. package/ThreeBase.d.ts +248 -248
  114. package/ThreeBase.js +1202 -1202
  115. package/ThreeBase.ts +1314 -1314
  116. package/ThreeMouse.d.ts +53 -54
  117. package/ThreeMouse.js +453 -453
  118. package/Transition.d.ts +45 -45
  119. package/Transition.js +209 -209
  120. package/Water/Water.d.ts +5 -5
  121. package/Water/Water.js +44 -44
  122. package/Water/index.d.ts +2 -2
  123. package/Water/index.js +2 -2
  124. package/WaterFlow/FloatingDuck.d.ts +88 -88
  125. package/WaterFlow/FloatingDuck.js +307 -307
  126. package/WaterFlow/FloatingDuck.ts +445 -445
  127. package/WaterFlow/GPUFluidSimulation.d.ts +56 -56
  128. package/WaterFlow/GPUFluidSimulation.js +781 -781
  129. package/WaterFlow/GPUFluidSimulation.ts +827 -827
  130. package/WaterFlow/ReservoirSystem.d.ts +106 -106
  131. package/WaterFlow/ReservoirSystem.js +611 -611
  132. package/WaterFlow/ReservoirSystem.ts +754 -754
  133. package/WaterFlow/SPHField.d.ts +35 -35
  134. package/WaterFlow/SPHField.js +203 -203
  135. package/WaterFlow/SPHField.ts +228 -228
  136. package/WaterFlow/ScreenSpaceFluidPass.d.ts +69 -69
  137. package/WaterFlow/ScreenSpaceFluidPass.js +728 -728
  138. package/WaterFlow/ScreenSpaceFluidPass.ts +789 -789
  139. package/WaterFlow/ShallowWaterSurface.d.ts +45 -45
  140. package/WaterFlow/ShallowWaterSurface.js +438 -438
  141. package/WaterFlow/ShallowWaterSurface.ts +510 -510
  142. package/WaterFlow/WaterFlow.d.ts +87 -87
  143. package/WaterFlow/WaterFlow.js +171 -171
  144. package/WaterFlow/WaterFlow.ts +219 -219
  145. package/WaterFlow/types.d.ts +161 -161
  146. package/WaterFlow/types.js +1 -1
  147. package/WaterFlow/types.ts +189 -189
  148. package/WaterFlow/waterFlowPass.d.ts +3 -3
  149. package/WaterFlow/waterFlowPass.js +20 -20
  150. package/WaterFlow/waterFlowPass.ts +40 -40
  151. package/collisionOBB.d.ts +42 -42
  152. package/collisionOBB.js +112 -112
  153. package/collisionOBB.ts +108 -108
  154. package/declare.d.ts +25 -25
  155. package/index.d.ts +1 -1
  156. package/index.js +1 -1
  157. package/instanced/InstancedBatch.d.ts +57 -57
  158. package/instanced/InstancedBatch.js +348 -348
  159. package/instanced/InstancedBatch.ts +406 -406
  160. package/instanced/InstancedChild.d.ts +174 -174
  161. package/instanced/InstancedChild.js +264 -264
  162. package/instanced/InstancedChild.ts +327 -327
  163. package/instanced/InstancedGroup.d.ts +43 -43
  164. package/instanced/InstancedGroup.js +126 -126
  165. package/instanced/InstancedGroup.ts +137 -137
  166. package/instanced/InstancedMeshBase.d.ts +38 -38
  167. package/instanced/InstancedMeshBase.js +282 -282
  168. package/instanced/InstancedTemplate.d.ts +46 -46
  169. package/instanced/InstancedTemplate.js +125 -125
  170. package/keyControls.d.ts +29 -30
  171. package/keyControls.js +101 -101
  172. package/lensflare/LensFlare.d.ts +39 -39
  173. package/lensflare/LensFlare.js +284 -284
  174. package/package.json +6 -1
  175. package/pass/bloom_pass.d.ts +2 -2
  176. package/pass/bloom_pass.js +174 -174
  177. package/pass/cover_pass.d.ts +2 -2
  178. package/pass/cover_pass.js +108 -108
  179. package/pass/debugDepthState.d.ts +20 -20
  180. package/pass/debugDepthState.js +9 -9
  181. package/pass/debugDepthState.ts +22 -22
  182. package/pass/depthTestBias.d.ts +12 -12
  183. package/pass/depthTestBias.js +12 -12
  184. package/pass/depthTestBias.ts +12 -12
  185. package/pass/effectUserData.d.ts +27 -27
  186. package/pass/effectUserData.js +1 -1
  187. package/pass/effectUserData.ts +46 -46
  188. package/pass/frosted_glass_pass.d.ts +2 -2
  189. package/pass/frosted_glass_pass.js +150 -150
  190. package/pass/fsQuad.d.ts +9 -9
  191. package/pass/fsQuad.js +28 -28
  192. package/pass/glow_pass.d.ts +2 -2
  193. package/pass/glow_pass.js +350 -350
  194. package/pass/screen_bloom_pass.d.ts +11 -11
  195. package/pass/screen_bloom_pass.js +136 -136
  196. package/pass/ssao_pass.d.ts +88 -88
  197. package/pass/ssao_pass.js +799 -799
  198. package/pass/ssao_pass.ts +918 -918
  199. package/pass/stroke_pass.d.ts +2 -2
  200. package/pass/stroke_pass.js +267 -267
  201. package/shaders/FXAAShaderMaterial.d.ts +3 -3
  202. package/shaders/FXAAShaderMaterial.js +20 -20
  203. package/shaders/baseDepthVisShader.d.ts +11 -11
  204. package/shaders/baseDepthVisShader.js +36 -36
  205. package/shaders/baseDepthVisShader.ts +36 -36
  206. package/shaders/baseTextureToRenderShader.d.ts +2 -2
  207. package/shaders/baseTextureToRenderShader.js +19 -19
  208. package/shaders/copyShader.d.ts +3 -3
  209. package/shaders/copyShader.js +13 -13
  210. package/shaders/debugDepthCompareShader.d.ts +14 -14
  211. package/shaders/debugDepthCompareShader.js +49 -49
  212. package/shaders/debugDepthCompareShader.ts +50 -50
  213. package/shaders/debugDepthJudgeShader.d.ts +18 -18
  214. package/shaders/debugDepthJudgeShader.js +52 -52
  215. package/shaders/debugDepthJudgeShader.ts +54 -54
  216. package/shaders/debugExcludedShader.d.ts +13 -13
  217. package/shaders/debugExcludedShader.js +46 -46
  218. package/shaders/debugExcludedShader.ts +48 -48
  219. package/spriteTextBg.d.ts +1 -1
  220. package/spriteTextBg.js +20 -20
  221. package/spriteTextBg.ts +20 -20
  222. package/threeLoader.d.ts +35 -35
  223. package/threeLoader.js +518 -518
  224. package/threeUtils.d.ts +53 -53
  225. package/threeUtils.js +329 -329
  226. package/types.d.ts +2 -2
  227. package/types.js +6 -6
@@ -1,754 +1,754 @@
1
- import * as THREE from 'three';
2
- import { ShallowWaterSurface } from './ShallowWaterSurface';
3
- import type { BasinConfig } from './types';
4
-
5
- /**
6
- * 构造选项:几何口径(length/width/壁高)+ 平面位置(centerX/centerZ)+ 尾流水面网格分辨率。
7
- * flowTexture 是溢出水花着色器采样的噪声图,缺省时 uniforms.uFlowMap 为 undefined。
8
- */
9
- export interface ReservoirOptions {
10
- flowTexture?: THREE.Texture;
11
- length?: number;
12
- width?: number;
13
- bottomY?: number;
14
- rimY?: number;
15
- centerX?: number;
16
- centerZ?: number;
17
- waveWidth?: number;
18
- waveHeight?: number;
19
- }
20
-
21
- /** update 的入参:入口水流量 + 撞击点/能量 + 是否正在喷射。 */
22
- export interface ReservoirUpdateOptions {
23
- flowRate: number;
24
- captureFraction?: number;
25
- impactX: number;
26
- impactZ?: number;
27
- impactEnergy?: number;
28
- emitting?: boolean;
29
- }
30
-
31
- /** update 的出参:本次入流/溢流的口径,供调用方做音效或统计。 */
32
- export interface ReservoirUpdateResult {
33
- captured: boolean;
34
- captureFraction: number;
35
- incomingRate: number;
36
- missedRate: number;
37
- overflowRate: number;
38
- }
39
-
40
- export class ReservoirSystem {
41
- /** 几何口径(全部由构造参数推导,构造后不变)。 */
42
- private readonly length: number;
43
- private readonly width: number;
44
- private readonly bottomY: number;
45
- private readonly rimY: number;
46
- private readonly innerBottomY: number;
47
- private readonly maxWaterY: number;
48
- private readonly innerLength: number;
49
- private readonly innerWidth: number;
50
- private readonly surfaceArea: number;
51
- private readonly capacity: number;
52
-
53
- /** 水量与溢流状态(每帧变化)。 */
54
- private volume: number;
55
- private overflowRate: number;
56
- private motionOverflowRate: number;
57
- private overflowIntensity: number;
58
- private active: boolean;
59
- private particleFluidMode: boolean;
60
-
61
- /** 容器晃动(xz 两个分量复用 Vector2 的 x/y)。 */
62
- private readonly motionPreviousPosition: THREE.Vector2;
63
- private readonly motionVelocity: THREE.Vector2;
64
- private readonly motionAcceleration: THREE.Vector2;
65
- private readonly sloshOffset: THREE.Vector2;
66
- private readonly sloshVelocity: THREE.Vector2;
67
- private readonly motionWaveImpulse: THREE.Vector2;
68
-
69
- /** 池体节点与拾取体。 */
70
- readonly renderer: THREE.WebGLRenderer;
71
- readonly group: THREE.Group;
72
- readonly hitMeshes: THREE.Mesh[];
73
-
74
- readonly rimFoamMaterial: THREE.MeshBasicMaterial;
75
- readonly rimFoamGroup: THREE.Group;
76
- readonly waterMaterial: THREE.ShaderMaterial;
77
- readonly waterBody: THREE.Mesh;
78
- /** 浅水波面;非WebGL2 或缺浮点扩展时为 null(此时只保留水体盒)。 */
79
- readonly surface: ShallowWaterSurface | null;
80
- readonly overflowMaterial: THREE.ShaderMaterial;
81
- readonly overflowGroup: THREE.Group;
82
-
83
- /** 溢流粒子(GPU points)的缓冲与状态。 */
84
- readonly dropCount: number;
85
- private readonly dropPositions: Float32Array;
86
- private readonly dropVelocities: Float32Array;
87
- private readonly dropLife: Float32Array;
88
- private readonly dropMaxLife: Float32Array;
89
- private readonly dropBounces: Uint8Array;
90
- private readonly dropSizes: Float32Array;
91
- private readonly dropAlpha: Float32Array;
92
- private dropCursor: number;
93
- private dropAccumulator: number;
94
- readonly dropMaterial: THREE.ShaderMaterial;
95
- readonly spillPoints: THREE.Points;
96
-
97
- constructor(renderer: THREE.WebGLRenderer, {
98
- flowTexture,
99
- length = 1.72,
100
- width = 1.28,
101
- bottomY = -0.13,
102
- rimY = 1.14,
103
- centerX = 1.82,
104
- centerZ = 0,
105
- waveWidth = 192,
106
- waveHeight = 128,
107
- }: ReservoirOptions = {}) {
108
- this.renderer = renderer;
109
- this.length = length;
110
- this.width = width;
111
- this.bottomY = bottomY;
112
- this.rimY = rimY;
113
- this.innerBottomY = bottomY + 0.11;
114
- this.maxWaterY = rimY - 0.024;
115
- this.innerLength = length - 0.14;
116
- this.innerWidth = width - 0.14;
117
- this.surfaceArea = this.innerLength * this.innerWidth;
118
- this.capacity = this.surfaceArea * (this.maxWaterY - this.innerBottomY);
119
- this.volume = 0;
120
- this.overflowRate = 0;
121
- this.motionOverflowRate = 0;
122
- this.overflowIntensity = 0;
123
- this.active = false;
124
- this.motionPreviousPosition = new THREE.Vector2(centerX, centerZ);
125
- this.motionVelocity = new THREE.Vector2();
126
- this.motionAcceleration = new THREE.Vector2();
127
- this.sloshOffset = new THREE.Vector2();
128
- this.sloshVelocity = new THREE.Vector2();
129
- this.motionWaveImpulse = new THREE.Vector2();
130
- this.particleFluidMode = false;
131
-
132
- this.group = new THREE.Group();
133
- this.group.position.set(centerX, 0, centerZ);
134
- this.hitMeshes = [];
135
-
136
- const steel = new THREE.MeshStandardMaterial({ color: 0x71878d, roughness: 0.2, metalness: 0.92 });
137
- const darkSteel = new THREE.MeshStandardMaterial({ color: 0x172a30, roughness: 0.36, metalness: 0.78 });
138
- const glass = new THREE.MeshPhysicalMaterial({
139
- color: 0x9edbe2,
140
- metalness: 0,
141
- roughness: 0.08,
142
- transmission: 0.9,
143
- transparent: true,
144
- opacity: 0.19,
145
- thickness: 0.04,
146
- ior: 1.48,
147
- clearcoat: 1,
148
- clearcoatRoughness: 0.04,
149
- depthWrite: false,
150
- side: THREE.DoubleSide,
151
- });
152
-
153
- const wallThickness = 0.035;
154
- const tankHeight = rimY - bottomY;
155
- const frontBackGeometry = new THREE.BoxGeometry(length, tankHeight, wallThickness);
156
- const sideGeometry = new THREE.BoxGeometry(wallThickness, tankHeight, width);
157
- [-1, 1].forEach((side) => {
158
- const wall = new THREE.Mesh(frontBackGeometry, glass);
159
- wall.position.set(0, bottomY + tankHeight * 0.5, side * width * 0.5);
160
- wall.renderOrder = 6;
161
- this.group.add(wall);
162
- this.hitMeshes.push(wall);
163
- });
164
- [-1, 1].forEach((side) => {
165
- const wall = new THREE.Mesh(sideGeometry, glass);
166
- wall.position.set(side * length * 0.5, bottomY + tankHeight * 0.5, 0);
167
- wall.renderOrder = 6;
168
- this.group.add(wall);
169
- this.hitMeshes.push(wall);
170
- });
171
-
172
- const base = new THREE.Mesh(new THREE.BoxGeometry(length + 0.08, 0.09, width + 0.08), darkSteel);
173
- base.position.y = bottomY + 0.025;
174
- base.castShadow = true;
175
- base.receiveShadow = true;
176
- this.group.add(base);
177
- this.hitMeshes.push(base);
178
-
179
- const rail = 0.045;
180
- const verticalGeometry = new THREE.BoxGeometry(rail, tankHeight + 0.08, rail);
181
- for (const x of [-1, 1]) {
182
- for (const z of [-1, 1]) {
183
- const post = new THREE.Mesh(verticalGeometry, steel);
184
- post.position.set(x * length * 0.5, bottomY + tankHeight * 0.5, z * width * 0.5);
185
- post.castShadow = true;
186
- this.group.add(post);
187
- }
188
- }
189
- const railXGeometry = new THREE.BoxGeometry(length + 0.08, rail, rail);
190
- const railZGeometry = new THREE.BoxGeometry(rail, rail, width + 0.08);
191
- for (const y of [bottomY + 0.035, rimY]) {
192
- for (const z of [-1, 1]) {
193
- const edge = new THREE.Mesh(railXGeometry, steel);
194
- edge.position.set(0, y, z * width * 0.5);
195
- edge.castShadow = true;
196
- this.group.add(edge);
197
- }
198
- for (const x of [-1, 1]) {
199
- const edge = new THREE.Mesh(railZGeometry, steel);
200
- edge.position.set(x * length * 0.5, y, 0);
201
- edge.castShadow = true;
202
- this.group.add(edge);
203
- }
204
- }
205
-
206
- this.rimFoamMaterial = new THREE.MeshBasicMaterial({
207
- color: 0xbdeff4,
208
- transparent: true,
209
- opacity: 0,
210
- depthWrite: false,
211
- blending: THREE.AdditiveBlending,
212
- });
213
- this.rimFoamGroup = new THREE.Group();
214
- const foamX = new THREE.BoxGeometry(this.innerLength * 0.96, 0.012, 0.025);
215
- const foamZ = new THREE.BoxGeometry(0.025, 0.012, this.innerWidth * 0.96);
216
- for (const z of [-1, 1]) {
217
- const foam = new THREE.Mesh(foamX, this.rimFoamMaterial);
218
- foam.position.set(0, this.maxWaterY + 0.014, z * this.innerWidth * 0.5);
219
- this.rimFoamGroup.add(foam);
220
- }
221
- for (const x of [-1, 1]) {
222
- const foam = new THREE.Mesh(foamZ, this.rimFoamMaterial);
223
- foam.position.set(x * this.innerLength * 0.5, this.maxWaterY + 0.014, 0);
224
- this.rimFoamGroup.add(foam);
225
- }
226
- this.group.add(this.rimFoamGroup);
227
-
228
- this.waterMaterial = new THREE.ShaderMaterial({
229
- uniforms: {
230
- uTime: { value: 0 },
231
- uFlowMap: { value: flowTexture },
232
- uOpacity: { value: 0.42 },
233
- },
234
- vertexShader: /* glsl */`
235
- varying vec2 vUv;
236
- varying vec3 vWorldPosition;
237
- varying vec3 vWorldNormal;
238
- void main() {
239
- vUv = uv;
240
- vec4 world = modelMatrix * vec4(position, 1.0);
241
- vWorldPosition = world.xyz;
242
- vWorldNormal = normalize(mat3(modelMatrix) * normal);
243
- gl_Position = projectionMatrix * viewMatrix * world;
244
- }
245
- `,
246
- fragmentShader: /* glsl */`
247
- uniform float uTime;
248
- uniform float uOpacity;
249
- uniform sampler2D uFlowMap;
250
- varying vec2 vUv;
251
- varying vec3 vWorldPosition;
252
- varying vec3 vWorldNormal;
253
- void main() {
254
- vec3 viewDir = normalize(cameraPosition - vWorldPosition);
255
- float fresnel = 0.02 + 0.98 * pow(1.0 - abs(dot(viewDir, normalize(vWorldNormal))), 5.0);
256
- vec3 detail = texture2D(uFlowMap, vUv * vec2(1.6, 2.8) + vec2(0.0, uTime * 0.018)).rgb;
257
- float depthTint = smoothstep(-0.2, 1.2, vWorldPosition.y);
258
- vec3 color = mix(vec3(0.008, 0.12, 0.16), vec3(0.035, 0.34, 0.4), depthTint);
259
- color += detail * 0.035 + vec3(0.2, 0.62, 0.68) * fresnel * 0.42;
260
- gl_FragColor = vec4(color, uOpacity + fresnel * 0.18);
261
- }
262
- `,
263
- transparent: true,
264
- depthWrite: false,
265
- side: THREE.DoubleSide,
266
- });
267
- this.waterBody = new THREE.Mesh(new THREE.BoxGeometry(this.innerLength, 1, this.innerWidth), this.waterMaterial);
268
- this.waterBody.renderOrder = 3;
269
- this.group.add(this.waterBody);
270
-
271
- this.surface = renderer.capabilities.isWebGL2 && renderer.extensions.has('EXT_color_buffer_float')
272
- ? new ShallowWaterSurface(renderer, {
273
- width: waveWidth,
274
- height: waveHeight,
275
- basinMinX: centerX - this.innerLength * 0.5,
276
- basinMaxX: centerX + this.innerLength * 0.5,
277
- basinHalfZ: this.innerWidth * 0.5,
278
- basinCenterZ: centerZ,
279
- waterY: this.innerBottomY,
280
- cpuSampling: true,
281
- })
282
- : null;
283
- if (this.surface) this.surface.mesh.renderOrder = 5;
284
-
285
- this.overflowMaterial = new THREE.ShaderMaterial({
286
- uniforms: {
287
- uTime: { value: 0 },
288
- uOverflow: { value: 0 },
289
- uFlowMap: { value: flowTexture },
290
- },
291
- vertexShader: /* glsl */`
292
- uniform float uTime;
293
- uniform float uOverflow;
294
- varying vec2 vUv;
295
- void main() {
296
- vUv = uv;
297
- vec3 p = position;
298
- float fall = 1.0 - uv.y;
299
- p.x *= mix(0.7, 1.0, uv.y);
300
- p.z += sin(uv.x * 29.0 + uTime * 5.0) * 0.012 * fall * uOverflow;
301
- p.y -= sin(uv.x * 17.0 + uTime * 3.7) * 0.016 * fall;
302
- gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
303
- }
304
- `,
305
- fragmentShader: /* glsl */`
306
- uniform float uTime;
307
- uniform float uOverflow;
308
- uniform sampler2D uFlowMap;
309
- varying vec2 vUv;
310
- void main() {
311
- vec2 flowUvA = vec2(vUv.x * 5.7 + uTime * 0.035, vUv.y * 7.5 + uTime * 0.82);
312
- vec2 flowUvB = vec2(vUv.x * 9.3 - uTime * 0.028, vUv.y * 4.2 + uTime * 0.57);
313
- float noiseA = dot(texture2D(uFlowMap, flowUvA).rgb, vec3(0.2, 0.58, 0.22));
314
- float noiseB = dot(texture2D(uFlowMap, flowUvB).rgb, vec3(0.35, 0.5, 0.15));
315
- float noise = mix(noiseA, noiseB, 0.46);
316
- float rivuletField = 0.5 + 0.5 * sin(vUv.x * 71.0 + noise * 9.0 + sin(vUv.y * 13.0) * 2.2);
317
- float rivulets = smoothstep(0.54 - uOverflow * 0.08, 0.86, rivuletField);
318
- float edge = smoothstep(0.0, 0.13, vUv.x) * smoothstep(1.0, 0.87, vUv.x);
319
- float verticalFade = smoothstep(0.0, 0.12, vUv.y);
320
- float breakup = smoothstep(0.27, 0.76, noise + vUv.y * 0.16);
321
- float alpha = edge * verticalFade * rivulets * breakup * (0.2 + uOverflow * 0.62);
322
- if (alpha < 0.025 || uOverflow < 0.01) discard;
323
- vec3 color = mix(vec3(0.045, 0.32, 0.38), vec3(0.58, 0.92, 0.97), noise * 0.3);
324
- gl_FragColor = vec4(color, alpha);
325
- }
326
- `,
327
- transparent: true,
328
- depthWrite: false,
329
- side: THREE.DoubleSide,
330
- });
331
-
332
- this.overflowGroup = new THREE.Group();
333
- const sheetHeight = 0.62;
334
- const makeSheet = (sheetWidth: number, x: number, z: number, rotationY = 0): void => {
335
- const sheet = new THREE.Mesh(new THREE.PlaneGeometry(sheetWidth, sheetHeight, 48, 24), this.overflowMaterial);
336
- sheet.position.set(x, rimY - sheetHeight * 0.5 - 0.012, z);
337
- sheet.rotation.y = rotationY;
338
- sheet.renderOrder = 7;
339
- this.overflowGroup.add(sheet);
340
- };
341
- makeSheet(length * 0.78, 0, -width * 0.5 - 0.026);
342
- makeSheet(length * 0.78, 0, width * 0.5 + 0.026);
343
- makeSheet(width * 0.72, -length * 0.5 - 0.026, 0, Math.PI * 0.5);
344
- makeSheet(width * 0.72, length * 0.5 + 0.026, 0, Math.PI * 0.5);
345
- this.group.add(this.overflowGroup);
346
-
347
- this.dropCount = waveWidth <= 112 ? 650 : 1400;
348
- this.dropPositions = new Float32Array(this.dropCount * 3);
349
- this.dropVelocities = new Float32Array(this.dropCount * 3);
350
- this.dropLife = new Float32Array(this.dropCount);
351
- this.dropMaxLife = new Float32Array(this.dropCount);
352
- this.dropBounces = new Uint8Array(this.dropCount);
353
- this.dropSizes = new Float32Array(this.dropCount);
354
- this.dropAlpha = new Float32Array(this.dropCount);
355
- this.dropCursor = 0;
356
- this.dropAccumulator = 0;
357
- this.dropPositions.fill(-100);
358
-
359
- const dropGeometry = new THREE.BufferGeometry();
360
- const positionAttribute = new THREE.BufferAttribute(this.dropPositions, 3);
361
- positionAttribute.setUsage(THREE.DynamicDrawUsage);
362
- const alphaAttribute = new THREE.BufferAttribute(this.dropAlpha, 1);
363
- alphaAttribute.setUsage(THREE.DynamicDrawUsage);
364
- const sizeAttribute = new THREE.BufferAttribute(this.dropSizes, 1);
365
- sizeAttribute.setUsage(THREE.DynamicDrawUsage);
366
- dropGeometry.setAttribute('position', positionAttribute);
367
- dropGeometry.setAttribute('aAlpha', alphaAttribute);
368
- dropGeometry.setAttribute('aSize', sizeAttribute);
369
- this.dropMaterial = new THREE.ShaderMaterial({
370
- uniforms: {
371
- uPixelRatio: { value: Math.min(devicePixelRatio, 2) },
372
- },
373
- vertexShader: /* glsl */`
374
- attribute float aAlpha;
375
- attribute float aSize;
376
- uniform float uPixelRatio;
377
- varying float vAlpha;
378
- void main() {
379
- vec4 mv = modelViewMatrix * vec4(position, 1.0);
380
- gl_Position = aAlpha > 0.001 ? projectionMatrix * mv : vec4(2.0, 2.0, 2.0, 1.0);
381
- gl_PointSize = min(7.5, aSize * uPixelRatio * (5.0 / max(2.0, -mv.z)));
382
- vAlpha = aAlpha;
383
- }
384
- `,
385
- fragmentShader: /* glsl */`
386
- varying float vAlpha;
387
- void main() {
388
- float d = length(gl_PointCoord - 0.5);
389
- if (d > 0.5) discard;
390
- float core = smoothstep(0.5, 0.08, d);
391
- float glint = 1.0 - smoothstep(0.015, 0.13, distance(gl_PointCoord, vec2(0.34, 0.3)));
392
- float alpha = core * vAlpha;
393
- gl_FragColor = vec4(vec3(0.42, 0.87, 0.96) + glint * 0.45, alpha);
394
- }
395
- `,
396
- transparent: true,
397
- depthWrite: false,
398
- blending: THREE.AdditiveBlending,
399
- toneMapped: false,
400
- });
401
- this.spillPoints = new THREE.Points(dropGeometry, this.dropMaterial);
402
- this.spillPoints.frustumCulled = false;
403
- this.spillPoints.renderOrder = 8;
404
-
405
- this.reset();
406
- this.setActive(false);
407
- }
408
-
409
- setActive(active: boolean): void {
410
- this.active = active;
411
- this.group.visible = active;
412
- if (this.surface) this.surface.mesh.visible = active && this.volume > 0.0005 && !this.particleFluidMode;
413
- if (active) this.motionPreviousPosition.set(this.centerX, this.centerZ);
414
- }
415
-
416
- setParticleFluidMode(enabled: boolean): void {
417
- this.particleFluidMode = Boolean(enabled);
418
- this.updateGeometry();
419
- }
420
-
421
- moveTo(x: number, z: number): void {
422
- this.group.position.x = x;
423
- this.group.position.z = z;
424
- this.updateGeometry();
425
- }
426
-
427
- reset(fillRatio = 0): void {
428
- this.volume = this.capacity * THREE.MathUtils.clamp(fillRatio, 0, 1);
429
- this.overflowRate = 0;
430
- this.motionOverflowRate = 0;
431
- this.overflowIntensity = 0;
432
- this.clearSpillParticles();
433
- this.surface?.clear();
434
- this.motionPreviousPosition.set(this.centerX, this.centerZ);
435
- this.motionVelocity.set(0, 0);
436
- this.motionAcceleration.set(0, 0);
437
- this.sloshOffset.set(0, 0);
438
- this.sloshVelocity.set(0, 0);
439
- this.motionWaveImpulse.set(0, 0);
440
- this.surface?.setContainerMotion(0, 0, 0, 0);
441
- this.updateGeometry();
442
- }
443
-
444
- updateContainerMotion(delta: number): void {
445
- const safeDelta = Math.max(delta, 1 / 240);
446
- const currentX = this.centerX;
447
- const currentZ = this.centerZ;
448
- const rawVelocityX = THREE.MathUtils.clamp(
449
- (currentX - this.motionPreviousPosition.x) / safeDelta,
450
- -3.4,
451
- 3.4,
452
- );
453
- const rawVelocityZ = THREE.MathUtils.clamp(
454
- (currentZ - this.motionPreviousPosition.y) / safeDelta,
455
- -3.4,
456
- 3.4,
457
- );
458
- const previousVelocityX = this.motionVelocity.x;
459
- const previousVelocityZ = this.motionVelocity.y;
460
- const velocityResponse = 1 - Math.exp(-safeDelta * 18);
461
- this.motionVelocity.x = THREE.MathUtils.lerp(previousVelocityX, rawVelocityX, velocityResponse);
462
- this.motionVelocity.y = THREE.MathUtils.lerp(previousVelocityZ, rawVelocityZ, velocityResponse);
463
- this.motionAcceleration.x = THREE.MathUtils.clamp(
464
- (this.motionVelocity.x - previousVelocityX) / safeDelta,
465
- -22,
466
- 22,
467
- );
468
- this.motionAcceleration.y = THREE.MathUtils.clamp(
469
- (this.motionVelocity.y - previousVelocityZ) / safeDelta,
470
- -22,
471
- 22,
472
- );
473
- this.motionPreviousPosition.set(currentX, currentZ);
474
-
475
- const omega = 3.65;
476
- const damping = 0.28;
477
- const targetX = THREE.MathUtils.clamp(
478
- -this.motionAcceleration.x / 9.81 * this.innerLength * 0.5,
479
- -0.18,
480
- 0.18,
481
- );
482
- const targetZ = THREE.MathUtils.clamp(
483
- -this.motionAcceleration.y / 9.81 * this.innerWidth * 0.5,
484
- -0.15,
485
- 0.15,
486
- );
487
- const dampingForce = 2 * damping * omega;
488
- this.sloshVelocity.x += (
489
- (targetX - this.sloshOffset.x) * omega * omega - this.sloshVelocity.x * dampingForce
490
- ) * safeDelta;
491
- this.sloshVelocity.y += (
492
- (targetZ - this.sloshOffset.y) * omega * omega - this.sloshVelocity.y * dampingForce
493
- ) * safeDelta;
494
- this.sloshOffset.addScaledVector(this.sloshVelocity, safeDelta);
495
- this.sloshOffset.x = THREE.MathUtils.clamp(this.sloshOffset.x, -0.16, 0.16);
496
- this.sloshOffset.y = THREE.MathUtils.clamp(this.sloshOffset.y, -0.13, 0.13);
497
-
498
- this.motionWaveImpulse.set(
499
- -this.motionAcceleration.x * safeDelta * 0.00015,
500
- -this.motionAcceleration.y * safeDelta * 0.00015,
501
- );
502
- if (!this.active || this.volume < 0.0005) {
503
- this.sloshOffset.multiplyScalar(Math.exp(-safeDelta * 5));
504
- this.sloshVelocity.multiplyScalar(Math.exp(-safeDelta * 5));
505
- this.motionWaveImpulse.set(0, 0);
506
- }
507
- this.surface?.setContainerMotion(
508
- this.sloshOffset.x,
509
- this.sloshOffset.y,
510
- this.motionWaveImpulse.x,
511
- this.motionWaveImpulse.y,
512
- );
513
- }
514
-
515
- clearSpillParticles(): void {
516
- if (!this.dropLife) return;
517
- this.dropLife.fill(0);
518
- this.dropAlpha.fill(0);
519
- this.dropPositions.fill(-100);
520
- this.dropAccumulator = 0;
521
- if (this.spillPoints) {
522
- this.spillPoints.geometry.attributes.position.needsUpdate = true;
523
- this.spillPoints.geometry.attributes.aAlpha.needsUpdate = true;
524
- }
525
- }
526
-
527
- spawnSpillParticle(): void {
528
- let index = -1;
529
- for (let attempt = 0; attempt < this.dropCount; attempt++) {
530
- const candidate = (this.dropCursor + attempt) % this.dropCount;
531
- if (this.dropLife[candidate] <= 0) {
532
- index = candidate;
533
- this.dropCursor = (candidate + 1) % this.dropCount;
534
- break;
535
- }
536
- }
537
- if (index < 0) return;
538
-
539
- let edge = Math.floor(Math.random() * 4);
540
- if (this.motionOverflowRate > 0.002) {
541
- if (Math.abs(this.sloshOffset.x) > Math.abs(this.sloshOffset.y)) {
542
- edge = this.sloshOffset.x >= 0 ? 3 : 2;
543
- } else {
544
- edge = this.sloshOffset.y >= 0 ? 0 : 1;
545
- }
546
- }
547
- const along = Math.random() - 0.5;
548
- const speedOut = 0.1 + Math.random() * 0.24 + this.overflowIntensity * 0.22;
549
- const velocityOffset = index * 3;
550
- const positionOffset = index * 3;
551
- let x = this.centerX;
552
- let z = this.centerZ;
553
- let vx = (Math.random() - 0.5) * 0.08;
554
- let vz = (Math.random() - 0.5) * 0.08;
555
-
556
- if (edge === 0) {
557
- x += along * this.innerLength * 0.92;
558
- z += this.innerWidth * 0.5 + 0.015;
559
- vz += speedOut;
560
- } else if (edge === 1) {
561
- x += along * this.innerLength * 0.92;
562
- z -= this.innerWidth * 0.5 + 0.015;
563
- vz -= speedOut;
564
- } else if (edge === 2) {
565
- x -= this.innerLength * 0.5 + 0.015;
566
- z += along * this.innerWidth * 0.9;
567
- vx -= speedOut;
568
- } else {
569
- x += this.innerLength * 0.5 + 0.015;
570
- z += along * this.innerWidth * 0.9;
571
- vx += speedOut;
572
- }
573
-
574
- this.dropPositions[positionOffset] = x;
575
- this.dropPositions[positionOffset + 1] = this.maxWaterY + 0.012 + Math.random() * 0.018;
576
- this.dropPositions[positionOffset + 2] = z;
577
- this.dropVelocities[velocityOffset] = vx;
578
- this.dropVelocities[velocityOffset + 1] = 0.03 + Math.random() * 0.19;
579
- this.dropVelocities[velocityOffset + 2] = vz;
580
- this.dropLife[index] = 1.25 + Math.random() * 0.8;
581
- this.dropMaxLife[index] = this.dropLife[index];
582
- this.dropBounces[index] = 0;
583
- this.dropSizes[index] = 2.2 + Math.random() * 3.3;
584
- this.dropAlpha[index] = 0.75 + Math.random() * 0.25;
585
- }
586
-
587
- updateSpillParticles(delta: number): void {
588
- if (this.active && this.overflowRate > 0.0001) {
589
- const spawnRate = Math.min(this.dropCount * 0.86, 140 + this.overflowRate * 2600);
590
- this.dropAccumulator += spawnRate * delta;
591
- const spawnCount = Math.min(48, Math.floor(this.dropAccumulator));
592
- this.dropAccumulator -= spawnCount;
593
- for (let i = 0; i < spawnCount; i++) this.spawnSpillParticle();
594
- }
595
-
596
- const floorY = -0.17;
597
- const drag = Math.exp(-0.18 * delta);
598
- for (let i = 0; i < this.dropCount; i++) {
599
- if (this.dropLife[i] <= 0) continue;
600
- const offset = i * 3;
601
- this.dropLife[i] -= delta;
602
- this.dropVelocities[offset] *= drag;
603
- this.dropVelocities[offset + 1] = this.dropVelocities[offset + 1] * drag - 9.81 * delta;
604
- this.dropVelocities[offset + 2] *= drag;
605
- this.dropPositions[offset] += this.dropVelocities[offset] * delta;
606
- this.dropPositions[offset + 1] += this.dropVelocities[offset + 1] * delta;
607
- this.dropPositions[offset + 2] += this.dropVelocities[offset + 2] * delta;
608
-
609
- if (this.dropPositions[offset + 1] <= floorY && this.dropVelocities[offset + 1] < 0) {
610
- if (this.dropBounces[i] === 0) {
611
- const impact = -this.dropVelocities[offset + 1];
612
- const angle = Math.random() * Math.PI * 2;
613
- const radial = impact * (0.08 + Math.random() * 0.11);
614
- this.dropPositions[offset + 1] = floorY + 0.006;
615
- this.dropVelocities[offset] = Math.cos(angle) * radial;
616
- this.dropVelocities[offset + 1] = impact * (0.17 + Math.random() * 0.12);
617
- this.dropVelocities[offset + 2] = Math.sin(angle) * radial;
618
- this.dropBounces[i] = 1;
619
- this.dropLife[i] = Math.min(this.dropLife[i], 0.48 + Math.random() * 0.18);
620
- this.dropSizes[i] *= 0.72;
621
- } else {
622
- this.dropLife[i] = 0;
623
- }
624
- }
625
-
626
- if (this.dropLife[i] <= 0) {
627
- this.dropAlpha[i] = 0;
628
- this.dropPositions[offset + 1] = -100;
629
- } else {
630
- const lifeRatio = this.dropLife[i] / Math.max(this.dropMaxLife[i], 0.001);
631
- this.dropAlpha[i] = THREE.MathUtils.clamp(lifeRatio * 1.35, 0, 1);
632
- }
633
- }
634
-
635
- this.spillPoints.geometry.attributes.position.needsUpdate = true;
636
- this.spillPoints.geometry.attributes.aAlpha.needsUpdate = true;
637
- this.spillPoints.geometry.attributes.aSize.needsUpdate = true;
638
- }
639
-
640
- updateGeometry(): void {
641
- const waterHeight = Math.max(0.001, this.waterY - this.innerBottomY);
642
- this.waterBody.scale.y = waterHeight;
643
- this.waterBody.position.y = this.innerBottomY + waterHeight * 0.5;
644
- this.waterBody.visible = this.active && this.volume > 0.0005 && !this.particleFluidMode;
645
- if (this.surface) {
646
- this.surface.setBounds({
647
- basinMinX: this.minX,
648
- basinMaxX: this.maxX,
649
- basinHalfZ: this.innerWidth * 0.5,
650
- basinCenterZ: this.centerZ,
651
- waterY: this.waterY,
652
- });
653
- this.surface.mesh.visible = this.active && this.volume > 0.0005 && !this.particleFluidMode;
654
- }
655
- }
656
-
657
- update(
658
- delta: number,
659
- elapsed: number,
660
- {
661
- flowRate,
662
- captureFraction = 0,
663
- impactX,
664
- impactZ = 0,
665
- impactEnergy = 0,
666
- emitting = false,
667
- }: ReservoirUpdateOptions,
668
- ): ReservoirUpdateResult {
669
- this.updateContainerMotion(delta);
670
- const effectiveCapture = this.active && emitting ? THREE.MathUtils.clamp(captureFraction, 0, 1) : 0;
671
- const captured = effectiveCapture > 0.001;
672
- const incomingRate = flowRate * effectiveCapture;
673
- const incomingVolume = incomingRate * delta;
674
- const available = Math.max(0, this.capacity - this.volume);
675
- const stored = Math.min(available, incomingVolume);
676
- const overflowVolume = Math.max(0, incomingVolume - stored);
677
- this.volume = Math.min(this.capacity, this.volume + stored);
678
- const freeboard = Math.max(0, this.maxWaterY - this.waterY);
679
- const crestRise = Math.max(Math.abs(this.sloshOffset.x), Math.abs(this.sloshOffset.y));
680
- const overtoppingHead = Math.max(0, crestRise - freeboard);
681
- this.motionOverflowRate = this.active
682
- ? Math.min(0.38, overtoppingHead * this.surfaceArea * 1.25)
683
- : 0;
684
- const motionOverflowVolume = Math.min(this.volume, this.motionOverflowRate * delta);
685
- this.volume = Math.max(0, this.volume - motionOverflowVolume);
686
- this.overflowRate = (delta > 0 ? overflowVolume / delta : 0) + this.motionOverflowRate;
687
- const targetOverflow = THREE.MathUtils.clamp(this.overflowRate / 0.16, 0, 1);
688
- this.overflowIntensity = THREE.MathUtils.lerp(
689
- this.overflowIntensity,
690
- targetOverflow,
691
- 1 - Math.exp(-delta * (targetOverflow > this.overflowIntensity ? 5.5 : 2.2)),
692
- );
693
- this.updateGeometry();
694
-
695
- this.waterMaterial.uniforms.uTime.value = elapsed;
696
- this.overflowMaterial.uniforms.uTime.value = elapsed;
697
- this.overflowMaterial.uniforms.uOverflow.value = this.overflowIntensity;
698
- this.overflowGroup.visible = this.active && this.overflowIntensity > 0.008;
699
- this.rimFoamMaterial.opacity = this.overflowIntensity * 0.38;
700
- this.rimFoamGroup.visible = this.active && this.overflowIntensity > 0.025;
701
- this.updateSpillParticles(delta);
702
-
703
- if (this.active) {
704
- this.surface?.update(delta, elapsed, impactX, impactEnergy, captured && emitting, impactZ);
705
- }
706
- return {
707
- captured,
708
- captureFraction: effectiveCapture,
709
- incomingRate,
710
- missedRate: Math.max(0, flowRate - incomingRate),
711
- overflowRate: this.overflowRate,
712
- };
713
- }
714
-
715
- setPixelRatio(pixelRatio: number): void {
716
- this.dropMaterial.uniforms.uPixelRatio.value = Math.min(pixelRatio, 2);
717
- }
718
-
719
- get fillRatio(): number {
720
- return this.capacity > 0 ? this.volume / this.capacity : 0;
721
- }
722
-
723
- get waterY(): number {
724
- return THREE.MathUtils.lerp(this.innerBottomY, this.maxWaterY, this.fillRatio);
725
- }
726
-
727
- get centerX(): number {
728
- return this.group.position.x;
729
- }
730
-
731
- get centerZ(): number {
732
- return this.group.position.z;
733
- }
734
-
735
- get minX(): number {
736
- return this.centerX - this.innerLength * 0.5;
737
- }
738
-
739
- get maxX(): number {
740
- return this.centerX + this.innerLength * 0.5;
741
- }
742
-
743
- get collisionBounds(): BasinConfig {
744
- return {
745
- minX: this.minX,
746
- maxX: this.maxX,
747
- halfZ: this.innerWidth * 0.5,
748
- centerZ: this.centerZ,
749
- waterY: this.waterY,
750
- wallTop: this.rimY,
751
- containerFloorY: this.innerBottomY,
752
- };
753
- }
754
- }
1
+ import * as THREE from 'three';
2
+ import { ShallowWaterSurface } from './ShallowWaterSurface';
3
+ import type { BasinConfig } from './types';
4
+
5
+ /**
6
+ * 构造选项:几何口径(length/width/壁高)+ 平面位置(centerX/centerZ)+ 尾流水面网格分辨率。
7
+ * flowTexture 是溢出水花着色器采样的噪声图,缺省时 uniforms.uFlowMap 为 undefined。
8
+ */
9
+ export interface ReservoirOptions {
10
+ flowTexture?: THREE.Texture;
11
+ length?: number;
12
+ width?: number;
13
+ bottomY?: number;
14
+ rimY?: number;
15
+ centerX?: number;
16
+ centerZ?: number;
17
+ waveWidth?: number;
18
+ waveHeight?: number;
19
+ }
20
+
21
+ /** update 的入参:入口水流量 + 撞击点/能量 + 是否正在喷射。 */
22
+ export interface ReservoirUpdateOptions {
23
+ flowRate: number;
24
+ captureFraction?: number;
25
+ impactX: number;
26
+ impactZ?: number;
27
+ impactEnergy?: number;
28
+ emitting?: boolean;
29
+ }
30
+
31
+ /** update 的出参:本次入流/溢流的口径,供调用方做音效或统计。 */
32
+ export interface ReservoirUpdateResult {
33
+ captured: boolean;
34
+ captureFraction: number;
35
+ incomingRate: number;
36
+ missedRate: number;
37
+ overflowRate: number;
38
+ }
39
+
40
+ export class ReservoirSystem {
41
+ /** 几何口径(全部由构造参数推导,构造后不变)。 */
42
+ private readonly length: number;
43
+ private readonly width: number;
44
+ private readonly bottomY: number;
45
+ private readonly rimY: number;
46
+ private readonly innerBottomY: number;
47
+ private readonly maxWaterY: number;
48
+ private readonly innerLength: number;
49
+ private readonly innerWidth: number;
50
+ private readonly surfaceArea: number;
51
+ private readonly capacity: number;
52
+
53
+ /** 水量与溢流状态(每帧变化)。 */
54
+ private volume: number;
55
+ private overflowRate: number;
56
+ private motionOverflowRate: number;
57
+ private overflowIntensity: number;
58
+ private active: boolean;
59
+ private particleFluidMode: boolean;
60
+
61
+ /** 容器晃动(xz 两个分量复用 Vector2 的 x/y)。 */
62
+ private readonly motionPreviousPosition: THREE.Vector2;
63
+ private readonly motionVelocity: THREE.Vector2;
64
+ private readonly motionAcceleration: THREE.Vector2;
65
+ private readonly sloshOffset: THREE.Vector2;
66
+ private readonly sloshVelocity: THREE.Vector2;
67
+ private readonly motionWaveImpulse: THREE.Vector2;
68
+
69
+ /** 池体节点与拾取体。 */
70
+ readonly renderer: THREE.WebGLRenderer;
71
+ readonly group: THREE.Group;
72
+ readonly hitMeshes: THREE.Mesh[];
73
+
74
+ readonly rimFoamMaterial: THREE.MeshBasicMaterial;
75
+ readonly rimFoamGroup: THREE.Group;
76
+ readonly waterMaterial: THREE.ShaderMaterial;
77
+ readonly waterBody: THREE.Mesh;
78
+ /** 浅水波面;非WebGL2 或缺浮点扩展时为 null(此时只保留水体盒)。 */
79
+ readonly surface: ShallowWaterSurface | null;
80
+ readonly overflowMaterial: THREE.ShaderMaterial;
81
+ readonly overflowGroup: THREE.Group;
82
+
83
+ /** 溢流粒子(GPU points)的缓冲与状态。 */
84
+ readonly dropCount: number;
85
+ private readonly dropPositions: Float32Array;
86
+ private readonly dropVelocities: Float32Array;
87
+ private readonly dropLife: Float32Array;
88
+ private readonly dropMaxLife: Float32Array;
89
+ private readonly dropBounces: Uint8Array;
90
+ private readonly dropSizes: Float32Array;
91
+ private readonly dropAlpha: Float32Array;
92
+ private dropCursor: number;
93
+ private dropAccumulator: number;
94
+ readonly dropMaterial: THREE.ShaderMaterial;
95
+ readonly spillPoints: THREE.Points;
96
+
97
+ constructor(renderer: THREE.WebGLRenderer, {
98
+ flowTexture,
99
+ length = 1.72,
100
+ width = 1.28,
101
+ bottomY = -0.13,
102
+ rimY = 1.14,
103
+ centerX = 1.82,
104
+ centerZ = 0,
105
+ waveWidth = 192,
106
+ waveHeight = 128,
107
+ }: ReservoirOptions = {}) {
108
+ this.renderer = renderer;
109
+ this.length = length;
110
+ this.width = width;
111
+ this.bottomY = bottomY;
112
+ this.rimY = rimY;
113
+ this.innerBottomY = bottomY + 0.11;
114
+ this.maxWaterY = rimY - 0.024;
115
+ this.innerLength = length - 0.14;
116
+ this.innerWidth = width - 0.14;
117
+ this.surfaceArea = this.innerLength * this.innerWidth;
118
+ this.capacity = this.surfaceArea * (this.maxWaterY - this.innerBottomY);
119
+ this.volume = 0;
120
+ this.overflowRate = 0;
121
+ this.motionOverflowRate = 0;
122
+ this.overflowIntensity = 0;
123
+ this.active = false;
124
+ this.motionPreviousPosition = new THREE.Vector2(centerX, centerZ);
125
+ this.motionVelocity = new THREE.Vector2();
126
+ this.motionAcceleration = new THREE.Vector2();
127
+ this.sloshOffset = new THREE.Vector2();
128
+ this.sloshVelocity = new THREE.Vector2();
129
+ this.motionWaveImpulse = new THREE.Vector2();
130
+ this.particleFluidMode = false;
131
+
132
+ this.group = new THREE.Group();
133
+ this.group.position.set(centerX, 0, centerZ);
134
+ this.hitMeshes = [];
135
+
136
+ const steel = new THREE.MeshStandardMaterial({ color: 0x71878d, roughness: 0.2, metalness: 0.92 });
137
+ const darkSteel = new THREE.MeshStandardMaterial({ color: 0x172a30, roughness: 0.36, metalness: 0.78 });
138
+ const glass = new THREE.MeshPhysicalMaterial({
139
+ color: 0x9edbe2,
140
+ metalness: 0,
141
+ roughness: 0.08,
142
+ transmission: 0.9,
143
+ transparent: true,
144
+ opacity: 0.19,
145
+ thickness: 0.04,
146
+ ior: 1.48,
147
+ clearcoat: 1,
148
+ clearcoatRoughness: 0.04,
149
+ depthWrite: false,
150
+ side: THREE.DoubleSide,
151
+ });
152
+
153
+ const wallThickness = 0.035;
154
+ const tankHeight = rimY - bottomY;
155
+ const frontBackGeometry = new THREE.BoxGeometry(length, tankHeight, wallThickness);
156
+ const sideGeometry = new THREE.BoxGeometry(wallThickness, tankHeight, width);
157
+ [-1, 1].forEach((side) => {
158
+ const wall = new THREE.Mesh(frontBackGeometry, glass);
159
+ wall.position.set(0, bottomY + tankHeight * 0.5, side * width * 0.5);
160
+ wall.renderOrder = 6;
161
+ this.group.add(wall);
162
+ this.hitMeshes.push(wall);
163
+ });
164
+ [-1, 1].forEach((side) => {
165
+ const wall = new THREE.Mesh(sideGeometry, glass);
166
+ wall.position.set(side * length * 0.5, bottomY + tankHeight * 0.5, 0);
167
+ wall.renderOrder = 6;
168
+ this.group.add(wall);
169
+ this.hitMeshes.push(wall);
170
+ });
171
+
172
+ const base = new THREE.Mesh(new THREE.BoxGeometry(length + 0.08, 0.09, width + 0.08), darkSteel);
173
+ base.position.y = bottomY + 0.025;
174
+ base.castShadow = true;
175
+ base.receiveShadow = true;
176
+ this.group.add(base);
177
+ this.hitMeshes.push(base);
178
+
179
+ const rail = 0.045;
180
+ const verticalGeometry = new THREE.BoxGeometry(rail, tankHeight + 0.08, rail);
181
+ for (const x of [-1, 1]) {
182
+ for (const z of [-1, 1]) {
183
+ const post = new THREE.Mesh(verticalGeometry, steel);
184
+ post.position.set(x * length * 0.5, bottomY + tankHeight * 0.5, z * width * 0.5);
185
+ post.castShadow = true;
186
+ this.group.add(post);
187
+ }
188
+ }
189
+ const railXGeometry = new THREE.BoxGeometry(length + 0.08, rail, rail);
190
+ const railZGeometry = new THREE.BoxGeometry(rail, rail, width + 0.08);
191
+ for (const y of [bottomY + 0.035, rimY]) {
192
+ for (const z of [-1, 1]) {
193
+ const edge = new THREE.Mesh(railXGeometry, steel);
194
+ edge.position.set(0, y, z * width * 0.5);
195
+ edge.castShadow = true;
196
+ this.group.add(edge);
197
+ }
198
+ for (const x of [-1, 1]) {
199
+ const edge = new THREE.Mesh(railZGeometry, steel);
200
+ edge.position.set(x * length * 0.5, y, 0);
201
+ edge.castShadow = true;
202
+ this.group.add(edge);
203
+ }
204
+ }
205
+
206
+ this.rimFoamMaterial = new THREE.MeshBasicMaterial({
207
+ color: 0xbdeff4,
208
+ transparent: true,
209
+ opacity: 0,
210
+ depthWrite: false,
211
+ blending: THREE.AdditiveBlending,
212
+ });
213
+ this.rimFoamGroup = new THREE.Group();
214
+ const foamX = new THREE.BoxGeometry(this.innerLength * 0.96, 0.012, 0.025);
215
+ const foamZ = new THREE.BoxGeometry(0.025, 0.012, this.innerWidth * 0.96);
216
+ for (const z of [-1, 1]) {
217
+ const foam = new THREE.Mesh(foamX, this.rimFoamMaterial);
218
+ foam.position.set(0, this.maxWaterY + 0.014, z * this.innerWidth * 0.5);
219
+ this.rimFoamGroup.add(foam);
220
+ }
221
+ for (const x of [-1, 1]) {
222
+ const foam = new THREE.Mesh(foamZ, this.rimFoamMaterial);
223
+ foam.position.set(x * this.innerLength * 0.5, this.maxWaterY + 0.014, 0);
224
+ this.rimFoamGroup.add(foam);
225
+ }
226
+ this.group.add(this.rimFoamGroup);
227
+
228
+ this.waterMaterial = new THREE.ShaderMaterial({
229
+ uniforms: {
230
+ uTime: { value: 0 },
231
+ uFlowMap: { value: flowTexture },
232
+ uOpacity: { value: 0.42 },
233
+ },
234
+ vertexShader: /* glsl */`
235
+ varying vec2 vUv;
236
+ varying vec3 vWorldPosition;
237
+ varying vec3 vWorldNormal;
238
+ void main() {
239
+ vUv = uv;
240
+ vec4 world = modelMatrix * vec4(position, 1.0);
241
+ vWorldPosition = world.xyz;
242
+ vWorldNormal = normalize(mat3(modelMatrix) * normal);
243
+ gl_Position = projectionMatrix * viewMatrix * world;
244
+ }
245
+ `,
246
+ fragmentShader: /* glsl */`
247
+ uniform float uTime;
248
+ uniform float uOpacity;
249
+ uniform sampler2D uFlowMap;
250
+ varying vec2 vUv;
251
+ varying vec3 vWorldPosition;
252
+ varying vec3 vWorldNormal;
253
+ void main() {
254
+ vec3 viewDir = normalize(cameraPosition - vWorldPosition);
255
+ float fresnel = 0.02 + 0.98 * pow(1.0 - abs(dot(viewDir, normalize(vWorldNormal))), 5.0);
256
+ vec3 detail = texture2D(uFlowMap, vUv * vec2(1.6, 2.8) + vec2(0.0, uTime * 0.018)).rgb;
257
+ float depthTint = smoothstep(-0.2, 1.2, vWorldPosition.y);
258
+ vec3 color = mix(vec3(0.008, 0.12, 0.16), vec3(0.035, 0.34, 0.4), depthTint);
259
+ color += detail * 0.035 + vec3(0.2, 0.62, 0.68) * fresnel * 0.42;
260
+ gl_FragColor = vec4(color, uOpacity + fresnel * 0.18);
261
+ }
262
+ `,
263
+ transparent: true,
264
+ depthWrite: false,
265
+ side: THREE.DoubleSide,
266
+ });
267
+ this.waterBody = new THREE.Mesh(new THREE.BoxGeometry(this.innerLength, 1, this.innerWidth), this.waterMaterial);
268
+ this.waterBody.renderOrder = 3;
269
+ this.group.add(this.waterBody);
270
+
271
+ this.surface = renderer.capabilities.isWebGL2 && renderer.extensions.has('EXT_color_buffer_float')
272
+ ? new ShallowWaterSurface(renderer, {
273
+ width: waveWidth,
274
+ height: waveHeight,
275
+ basinMinX: centerX - this.innerLength * 0.5,
276
+ basinMaxX: centerX + this.innerLength * 0.5,
277
+ basinHalfZ: this.innerWidth * 0.5,
278
+ basinCenterZ: centerZ,
279
+ waterY: this.innerBottomY,
280
+ cpuSampling: true,
281
+ })
282
+ : null;
283
+ if (this.surface) this.surface.mesh.renderOrder = 5;
284
+
285
+ this.overflowMaterial = new THREE.ShaderMaterial({
286
+ uniforms: {
287
+ uTime: { value: 0 },
288
+ uOverflow: { value: 0 },
289
+ uFlowMap: { value: flowTexture },
290
+ },
291
+ vertexShader: /* glsl */`
292
+ uniform float uTime;
293
+ uniform float uOverflow;
294
+ varying vec2 vUv;
295
+ void main() {
296
+ vUv = uv;
297
+ vec3 p = position;
298
+ float fall = 1.0 - uv.y;
299
+ p.x *= mix(0.7, 1.0, uv.y);
300
+ p.z += sin(uv.x * 29.0 + uTime * 5.0) * 0.012 * fall * uOverflow;
301
+ p.y -= sin(uv.x * 17.0 + uTime * 3.7) * 0.016 * fall;
302
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
303
+ }
304
+ `,
305
+ fragmentShader: /* glsl */`
306
+ uniform float uTime;
307
+ uniform float uOverflow;
308
+ uniform sampler2D uFlowMap;
309
+ varying vec2 vUv;
310
+ void main() {
311
+ vec2 flowUvA = vec2(vUv.x * 5.7 + uTime * 0.035, vUv.y * 7.5 + uTime * 0.82);
312
+ vec2 flowUvB = vec2(vUv.x * 9.3 - uTime * 0.028, vUv.y * 4.2 + uTime * 0.57);
313
+ float noiseA = dot(texture2D(uFlowMap, flowUvA).rgb, vec3(0.2, 0.58, 0.22));
314
+ float noiseB = dot(texture2D(uFlowMap, flowUvB).rgb, vec3(0.35, 0.5, 0.15));
315
+ float noise = mix(noiseA, noiseB, 0.46);
316
+ float rivuletField = 0.5 + 0.5 * sin(vUv.x * 71.0 + noise * 9.0 + sin(vUv.y * 13.0) * 2.2);
317
+ float rivulets = smoothstep(0.54 - uOverflow * 0.08, 0.86, rivuletField);
318
+ float edge = smoothstep(0.0, 0.13, vUv.x) * smoothstep(1.0, 0.87, vUv.x);
319
+ float verticalFade = smoothstep(0.0, 0.12, vUv.y);
320
+ float breakup = smoothstep(0.27, 0.76, noise + vUv.y * 0.16);
321
+ float alpha = edge * verticalFade * rivulets * breakup * (0.2 + uOverflow * 0.62);
322
+ if (alpha < 0.025 || uOverflow < 0.01) discard;
323
+ vec3 color = mix(vec3(0.045, 0.32, 0.38), vec3(0.58, 0.92, 0.97), noise * 0.3);
324
+ gl_FragColor = vec4(color, alpha);
325
+ }
326
+ `,
327
+ transparent: true,
328
+ depthWrite: false,
329
+ side: THREE.DoubleSide,
330
+ });
331
+
332
+ this.overflowGroup = new THREE.Group();
333
+ const sheetHeight = 0.62;
334
+ const makeSheet = (sheetWidth: number, x: number, z: number, rotationY = 0): void => {
335
+ const sheet = new THREE.Mesh(new THREE.PlaneGeometry(sheetWidth, sheetHeight, 48, 24), this.overflowMaterial);
336
+ sheet.position.set(x, rimY - sheetHeight * 0.5 - 0.012, z);
337
+ sheet.rotation.y = rotationY;
338
+ sheet.renderOrder = 7;
339
+ this.overflowGroup.add(sheet);
340
+ };
341
+ makeSheet(length * 0.78, 0, -width * 0.5 - 0.026);
342
+ makeSheet(length * 0.78, 0, width * 0.5 + 0.026);
343
+ makeSheet(width * 0.72, -length * 0.5 - 0.026, 0, Math.PI * 0.5);
344
+ makeSheet(width * 0.72, length * 0.5 + 0.026, 0, Math.PI * 0.5);
345
+ this.group.add(this.overflowGroup);
346
+
347
+ this.dropCount = waveWidth <= 112 ? 650 : 1400;
348
+ this.dropPositions = new Float32Array(this.dropCount * 3);
349
+ this.dropVelocities = new Float32Array(this.dropCount * 3);
350
+ this.dropLife = new Float32Array(this.dropCount);
351
+ this.dropMaxLife = new Float32Array(this.dropCount);
352
+ this.dropBounces = new Uint8Array(this.dropCount);
353
+ this.dropSizes = new Float32Array(this.dropCount);
354
+ this.dropAlpha = new Float32Array(this.dropCount);
355
+ this.dropCursor = 0;
356
+ this.dropAccumulator = 0;
357
+ this.dropPositions.fill(-100);
358
+
359
+ const dropGeometry = new THREE.BufferGeometry();
360
+ const positionAttribute = new THREE.BufferAttribute(this.dropPositions, 3);
361
+ positionAttribute.setUsage(THREE.DynamicDrawUsage);
362
+ const alphaAttribute = new THREE.BufferAttribute(this.dropAlpha, 1);
363
+ alphaAttribute.setUsage(THREE.DynamicDrawUsage);
364
+ const sizeAttribute = new THREE.BufferAttribute(this.dropSizes, 1);
365
+ sizeAttribute.setUsage(THREE.DynamicDrawUsage);
366
+ dropGeometry.setAttribute('position', positionAttribute);
367
+ dropGeometry.setAttribute('aAlpha', alphaAttribute);
368
+ dropGeometry.setAttribute('aSize', sizeAttribute);
369
+ this.dropMaterial = new THREE.ShaderMaterial({
370
+ uniforms: {
371
+ uPixelRatio: { value: Math.min(devicePixelRatio, 2) },
372
+ },
373
+ vertexShader: /* glsl */`
374
+ attribute float aAlpha;
375
+ attribute float aSize;
376
+ uniform float uPixelRatio;
377
+ varying float vAlpha;
378
+ void main() {
379
+ vec4 mv = modelViewMatrix * vec4(position, 1.0);
380
+ gl_Position = aAlpha > 0.001 ? projectionMatrix * mv : vec4(2.0, 2.0, 2.0, 1.0);
381
+ gl_PointSize = min(7.5, aSize * uPixelRatio * (5.0 / max(2.0, -mv.z)));
382
+ vAlpha = aAlpha;
383
+ }
384
+ `,
385
+ fragmentShader: /* glsl */`
386
+ varying float vAlpha;
387
+ void main() {
388
+ float d = length(gl_PointCoord - 0.5);
389
+ if (d > 0.5) discard;
390
+ float core = smoothstep(0.5, 0.08, d);
391
+ float glint = 1.0 - smoothstep(0.015, 0.13, distance(gl_PointCoord, vec2(0.34, 0.3)));
392
+ float alpha = core * vAlpha;
393
+ gl_FragColor = vec4(vec3(0.42, 0.87, 0.96) + glint * 0.45, alpha);
394
+ }
395
+ `,
396
+ transparent: true,
397
+ depthWrite: false,
398
+ blending: THREE.AdditiveBlending,
399
+ toneMapped: false,
400
+ });
401
+ this.spillPoints = new THREE.Points(dropGeometry, this.dropMaterial);
402
+ this.spillPoints.frustumCulled = false;
403
+ this.spillPoints.renderOrder = 8;
404
+
405
+ this.reset();
406
+ this.setActive(false);
407
+ }
408
+
409
+ setActive(active: boolean): void {
410
+ this.active = active;
411
+ this.group.visible = active;
412
+ if (this.surface) this.surface.mesh.visible = active && this.volume > 0.0005 && !this.particleFluidMode;
413
+ if (active) this.motionPreviousPosition.set(this.centerX, this.centerZ);
414
+ }
415
+
416
+ setParticleFluidMode(enabled: boolean): void {
417
+ this.particleFluidMode = Boolean(enabled);
418
+ this.updateGeometry();
419
+ }
420
+
421
+ moveTo(x: number, z: number): void {
422
+ this.group.position.x = x;
423
+ this.group.position.z = z;
424
+ this.updateGeometry();
425
+ }
426
+
427
+ reset(fillRatio = 0): void {
428
+ this.volume = this.capacity * THREE.MathUtils.clamp(fillRatio, 0, 1);
429
+ this.overflowRate = 0;
430
+ this.motionOverflowRate = 0;
431
+ this.overflowIntensity = 0;
432
+ this.clearSpillParticles();
433
+ this.surface?.clear();
434
+ this.motionPreviousPosition.set(this.centerX, this.centerZ);
435
+ this.motionVelocity.set(0, 0);
436
+ this.motionAcceleration.set(0, 0);
437
+ this.sloshOffset.set(0, 0);
438
+ this.sloshVelocity.set(0, 0);
439
+ this.motionWaveImpulse.set(0, 0);
440
+ this.surface?.setContainerMotion(0, 0, 0, 0);
441
+ this.updateGeometry();
442
+ }
443
+
444
+ updateContainerMotion(delta: number): void {
445
+ const safeDelta = Math.max(delta, 1 / 240);
446
+ const currentX = this.centerX;
447
+ const currentZ = this.centerZ;
448
+ const rawVelocityX = THREE.MathUtils.clamp(
449
+ (currentX - this.motionPreviousPosition.x) / safeDelta,
450
+ -3.4,
451
+ 3.4,
452
+ );
453
+ const rawVelocityZ = THREE.MathUtils.clamp(
454
+ (currentZ - this.motionPreviousPosition.y) / safeDelta,
455
+ -3.4,
456
+ 3.4,
457
+ );
458
+ const previousVelocityX = this.motionVelocity.x;
459
+ const previousVelocityZ = this.motionVelocity.y;
460
+ const velocityResponse = 1 - Math.exp(-safeDelta * 18);
461
+ this.motionVelocity.x = THREE.MathUtils.lerp(previousVelocityX, rawVelocityX, velocityResponse);
462
+ this.motionVelocity.y = THREE.MathUtils.lerp(previousVelocityZ, rawVelocityZ, velocityResponse);
463
+ this.motionAcceleration.x = THREE.MathUtils.clamp(
464
+ (this.motionVelocity.x - previousVelocityX) / safeDelta,
465
+ -22,
466
+ 22,
467
+ );
468
+ this.motionAcceleration.y = THREE.MathUtils.clamp(
469
+ (this.motionVelocity.y - previousVelocityZ) / safeDelta,
470
+ -22,
471
+ 22,
472
+ );
473
+ this.motionPreviousPosition.set(currentX, currentZ);
474
+
475
+ const omega = 3.65;
476
+ const damping = 0.28;
477
+ const targetX = THREE.MathUtils.clamp(
478
+ -this.motionAcceleration.x / 9.81 * this.innerLength * 0.5,
479
+ -0.18,
480
+ 0.18,
481
+ );
482
+ const targetZ = THREE.MathUtils.clamp(
483
+ -this.motionAcceleration.y / 9.81 * this.innerWidth * 0.5,
484
+ -0.15,
485
+ 0.15,
486
+ );
487
+ const dampingForce = 2 * damping * omega;
488
+ this.sloshVelocity.x += (
489
+ (targetX - this.sloshOffset.x) * omega * omega - this.sloshVelocity.x * dampingForce
490
+ ) * safeDelta;
491
+ this.sloshVelocity.y += (
492
+ (targetZ - this.sloshOffset.y) * omega * omega - this.sloshVelocity.y * dampingForce
493
+ ) * safeDelta;
494
+ this.sloshOffset.addScaledVector(this.sloshVelocity, safeDelta);
495
+ this.sloshOffset.x = THREE.MathUtils.clamp(this.sloshOffset.x, -0.16, 0.16);
496
+ this.sloshOffset.y = THREE.MathUtils.clamp(this.sloshOffset.y, -0.13, 0.13);
497
+
498
+ this.motionWaveImpulse.set(
499
+ -this.motionAcceleration.x * safeDelta * 0.00015,
500
+ -this.motionAcceleration.y * safeDelta * 0.00015,
501
+ );
502
+ if (!this.active || this.volume < 0.0005) {
503
+ this.sloshOffset.multiplyScalar(Math.exp(-safeDelta * 5));
504
+ this.sloshVelocity.multiplyScalar(Math.exp(-safeDelta * 5));
505
+ this.motionWaveImpulse.set(0, 0);
506
+ }
507
+ this.surface?.setContainerMotion(
508
+ this.sloshOffset.x,
509
+ this.sloshOffset.y,
510
+ this.motionWaveImpulse.x,
511
+ this.motionWaveImpulse.y,
512
+ );
513
+ }
514
+
515
+ clearSpillParticles(): void {
516
+ if (!this.dropLife) return;
517
+ this.dropLife.fill(0);
518
+ this.dropAlpha.fill(0);
519
+ this.dropPositions.fill(-100);
520
+ this.dropAccumulator = 0;
521
+ if (this.spillPoints) {
522
+ this.spillPoints.geometry.attributes.position.needsUpdate = true;
523
+ this.spillPoints.geometry.attributes.aAlpha.needsUpdate = true;
524
+ }
525
+ }
526
+
527
+ spawnSpillParticle(): void {
528
+ let index = -1;
529
+ for (let attempt = 0; attempt < this.dropCount; attempt++) {
530
+ const candidate = (this.dropCursor + attempt) % this.dropCount;
531
+ if (this.dropLife[candidate] <= 0) {
532
+ index = candidate;
533
+ this.dropCursor = (candidate + 1) % this.dropCount;
534
+ break;
535
+ }
536
+ }
537
+ if (index < 0) return;
538
+
539
+ let edge = Math.floor(Math.random() * 4);
540
+ if (this.motionOverflowRate > 0.002) {
541
+ if (Math.abs(this.sloshOffset.x) > Math.abs(this.sloshOffset.y)) {
542
+ edge = this.sloshOffset.x >= 0 ? 3 : 2;
543
+ } else {
544
+ edge = this.sloshOffset.y >= 0 ? 0 : 1;
545
+ }
546
+ }
547
+ const along = Math.random() - 0.5;
548
+ const speedOut = 0.1 + Math.random() * 0.24 + this.overflowIntensity * 0.22;
549
+ const velocityOffset = index * 3;
550
+ const positionOffset = index * 3;
551
+ let x = this.centerX;
552
+ let z = this.centerZ;
553
+ let vx = (Math.random() - 0.5) * 0.08;
554
+ let vz = (Math.random() - 0.5) * 0.08;
555
+
556
+ if (edge === 0) {
557
+ x += along * this.innerLength * 0.92;
558
+ z += this.innerWidth * 0.5 + 0.015;
559
+ vz += speedOut;
560
+ } else if (edge === 1) {
561
+ x += along * this.innerLength * 0.92;
562
+ z -= this.innerWidth * 0.5 + 0.015;
563
+ vz -= speedOut;
564
+ } else if (edge === 2) {
565
+ x -= this.innerLength * 0.5 + 0.015;
566
+ z += along * this.innerWidth * 0.9;
567
+ vx -= speedOut;
568
+ } else {
569
+ x += this.innerLength * 0.5 + 0.015;
570
+ z += along * this.innerWidth * 0.9;
571
+ vx += speedOut;
572
+ }
573
+
574
+ this.dropPositions[positionOffset] = x;
575
+ this.dropPositions[positionOffset + 1] = this.maxWaterY + 0.012 + Math.random() * 0.018;
576
+ this.dropPositions[positionOffset + 2] = z;
577
+ this.dropVelocities[velocityOffset] = vx;
578
+ this.dropVelocities[velocityOffset + 1] = 0.03 + Math.random() * 0.19;
579
+ this.dropVelocities[velocityOffset + 2] = vz;
580
+ this.dropLife[index] = 1.25 + Math.random() * 0.8;
581
+ this.dropMaxLife[index] = this.dropLife[index];
582
+ this.dropBounces[index] = 0;
583
+ this.dropSizes[index] = 2.2 + Math.random() * 3.3;
584
+ this.dropAlpha[index] = 0.75 + Math.random() * 0.25;
585
+ }
586
+
587
+ updateSpillParticles(delta: number): void {
588
+ if (this.active && this.overflowRate > 0.0001) {
589
+ const spawnRate = Math.min(this.dropCount * 0.86, 140 + this.overflowRate * 2600);
590
+ this.dropAccumulator += spawnRate * delta;
591
+ const spawnCount = Math.min(48, Math.floor(this.dropAccumulator));
592
+ this.dropAccumulator -= spawnCount;
593
+ for (let i = 0; i < spawnCount; i++) this.spawnSpillParticle();
594
+ }
595
+
596
+ const floorY = -0.17;
597
+ const drag = Math.exp(-0.18 * delta);
598
+ for (let i = 0; i < this.dropCount; i++) {
599
+ if (this.dropLife[i] <= 0) continue;
600
+ const offset = i * 3;
601
+ this.dropLife[i] -= delta;
602
+ this.dropVelocities[offset] *= drag;
603
+ this.dropVelocities[offset + 1] = this.dropVelocities[offset + 1] * drag - 9.81 * delta;
604
+ this.dropVelocities[offset + 2] *= drag;
605
+ this.dropPositions[offset] += this.dropVelocities[offset] * delta;
606
+ this.dropPositions[offset + 1] += this.dropVelocities[offset + 1] * delta;
607
+ this.dropPositions[offset + 2] += this.dropVelocities[offset + 2] * delta;
608
+
609
+ if (this.dropPositions[offset + 1] <= floorY && this.dropVelocities[offset + 1] < 0) {
610
+ if (this.dropBounces[i] === 0) {
611
+ const impact = -this.dropVelocities[offset + 1];
612
+ const angle = Math.random() * Math.PI * 2;
613
+ const radial = impact * (0.08 + Math.random() * 0.11);
614
+ this.dropPositions[offset + 1] = floorY + 0.006;
615
+ this.dropVelocities[offset] = Math.cos(angle) * radial;
616
+ this.dropVelocities[offset + 1] = impact * (0.17 + Math.random() * 0.12);
617
+ this.dropVelocities[offset + 2] = Math.sin(angle) * radial;
618
+ this.dropBounces[i] = 1;
619
+ this.dropLife[i] = Math.min(this.dropLife[i], 0.48 + Math.random() * 0.18);
620
+ this.dropSizes[i] *= 0.72;
621
+ } else {
622
+ this.dropLife[i] = 0;
623
+ }
624
+ }
625
+
626
+ if (this.dropLife[i] <= 0) {
627
+ this.dropAlpha[i] = 0;
628
+ this.dropPositions[offset + 1] = -100;
629
+ } else {
630
+ const lifeRatio = this.dropLife[i] / Math.max(this.dropMaxLife[i], 0.001);
631
+ this.dropAlpha[i] = THREE.MathUtils.clamp(lifeRatio * 1.35, 0, 1);
632
+ }
633
+ }
634
+
635
+ this.spillPoints.geometry.attributes.position.needsUpdate = true;
636
+ this.spillPoints.geometry.attributes.aAlpha.needsUpdate = true;
637
+ this.spillPoints.geometry.attributes.aSize.needsUpdate = true;
638
+ }
639
+
640
+ updateGeometry(): void {
641
+ const waterHeight = Math.max(0.001, this.waterY - this.innerBottomY);
642
+ this.waterBody.scale.y = waterHeight;
643
+ this.waterBody.position.y = this.innerBottomY + waterHeight * 0.5;
644
+ this.waterBody.visible = this.active && this.volume > 0.0005 && !this.particleFluidMode;
645
+ if (this.surface) {
646
+ this.surface.setBounds({
647
+ basinMinX: this.minX,
648
+ basinMaxX: this.maxX,
649
+ basinHalfZ: this.innerWidth * 0.5,
650
+ basinCenterZ: this.centerZ,
651
+ waterY: this.waterY,
652
+ });
653
+ this.surface.mesh.visible = this.active && this.volume > 0.0005 && !this.particleFluidMode;
654
+ }
655
+ }
656
+
657
+ update(
658
+ delta: number,
659
+ elapsed: number,
660
+ {
661
+ flowRate,
662
+ captureFraction = 0,
663
+ impactX,
664
+ impactZ = 0,
665
+ impactEnergy = 0,
666
+ emitting = false,
667
+ }: ReservoirUpdateOptions,
668
+ ): ReservoirUpdateResult {
669
+ this.updateContainerMotion(delta);
670
+ const effectiveCapture = this.active && emitting ? THREE.MathUtils.clamp(captureFraction, 0, 1) : 0;
671
+ const captured = effectiveCapture > 0.001;
672
+ const incomingRate = flowRate * effectiveCapture;
673
+ const incomingVolume = incomingRate * delta;
674
+ const available = Math.max(0, this.capacity - this.volume);
675
+ const stored = Math.min(available, incomingVolume);
676
+ const overflowVolume = Math.max(0, incomingVolume - stored);
677
+ this.volume = Math.min(this.capacity, this.volume + stored);
678
+ const freeboard = Math.max(0, this.maxWaterY - this.waterY);
679
+ const crestRise = Math.max(Math.abs(this.sloshOffset.x), Math.abs(this.sloshOffset.y));
680
+ const overtoppingHead = Math.max(0, crestRise - freeboard);
681
+ this.motionOverflowRate = this.active
682
+ ? Math.min(0.38, overtoppingHead * this.surfaceArea * 1.25)
683
+ : 0;
684
+ const motionOverflowVolume = Math.min(this.volume, this.motionOverflowRate * delta);
685
+ this.volume = Math.max(0, this.volume - motionOverflowVolume);
686
+ this.overflowRate = (delta > 0 ? overflowVolume / delta : 0) + this.motionOverflowRate;
687
+ const targetOverflow = THREE.MathUtils.clamp(this.overflowRate / 0.16, 0, 1);
688
+ this.overflowIntensity = THREE.MathUtils.lerp(
689
+ this.overflowIntensity,
690
+ targetOverflow,
691
+ 1 - Math.exp(-delta * (targetOverflow > this.overflowIntensity ? 5.5 : 2.2)),
692
+ );
693
+ this.updateGeometry();
694
+
695
+ this.waterMaterial.uniforms.uTime.value = elapsed;
696
+ this.overflowMaterial.uniforms.uTime.value = elapsed;
697
+ this.overflowMaterial.uniforms.uOverflow.value = this.overflowIntensity;
698
+ this.overflowGroup.visible = this.active && this.overflowIntensity > 0.008;
699
+ this.rimFoamMaterial.opacity = this.overflowIntensity * 0.38;
700
+ this.rimFoamGroup.visible = this.active && this.overflowIntensity > 0.025;
701
+ this.updateSpillParticles(delta);
702
+
703
+ if (this.active) {
704
+ this.surface?.update(delta, elapsed, impactX, impactEnergy, captured && emitting, impactZ);
705
+ }
706
+ return {
707
+ captured,
708
+ captureFraction: effectiveCapture,
709
+ incomingRate,
710
+ missedRate: Math.max(0, flowRate - incomingRate),
711
+ overflowRate: this.overflowRate,
712
+ };
713
+ }
714
+
715
+ setPixelRatio(pixelRatio: number): void {
716
+ this.dropMaterial.uniforms.uPixelRatio.value = Math.min(pixelRatio, 2);
717
+ }
718
+
719
+ get fillRatio(): number {
720
+ return this.capacity > 0 ? this.volume / this.capacity : 0;
721
+ }
722
+
723
+ get waterY(): number {
724
+ return THREE.MathUtils.lerp(this.innerBottomY, this.maxWaterY, this.fillRatio);
725
+ }
726
+
727
+ get centerX(): number {
728
+ return this.group.position.x;
729
+ }
730
+
731
+ get centerZ(): number {
732
+ return this.group.position.z;
733
+ }
734
+
735
+ get minX(): number {
736
+ return this.centerX - this.innerLength * 0.5;
737
+ }
738
+
739
+ get maxX(): number {
740
+ return this.centerX + this.innerLength * 0.5;
741
+ }
742
+
743
+ get collisionBounds(): BasinConfig {
744
+ return {
745
+ minX: this.minX,
746
+ maxX: this.maxX,
747
+ halfZ: this.innerWidth * 0.5,
748
+ centerZ: this.centerZ,
749
+ waterY: this.waterY,
750
+ wallTop: this.rimY,
751
+ containerFloorY: this.innerBottomY,
752
+ };
753
+ }
754
+ }