three-base 2.0.134 → 2.0.142

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 (305) hide show
  1. package/DirectionalLight.d.ts +6 -6
  2. package/DirectionalLight.js +68 -68
  3. package/DirectionalLight.ts +70 -0
  4. package/EventMap.d.ts +103 -0
  5. package/EventMap.js +17 -0
  6. package/EventMap.ts +139 -0
  7. package/EventMesh.d.ts +18 -18
  8. package/EventMesh.js +59 -59
  9. package/EventMesh.ts +60 -0
  10. package/ExtrudeMesh.d.ts +112 -112
  11. package/ExtrudeMesh.js +342 -366
  12. package/ExtrudeMesh.ts +553 -0
  13. package/FPScontrol.d.ts +511 -71
  14. package/FPScontrol.js +1799 -505
  15. package/FPScontrol.ts +1855 -0
  16. package/Fire/Fire.d.ts +28 -28
  17. package/Fire/Fire.js +80 -79
  18. package/Fire/Fire.ts +89 -0
  19. package/Fire/{FireGromtry.d.ts → FireGeometry.d.ts} +9 -9
  20. package/Fire/{FireGromtry.js → FireGeometry.js} +40 -40
  21. package/Fire/FireGeometry.ts +40 -0
  22. package/Fire/FireMaterial.d.ts +28 -28
  23. package/Fire/FireMaterial.js +32 -32
  24. package/Fire/FireMaterial.ts +89 -0
  25. package/Fire/index.d.ts +2 -1
  26. package/Fire/index.js +2 -1
  27. package/Fire/index.ts +2 -0
  28. package/GLTFDracoLoader.d.ts +71 -0
  29. package/GLTFDracoLoader.js +266 -0
  30. package/GLTFDracoLoader.ts +430 -0
  31. package/Grass/Grass.d.ts +8 -8
  32. package/Grass/Grass.js +38 -38
  33. package/Grass/Grass.ts +38 -0
  34. package/Grass/index.d.ts +2 -1
  35. package/Grass/index.js +2 -1
  36. package/Grass/index.ts +2 -0
  37. package/Ground1/Ground1.d.ts +10 -11
  38. package/Ground1/Ground1.js +95 -91
  39. package/Ground1/Ground1.ts +114 -0
  40. package/Ground1/index.d.ts +1 -1
  41. package/Ground1/index.js +1 -1
  42. package/Ground1/index.ts +1 -0
  43. package/MeshBase.d.ts +36 -35
  44. package/MeshBase.js +247 -223
  45. package/MeshBase.ts +253 -0
  46. package/Mirror.d.ts +25 -25
  47. package/Mirror.js +143 -139
  48. package/Mirror.ts +203 -0
  49. package/MktMesh/MktMesh.d.ts +291 -0
  50. package/MktMesh/MktMesh.js +1045 -0
  51. package/MktMesh/MktMesh.ts +1075 -0
  52. package/MktMesh/assetUrl.d.ts +35 -0
  53. package/MktMesh/assetUrl.js +69 -0
  54. package/MktMesh/assetUrl.ts +69 -0
  55. package/MktMesh/mktMappingShader.d.ts +12 -0
  56. package/MktMesh/mktMappingShader.js +139 -0
  57. package/MktMesh/mktMappingShader.ts +139 -0
  58. package/MktMesh/mktShader.d.ts +263 -0
  59. package/MktMesh/mktShader.js +1248 -0
  60. package/MktMesh/mktShader.ts +1344 -0
  61. package/MktMesh/panoBitmapTexture.d.ts +25 -0
  62. package/MktMesh/panoBitmapTexture.js +52 -0
  63. package/MktMesh/panoBitmapTexture.ts +54 -0
  64. package/MktMesh/panoSlotBind.d.ts +29 -0
  65. package/MktMesh/panoSlotBind.js +106 -0
  66. package/MktMesh/panoSlotBind.ts +153 -0
  67. package/MktMesh/panoSlotRoster.d.ts +22 -0
  68. package/MktMesh/panoSlotRoster.js +169 -0
  69. package/MktMesh/panoSlotRoster.ts +168 -0
  70. package/MktMesh/panoTileStitcher.d.ts +119 -0
  71. package/MktMesh/panoTileStitcher.js +533 -0
  72. package/MktMesh/panoTileStitcher.ts +616 -0
  73. package/MktMesh/types.d.ts +75 -0
  74. package/MktMesh/types.js +1 -0
  75. package/MktMesh/types.ts +87 -0
  76. package/MktMesh/wallCapTessellate.d.ts +33 -0
  77. package/MktMesh/wallCapTessellate.js +97 -0
  78. package/MktMesh/wallCapTessellate.ts +68 -0
  79. package/PipeMesh/PathGeometry.d.ts +44 -44
  80. package/PipeMesh/PathGeometry.js +307 -300
  81. package/PipeMesh/PathGeometry.ts +343 -0
  82. package/PipeMesh/PathPoint.d.ts +17 -17
  83. package/PipeMesh/PathPoint.js +31 -31
  84. package/PipeMesh/PathPoint.ts +36 -0
  85. package/PipeMesh/PathPointList.d.ts +31 -31
  86. package/PipeMesh/PathPointList.js +209 -208
  87. package/PipeMesh/PathPointList.ts +227 -0
  88. package/PipeMesh/PathTubeGeometry.d.ts +25 -25
  89. package/PipeMesh/PathTubeGeometry.js +130 -130
  90. package/PipeMesh/PathTubeGeometry.ts +146 -0
  91. package/PipeMesh/PipeMesh.d.ts +25 -25
  92. package/PipeMesh/PipeMesh.js +63 -63
  93. package/PipeMesh/PipeMesh.ts +76 -0
  94. package/PipeMesh/index.d.ts +1 -1
  95. package/PipeMesh/index.js +1 -1
  96. package/PipeMesh/index.ts +1 -0
  97. package/PlaneModal1/PlaneModal1.d.ts +34 -31
  98. package/PlaneModal1/PlaneModal1.js +109 -105
  99. package/PlaneModal1/PlaneModal1.ts +138 -0
  100. package/PlaneModal1/index.d.ts +1 -1
  101. package/PlaneModal1/index.js +1 -1
  102. package/PlaneModal1/index.ts +1 -0
  103. package/PlaneText.d.ts +27 -39
  104. package/PlaneText.js +35 -30
  105. package/PlaneText.ts +55 -0
  106. package/PlaneWithHtml.d.ts +13 -11
  107. package/PlaneWithHtml.js +64 -63
  108. package/PlaneWithHtml.ts +70 -0
  109. package/PointLight/PointLight.d.ts +13 -14
  110. package/PointLight/PointLight.js +64 -70
  111. package/PointLight/PointLight.ts +67 -0
  112. package/PointLight/index.d.ts +1 -1
  113. package/PointLight/index.js +1 -1
  114. package/PointLight/index.ts +1 -0
  115. package/README.md +110 -79
  116. package/Sky/Sky.d.ts +9 -9
  117. package/Sky/Sky.js +22 -22
  118. package/Sky/Sky.ts +22 -0
  119. package/Sky/index.d.ts +1 -1
  120. package/Sky/index.js +1 -1
  121. package/Sky/index.ts +1 -0
  122. package/SpotLight.d.ts +6 -6
  123. package/SpotLight.js +72 -72
  124. package/SpotLight.ts +74 -0
  125. package/SpriteModal1/SpriteModal1.d.ts +33 -30
  126. package/SpriteModal1/SpriteModal1.js +116 -110
  127. package/SpriteModal1/SpriteModal1.ts +146 -0
  128. package/SpriteModal1/index.d.ts +1 -1
  129. package/SpriteModal1/index.js +1 -1
  130. package/SpriteModal1/index.ts +1 -0
  131. package/SpriteText.d.ts +46 -45
  132. package/SpriteText.js +176 -163
  133. package/SpriteText.ts +205 -0
  134. package/SpriteText2.d.ts +48 -47
  135. package/SpriteText2.js +223 -198
  136. package/SpriteText2.ts +258 -0
  137. package/SpriteText3.d.ts +86 -0
  138. package/SpriteText3.js +519 -0
  139. package/SpriteText3.ts +599 -0
  140. package/SpriteWithHtml.d.ts +9 -7
  141. package/SpriteWithHtml.js +44 -44
  142. package/SpriteWithHtml.ts +50 -0
  143. package/Text3D/Text3D.d.ts +20 -20
  144. package/Text3D/Text3D.js +58 -62
  145. package/Text3D/Text3D.ts +77 -0
  146. package/Text3D/fontController.d.ts +60 -59
  147. package/Text3D/fontController.js +166 -199
  148. package/Text3D/fontController.ts +209 -0
  149. package/Text3D/index.d.ts +1 -1
  150. package/Text3D/index.js +1 -1
  151. package/Text3D/index.ts +1 -0
  152. package/ThreeBase.d.ts +248 -97
  153. package/ThreeBase.js +1202 -613
  154. package/ThreeBase.ts +1314 -0
  155. package/ThreeMouse.d.ts +54 -40
  156. package/ThreeMouse.js +453 -407
  157. package/ThreeMouse.ts +472 -0
  158. package/Transition.d.ts +45 -40
  159. package/Transition.js +209 -210
  160. package/Transition.ts +242 -0
  161. package/Water/Water.d.ts +5 -5
  162. package/Water/Water.js +44 -26
  163. package/Water/Water.ts +45 -0
  164. package/Water/index.d.ts +2 -0
  165. package/Water/index.js +2 -0
  166. package/Water/index.ts +2 -0
  167. package/WaterFlow/FloatingDuck.d.ts +88 -0
  168. package/WaterFlow/FloatingDuck.js +307 -0
  169. package/WaterFlow/FloatingDuck.ts +445 -0
  170. package/WaterFlow/GPUFluidSimulation.d.ts +56 -0
  171. package/WaterFlow/GPUFluidSimulation.js +785 -0
  172. package/WaterFlow/GPUFluidSimulation.ts +827 -0
  173. package/WaterFlow/ReservoirSystem.d.ts +106 -0
  174. package/WaterFlow/ReservoirSystem.js +621 -0
  175. package/WaterFlow/ReservoirSystem.ts +754 -0
  176. package/WaterFlow/SPHField.d.ts +35 -0
  177. package/WaterFlow/SPHField.js +204 -0
  178. package/WaterFlow/SPHField.ts +228 -0
  179. package/WaterFlow/ScreenSpaceFluidPass.d.ts +69 -0
  180. package/WaterFlow/ScreenSpaceFluidPass.js +736 -0
  181. package/WaterFlow/ScreenSpaceFluidPass.ts +789 -0
  182. package/WaterFlow/ShallowWaterSurface.d.ts +45 -0
  183. package/WaterFlow/ShallowWaterSurface.js +441 -0
  184. package/WaterFlow/ShallowWaterSurface.ts +510 -0
  185. package/WaterFlow/WaterFlow.d.ts +87 -0
  186. package/WaterFlow/WaterFlow.js +171 -0
  187. package/WaterFlow/WaterFlow.ts +219 -0
  188. package/WaterFlow/types.d.ts +161 -0
  189. package/WaterFlow/types.js +1 -0
  190. package/WaterFlow/types.ts +189 -0
  191. package/WaterFlow/waterFlowPass.d.ts +3 -0
  192. package/WaterFlow/waterFlowPass.js +20 -0
  193. package/WaterFlow/waterFlowPass.ts +40 -0
  194. package/collisionOBB.d.ts +42 -0
  195. package/collisionOBB.js +112 -0
  196. package/collisionOBB.ts +108 -0
  197. package/declare.d.ts +23 -165
  198. package/index.d.ts +1 -1
  199. package/index.js +1 -2
  200. package/index.ts +1 -0
  201. package/instanced/InstancedBatch.d.ts +57 -0
  202. package/instanced/InstancedBatch.js +348 -0
  203. package/instanced/InstancedBatch.ts +406 -0
  204. package/instanced/InstancedChild.d.ts +174 -0
  205. package/instanced/InstancedChild.js +264 -0
  206. package/instanced/InstancedChild.ts +327 -0
  207. package/instanced/InstancedGroup.d.ts +43 -0
  208. package/instanced/InstancedGroup.js +126 -0
  209. package/instanced/InstancedGroup.ts +137 -0
  210. package/{InstancedMeshBase.d.ts → instanced/InstancedMeshBase.d.ts} +38 -39
  211. package/{InstancedMeshBase.js → instanced/InstancedMeshBase.js} +282 -293
  212. package/instanced/InstancedMeshBase.ts +292 -0
  213. package/instanced/InstancedTemplate.d.ts +46 -0
  214. package/instanced/InstancedTemplate.js +125 -0
  215. package/instanced/InstancedTemplate.ts +188 -0
  216. package/keyControls.d.ts +30 -0
  217. package/{keyContorls.js → keyControls.js} +101 -98
  218. package/keyControls.ts +107 -0
  219. package/lensflare/LensFlare.d.ts +39 -0
  220. package/lensflare/LensFlare.js +373 -0
  221. package/lensflare/LensFlare.ts +448 -0
  222. package/lensflare/lensflare0.png +0 -0
  223. package/lensflare/lensflare3.png +0 -0
  224. package/lensflare/lensflare_custom.png +0 -0
  225. package/lensflare/lensflare_hex_ghost.png +0 -0
  226. package/lensflare/lensflare_hex_main.png +0 -0
  227. package/package.json +101 -26
  228. package/pass/bloom_pass.d.ts +2 -2
  229. package/pass/bloom_pass.js +188 -101
  230. package/pass/bloom_pass.ts +243 -0
  231. package/pass/cover_pass.d.ts +2 -2
  232. package/pass/cover_pass.js +122 -99
  233. package/pass/cover_pass.ts +182 -0
  234. package/pass/debugDepthState.d.ts +20 -0
  235. package/pass/debugDepthState.js +9 -0
  236. package/pass/debugDepthState.ts +22 -0
  237. package/pass/depthTestBias.d.ts +12 -0
  238. package/pass/depthTestBias.js +12 -0
  239. package/pass/depthTestBias.ts +12 -0
  240. package/pass/effectUserData.d.ts +27 -0
  241. package/pass/effectUserData.js +1 -0
  242. package/pass/effectUserData.ts +46 -0
  243. package/pass/frosted_glass_pass.d.ts +2 -2
  244. package/pass/frosted_glass_pass.js +167 -186
  245. package/pass/frosted_glass_pass.ts +288 -0
  246. package/pass/fsQuad.d.ts +9 -9
  247. package/pass/fsQuad.js +28 -28
  248. package/pass/fsQuad.ts +34 -0
  249. package/pass/glow_pass.d.ts +2 -2
  250. package/pass/glow_pass.js +449 -233
  251. package/pass/glow_pass.ts +654 -0
  252. package/pass/screen_bloom_pass.d.ts +11 -0
  253. package/pass/screen_bloom_pass.js +235 -0
  254. package/pass/screen_bloom_pass.ts +263 -0
  255. package/pass/ssao_pass.d.ts +88 -11
  256. package/pass/ssao_pass.js +802 -288
  257. package/pass/ssao_pass.ts +918 -0
  258. package/pass/stroke_pass.d.ts +2 -2
  259. package/pass/stroke_pass.js +329 -258
  260. package/pass/stroke_pass.ts +638 -0
  261. package/shaders/FXAAShaderMaterial.d.ts +3 -3
  262. package/shaders/FXAAShaderMaterial.js +23 -66
  263. package/shaders/FXAAShaderMaterial.ts +201 -0
  264. package/shaders/baseDepthVisShader.d.ts +11 -0
  265. package/shaders/baseDepthVisShader.js +36 -0
  266. package/shaders/baseDepthVisShader.ts +36 -0
  267. package/shaders/baseTextureToRenderShader.d.ts +2 -2
  268. package/shaders/baseTextureToRenderShader.js +55 -60
  269. package/shaders/baseTextureToRenderShader.ts +232 -0
  270. package/shaders/copyShader.d.ts +3 -3
  271. package/shaders/copyShader.js +13 -14
  272. package/shaders/copyShader.ts +32 -0
  273. package/shaders/debugDepthCompareShader.d.ts +14 -0
  274. package/shaders/debugDepthCompareShader.js +49 -0
  275. package/shaders/debugDepthCompareShader.ts +50 -0
  276. package/shaders/debugDepthJudgeShader.d.ts +18 -0
  277. package/shaders/debugDepthJudgeShader.js +52 -0
  278. package/shaders/debugDepthJudgeShader.ts +54 -0
  279. package/shaders/debugExcludedShader.d.ts +13 -0
  280. package/shaders/debugExcludedShader.js +46 -0
  281. package/shaders/debugExcludedShader.ts +48 -0
  282. package/spriteTextBg.d.ts +1 -0
  283. package/spriteTextBg.js +20 -0
  284. package/spriteTextBg.ts +20 -0
  285. package/threeLoader.d.ts +35 -34
  286. package/threeLoader.js +518 -527
  287. package/threeLoader.ts +525 -0
  288. package/threeUtils.d.ts +53 -52
  289. package/threeUtils.js +329 -330
  290. package/threeUtils.ts +373 -0
  291. package/types.d.ts +2 -0
  292. package/types.js +6 -0
  293. package/types.ts +8 -0
  294. package/ApngTexture.d.ts +0 -13
  295. package/ApngTexture.js +0 -61
  296. package/ApngTextureManage.d.ts +0 -18
  297. package/ApngTextureManage.js +0 -87
  298. package/KeyFrameAnimation.d.ts +0 -25
  299. package/KeyFrameAnimation.js +0 -78
  300. package/keyContorls.d.ts +0 -25
  301. package/lib/draco/draco_decoder.js +0 -34
  302. package/lib/draco/draco_decoder.wasm +0 -0
  303. package/lib/draco/draco_wasm_wrapper.js +0 -117
  304. package/shaders.d.ts +0 -5
  305. package/shaders.js +0 -93
@@ -0,0 +1,827 @@
1
+ import * as THREE from 'three';
2
+ import { SPHField } from './SPHField';
3
+ import type { BasinConfig, FluidParameters, FluidSimulationOptions, ReservoirState } from './types';
4
+
5
+ const quadVertex = /* glsl */`
6
+ out vec2 vUv;
7
+ void main() {
8
+ vUv = uv;
9
+ gl_Position = vec4(position, 1.0);
10
+ }
11
+ `;
12
+
13
+ function createStateTarget(size: number): THREE.WebGLRenderTarget {
14
+ const target = new THREE.WebGLRenderTarget(size, size, {
15
+ count: 2,
16
+ format: THREE.RGBAFormat,
17
+ type: THREE.FloatType,
18
+ minFilter: THREE.NearestFilter,
19
+ magFilter: THREE.NearestFilter,
20
+ wrapS: THREE.ClampToEdgeWrapping,
21
+ wrapT: THREE.ClampToEdgeWrapping,
22
+ depthBuffer: false,
23
+ stencilBuffer: false,
24
+ });
25
+ target.textures[0].name = 'FluidPositionLife';
26
+ target.textures[1].name = 'FluidVelocityImpact';
27
+ return target;
28
+ }
29
+
30
+ function makeDataTexture(data: Float32Array, size: number): THREE.DataTexture {
31
+ const texture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat, THREE.FloatType);
32
+ texture.minFilter = THREE.NearestFilter;
33
+ texture.magFilter = THREE.NearestFilter;
34
+ texture.wrapS = THREE.ClampToEdgeWrapping;
35
+ texture.wrapT = THREE.ClampToEdgeWrapping;
36
+ texture.needsUpdate = true;
37
+ return texture;
38
+ }
39
+
40
+ /**
41
+ * GPUFluidSimulation —— GPGPU 粒子流体模拟(ping-pong 状态纹理 + 网格辅助 SPH)。
42
+ *
43
+ * ⚠️ 不支持时(非 WebGL2 或缺 EXT_color_buffer_float)构造器**提前 return**,
44
+ * 下列标记 `| null` 的资源字段在空壳实例上不会被赋值;所有读它们的入口
45
+ * (reset / update / 各 setXxx / dispose / positionTexture)都先过 `if (!this.supported) return;` 守卫。
46
+ */
47
+ export class GPUFluidSimulation {
48
+ readonly renderer: THREE.WebGLRenderer;
49
+ readonly size: number;
50
+ readonly count: number;
51
+ readonly nozzle: THREE.Vector3;
52
+ /** x=minX, y=maxX, z=zHalfExtent, w=waterY(与 shader 的 uBasin 一致)。 */
53
+ readonly basin: THREE.Vector4;
54
+ readonly jetDirection: THREE.Vector3;
55
+ /** 1=开启粒子与 basin/地面碰撞回弹,0=关闭(喂给 shader 的 uCollision,故为 0/1)。 */
56
+ collision: number;
57
+ readonly supported: boolean;
58
+
59
+ /** ping-pong 状态目标;update() 每帧整体交换 read/write,故非 readonly。 */
60
+ read: THREE.WebGLRenderTarget | null;
61
+ /** ping-pong 状态目标;update() 每帧整体交换 read/write,故非 readonly。 */
62
+ write: THREE.WebGLRenderTarget | null;
63
+ readonly sphField: SPHField | null;
64
+ readonly sphDomainMinimum: THREE.Vector3;
65
+ readonly sphDomainMaximum: THREE.Vector3;
66
+ readonly scene: THREE.Scene;
67
+ readonly camera: THREE.OrthographicCamera;
68
+ /** 材质在 reset/update 间切换(initMaterial ↔ updateMaterial),故类型含 null。 */
69
+ readonly mesh: THREE.Mesh<THREE.PlaneGeometry, THREE.ShaderMaterial | null>;
70
+
71
+ readonly initialPosition: THREE.DataTexture | null;
72
+ readonly initialVelocity: THREE.DataTexture | null;
73
+ readonly initMaterial: THREE.ShaderMaterial | null;
74
+ readonly updateMaterial: THREE.ShaderMaterial | null;
75
+
76
+ constructor(renderer: THREE.WebGLRenderer, {
77
+ size = 200,
78
+ nozzle = new THREE.Vector3(0.58, 1.55, 0),
79
+ basin = new THREE.Vector4(0.25, 3.85, 0.82, 0.2),
80
+ speed = 2.4,
81
+ aperture = 0.78,
82
+ jetDirection = new THREE.Vector3(1, 0, 0),
83
+ collision = true,
84
+ }: FluidSimulationOptions = {}) {
85
+ this.renderer = renderer;
86
+ this.size = size;
87
+ this.count = size * size;
88
+ this.nozzle = nozzle.clone();
89
+ this.basin = basin.clone();
90
+ this.jetDirection = jetDirection.clone().normalize();
91
+ this.collision = collision ? 1 : 0;
92
+ this.supported = renderer.capabilities.isWebGL2 && renderer.extensions.has('EXT_color_buffer_float');
93
+
94
+ if (!this.supported) return;
95
+
96
+ this.read = createStateTarget(size);
97
+ this.write = createStateTarget(size);
98
+ this.sphField = new SPHField(renderer, size, { mobile: size <= 128 });
99
+ this.sphDomainMinimum = new THREE.Vector3();
100
+ this.sphDomainMaximum = new THREE.Vector3();
101
+ this.scene = new THREE.Scene();
102
+ this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
103
+ this.mesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), null);
104
+ this.mesh.frustumCulled = false;
105
+ this.scene.add(this.mesh);
106
+
107
+ const positionData = new Float32Array(this.count * 4);
108
+ const velocityData = new Float32Array(this.count * 4);
109
+ for (let i = 0; i < this.count; i++) {
110
+ const seed = this.hash(i + 1);
111
+ positionData[i * 4] = this.nozzle.x;
112
+ positionData[i * 4 + 1] = this.nozzle.y;
113
+ positionData[i * 4 + 2] = this.nozzle.z;
114
+ positionData[i * 4 + 3] = -seed * 2.35;
115
+ velocityData[i * 4] = 0;
116
+ velocityData[i * 4 + 1] = 0;
117
+ velocityData[i * 4 + 2] = 0;
118
+ velocityData[i * 4 + 3] = 0;
119
+ }
120
+
121
+ this.initialPosition = makeDataTexture(positionData, size);
122
+ this.initialVelocity = makeDataTexture(velocityData, size);
123
+
124
+ this.initMaterial = new THREE.ShaderMaterial({
125
+ glslVersion: THREE.GLSL3,
126
+ uniforms: {
127
+ uInitialPosition: { value: this.initialPosition },
128
+ uInitialVelocity: { value: this.initialVelocity },
129
+ },
130
+ vertexShader: quadVertex,
131
+ fragmentShader: /* glsl */`
132
+ precision highp float;
133
+ uniform sampler2D uInitialPosition;
134
+ uniform sampler2D uInitialVelocity;
135
+ in vec2 vUv;
136
+ layout(location = 0) out vec4 outPosition;
137
+ layout(location = 1) out vec4 outVelocity;
138
+ void main() {
139
+ outPosition = texture(uInitialPosition, vUv);
140
+ outVelocity = texture(uInitialVelocity, vUv);
141
+ }
142
+ `,
143
+ depthTest: false,
144
+ depthWrite: false,
145
+ toneMapped: false,
146
+ });
147
+
148
+ this.updateMaterial = new THREE.ShaderMaterial({
149
+ glslVersion: THREE.GLSL3,
150
+ uniforms: {
151
+ uPosition: { value: null },
152
+ uVelocity: { value: null },
153
+ uDelta: { value: 1 / 90 },
154
+ uTime: { value: 0 },
155
+ uSpeed: { value: speed },
156
+ uAperture: { value: aperture },
157
+ uFillRatio: { value: 1 },
158
+ uEmission: { value: 1 },
159
+ uReservoirActive: { value: 0 },
160
+ uReservoirFill: { value: 0 },
161
+ uNozzle: { value: this.nozzle },
162
+ uBasin: { value: this.basin },
163
+ uBasinCenterZ: { value: 0 },
164
+ uWallTop: { value: this.basin.w + 0.22 },
165
+ uFloorY: { value: -0.17 },
166
+ uContainerFloorY: { value: 0.06 },
167
+ uContainerAcceleration: { value: new THREE.Vector2() },
168
+ uRimWidth: { value: 0.055 },
169
+ uTurbulence: { value: 0.36 },
170
+ uSPHDensity: { value: this.sphField.densityTexture },
171
+ uSPHMomentum: { value: this.sphField.momentumTexture },
172
+ uSPHDomainMin: { value: this.sphField.domainMin },
173
+ uSPHDomainMax: { value: this.sphField.domainMax },
174
+ uSPHGrid: { value: this.sphField.grid },
175
+ uSPHAtlasTiles: { value: this.sphField.tiles },
176
+ uSPHCell: { value: this.sphField.cellSize },
177
+ uSPHKernel: { value: this.sphField.kernelRadius },
178
+ uSPHRestDensity: { value: size <= 128 ? 1.45 : 2.2 },
179
+ uSPHPressure: { value: size <= 128 ? 7.2 : 9.4 },
180
+ uSPHViscosity: { value: 1.15 },
181
+ uSPHSurfaceTension: { value: 0.72 },
182
+ uJetDir: { value: this.jetDirection },
183
+ uCollision: { value: this.collision },
184
+ },
185
+ vertexShader: quadVertex,
186
+ fragmentShader: /* glsl */`
187
+ precision highp float;
188
+ uniform sampler2D uPosition;
189
+ uniform sampler2D uVelocity;
190
+ uniform float uDelta;
191
+ uniform float uTime;
192
+ uniform float uSpeed;
193
+ uniform float uAperture;
194
+ uniform float uFillRatio;
195
+ uniform float uEmission;
196
+ uniform float uReservoirActive;
197
+ uniform float uReservoirFill;
198
+ uniform float uTurbulence;
199
+ uniform float uBasinCenterZ;
200
+ uniform float uWallTop;
201
+ uniform float uFloorY;
202
+ uniform float uContainerFloorY;
203
+ uniform vec2 uContainerAcceleration;
204
+ uniform float uRimWidth;
205
+ uniform float uSPHKernel;
206
+ uniform float uSPHRestDensity;
207
+ uniform float uSPHPressure;
208
+ uniform float uSPHViscosity;
209
+ uniform float uSPHSurfaceTension;
210
+ uniform vec3 uNozzle;
211
+ uniform vec4 uBasin; // minX, maxX, zHalfExtent, waterY
212
+ uniform vec3 uJetDir; // 喷流轴向(已归一化)
213
+ uniform float uCollision; // 1=开启粒子与 basin/地面碰撞回弹,0=关闭(粒子自由飞行)
214
+ uniform sampler2D uSPHDensity;
215
+ uniform sampler2D uSPHMomentum;
216
+ uniform vec3 uSPHDomainMin;
217
+ uniform vec3 uSPHDomainMax;
218
+ uniform vec3 uSPHGrid;
219
+ uniform vec2 uSPHAtlasTiles;
220
+ uniform vec3 uSPHCell;
221
+ in vec2 vUv;
222
+ layout(location = 0) out vec4 outPosition;
223
+ layout(location = 1) out vec4 outVelocity;
224
+
225
+ float hash12(vec2 p) {
226
+ vec3 p3 = fract(vec3(p.xyx) * 0.1031);
227
+ p3 += dot(p3, p3.yzx + 33.33);
228
+ return fract((p3.x + p3.y) * p3.z);
229
+ }
230
+
231
+ vec2 hash22(vec2 p) {
232
+ float n = hash12(p);
233
+ return vec2(n, hash12(p + n + 19.19));
234
+ }
235
+
236
+ bool insideSPHDomain(vec3 position) {
237
+ return all(greaterThanEqual(position, uSPHDomainMin))
238
+ && all(lessThanEqual(position, uSPHDomainMax));
239
+ }
240
+
241
+ vec2 sphAtlasUv(vec2 normalizedXY, float slice) {
242
+ vec2 tile = vec2(mod(slice, uSPHAtlasTiles.x), floor(slice / uSPHAtlasTiles.x));
243
+ vec2 localCell = normalizedXY * (uSPHGrid.xy - 1.0) + vec2(0.5);
244
+ return (tile * uSPHGrid.xy + localCell) / (uSPHGrid.xy * uSPHAtlasTiles);
245
+ }
246
+
247
+ vec4 sampleSPHField(sampler2D field, vec3 position) {
248
+ if (!insideSPHDomain(position)) return vec4(0.0);
249
+ vec3 normalizedPosition = clamp(
250
+ (position - uSPHDomainMin) / (uSPHDomainMax - uSPHDomainMin),
251
+ vec3(0.0), vec3(1.0)
252
+ );
253
+ float sliceCoordinate = normalizedPosition.z * (uSPHGrid.z - 1.0);
254
+ float sliceA = floor(sliceCoordinate);
255
+ float sliceB = min(sliceA + 1.0, uSPHGrid.z - 1.0);
256
+ float blendZ = fract(sliceCoordinate);
257
+ vec4 sampleA = texture(field, sphAtlasUv(normalizedPosition.xy, sliceA));
258
+ vec4 sampleB = texture(field, sphAtlasUv(normalizedPosition.xy, sliceB));
259
+ return mix(sampleA, sampleB, blendZ);
260
+ }
261
+
262
+ float particleDensity(vec3 position) {
263
+ return sampleSPHField(uSPHDensity, position).r;
264
+ }
265
+
266
+ vec3 neighborVelocity(vec3 position) {
267
+ vec4 momentum = sampleSPHField(uSPHMomentum, position);
268
+ return momentum.rgb / max(momentum.a, 0.08);
269
+ }
270
+
271
+ float boundaryKernel(float distanceToWall) {
272
+ float q = max(0.0, 1.0 - abs(distanceToWall) / max(uSPHKernel, 0.001));
273
+ return q * q * uSPHRestDensity * 2.35;
274
+ }
275
+
276
+ float boundaryDensity(vec3 position) {
277
+ float density = 0.0;
278
+ bool withinX = position.x > uBasin.x - uSPHKernel
279
+ && position.x < uBasin.y + uSPHKernel;
280
+ bool withinZ = abs(position.z - uBasinCenterZ) < uBasin.z + uSPHKernel;
281
+ bool withinWallHeight = position.y < uWallTop + uSPHKernel
282
+ && position.y > uFloorY - uSPHKernel;
283
+
284
+ if (withinWallHeight && withinZ) {
285
+ density += boundaryKernel(position.x - uBasin.x);
286
+ density += boundaryKernel(position.x - uBasin.y);
287
+ }
288
+ if (withinWallHeight && withinX) {
289
+ density += boundaryKernel(position.z - (uBasinCenterZ - uBasin.z));
290
+ density += boundaryKernel(position.z - (uBasinCenterZ + uBasin.z));
291
+ }
292
+ if (withinX && withinZ) {
293
+ density += boundaryKernel(position.y - uContainerFloorY);
294
+ bool outsideOpening = position.x <= uBasin.x || position.x >= uBasin.y
295
+ || abs(position.z - uBasinCenterZ) >= uBasin.z;
296
+ if (outsideOpening) density += boundaryKernel(position.y - uWallTop);
297
+ }
298
+ return density;
299
+ }
300
+
301
+ float totalDensity(vec3 position) {
302
+ return particleDensity(position) + boundaryDensity(position);
303
+ }
304
+
305
+ float wettedFraction(float fillRatio) {
306
+ float axis = clamp(1.0 - 2.0 * fillRatio, -1.0, 1.0);
307
+ return (acos(axis) - axis * sqrt(max(0.0, 1.0 - axis * axis))) / 3.14159265359;
308
+ }
309
+
310
+ float reservoirParticleFraction() {
311
+ float fill = clamp(uReservoirFill, 0.0, 1.0);
312
+ // Keep particle density approximately constant as the retained volume
313
+ // rises, so screen-space reconstruction has a stable physical scale.
314
+ float volumeFraction = fill * 0.78;
315
+ return uReservoirActive * smoothstep(0.002, 0.018, fill) * volumeFraction;
316
+ }
317
+
318
+ void spawnStoredWater(inout vec4 positionLife, inout vec4 velocityImpact, float seed) {
319
+ vec2 randomXZ = hash22(vUv * 1257.3 + seed * 47.1);
320
+ float layerJitter = hash12(vUv * 2089.7 + seed * 83.4) - 0.5;
321
+ float inset = max(0.026, uSPHKernel * 0.32);
322
+ float bottom = uContainerFloorY + inset;
323
+ float top = max(bottom + 0.012, uBasin.w - inset * 0.55);
324
+ float fill = max(clamp(uReservoirFill, 0.0, 1.0), 0.002);
325
+ float fullColumnHeight = max((uBasin.w - bottom) / fill, 0.012);
326
+ // The seed also determines when a retained particle becomes active.
327
+ // Mapping that activation order to vertical layers keeps particle mass
328
+ // proportional to visible height while the reservoir fills or drains.
329
+ float layer = clamp(seed / 0.78, 0.0, 1.0);
330
+ float layerY = bottom + layer * fullColumnHeight
331
+ + layerJitter * min(uSPHKernel * 0.38, fullColumnHeight * 0.018);
332
+ positionLife = vec4(
333
+ mix(uBasin.x + inset, uBasin.y - inset, randomXZ.x),
334
+ clamp(layerY, bottom, top),
335
+ mix(uBasinCenterZ - uBasin.z + inset, uBasinCenterZ + uBasin.z - inset, randomXZ.y),
336
+ 0.2 + seed * 0.25
337
+ );
338
+ vec2 drift = hash22(vUv * 733.1 + uTime * 0.017) * 2.0 - 1.0;
339
+ velocityImpact = vec4(drift.x * 0.025, 0.0, drift.y * 0.025, -1.0);
340
+ }
341
+
342
+ void respawn(inout vec4 positionLife, inout vec4 velocityImpact, float seed) {
343
+ float outletRadius = 0.24 * uAperture;
344
+ float surfaceY = -outletRadius + 2.0 * outletRadius * clamp(uFillRatio, 0.0, 1.0);
345
+ vec2 disk = vec2(0.0, -outletRadius * 0.82);
346
+ bool accepted = false;
347
+ for (int i = 0; i < 10; i++) {
348
+ vec2 randomPair = hash22(vUv * 911.7 + seed * 31.3 + float(i) * 17.71);
349
+ vec2 candidate = (randomPair * 2.0 - 1.0) * outletRadius * 0.96;
350
+ if (!accepted && dot(candidate, candidate) < outletRadius * outletRadius * 0.92 && candidate.x <= surfaceY) {
351
+ disk = candidate;
352
+ accepted = true;
353
+ }
354
+ }
355
+ float radius01 = clamp(length(disk) / max(outletRadius, 0.001), 0.0, 1.0);
356
+ float profile = pow(max(0.02, 1.0 - radius01), 1.0 / 7.0);
357
+ vec3 jetAxis = normalize(uJetDir);
358
+ float axial = uSpeed * mix(0.88, 1.09, profile);
359
+ float wobble = (hash12(vUv * 173.1 + uTime) - 0.5) * uTurbulence;
360
+
361
+ positionLife = vec4(uNozzle + vec3(0.0, disk.x, disk.y), 0.0);
362
+ vec3 jetJitter = vec3(-disk.x * axial * 0.055 + wobble * 0.045, 0.0, -disk.y * axial * 0.055 + wobble * 0.055);
363
+ velocityImpact = vec4(jetAxis * axial + jetJitter, 0.0);
364
+ }
365
+
366
+ void main() {
367
+ vec4 positionLife = texture(uPosition, vUv);
368
+ vec4 velocityImpact = texture(uVelocity, vUv);
369
+ float seed = hash12(vUv * 1459.3 + 0.17);
370
+ vec3 jetAxis = normalize(uJetDir);
371
+ float maxLife = 2.65 + seed * 1.2;
372
+ float storedFraction = reservoirParticleFraction();
373
+
374
+ if (positionLife.w < 0.0) {
375
+ positionLife.xyz = uNozzle;
376
+ velocityImpact = vec4(0.0);
377
+ if (seed < storedFraction) {
378
+ spawnStoredWater(positionLife, velocityImpact, seed);
379
+ } else if (uEmission > 0.5) {
380
+ float activeFraction = mix(0.08, 1.0, wettedFraction(uFillRatio));
381
+ float emissionSeed = (seed - storedFraction) / max(1.0 - storedFraction, 0.001);
382
+ if (emissionSeed <= activeFraction) {
383
+ positionLife.w += uDelta;
384
+ if (positionLife.w >= 0.0) respawn(positionLife, velocityImpact, seed);
385
+ } else {
386
+ positionLife.w = -0.25 - seed * 0.2;
387
+ }
388
+ }
389
+ outPosition = positionLife;
390
+ outVelocity = velocityImpact;
391
+ return;
392
+ }
393
+
394
+ bool storedParticle = velocityImpact.w < -0.5;
395
+ if (storedParticle && (uReservoirActive < 0.5 || seed >= storedFraction)) {
396
+ positionLife = vec4(uNozzle, uEmission > 0.5 ? -0.05 - seed * 0.35 : -10.0 - seed);
397
+ velocityImpact = vec4(0.0);
398
+ outPosition = positionLife;
399
+ outVelocity = velocityImpact;
400
+ return;
401
+ }
402
+
403
+ positionLife.w = storedParticle ? min(positionLife.w + uDelta, 0.8) : positionLife.w + uDelta;
404
+ if (!storedParticle) velocityImpact.w = max(0.0, velocityImpact.w - uDelta * 0.5);
405
+
406
+ vec3 previousPosition = positionLife.xyz;
407
+ vec3 velocity = velocityImpact.xyz;
408
+
409
+ // Grid-assisted SPH: Poly6 density splats are sampled around each particle.
410
+ // Analytic boundary density acts like stationary ghost particles at solid walls.
411
+ float densityCenter = totalDensity(positionLife.xyz);
412
+ float densityPX = totalDensity(positionLife.xyz + vec3(uSPHCell.x, 0.0, 0.0));
413
+ float densityMX = totalDensity(positionLife.xyz - vec3(uSPHCell.x, 0.0, 0.0));
414
+ float densityPY = totalDensity(positionLife.xyz + vec3(0.0, uSPHCell.y, 0.0));
415
+ float densityMY = totalDensity(positionLife.xyz - vec3(0.0, uSPHCell.y, 0.0));
416
+ float densityPZ = totalDensity(positionLife.xyz + vec3(0.0, 0.0, uSPHCell.z));
417
+ float densityMZ = totalDensity(positionLife.xyz - vec3(0.0, 0.0, uSPHCell.z));
418
+ vec3 normalizedPressureGradient = vec3(
419
+ (densityPX - densityMX) / max(densityPX + densityMX + uSPHRestDensity, 0.001),
420
+ (densityPY - densityMY) / max(densityPY + densityMY + uSPHRestDensity, 0.001),
421
+ (densityPZ - densityMZ) / max(densityPZ + densityMZ + uSPHRestDensity, 0.001)
422
+ );
423
+ float compression = smoothstep(
424
+ uSPHRestDensity * 0.72,
425
+ uSPHRestDensity * 3.4,
426
+ densityCenter
427
+ );
428
+ vec3 pressureAcceleration = -normalizedPressureGradient * uSPHPressure * compression;
429
+ if (storedParticle) pressureAcceleration *= 0.9;
430
+ float pressureMagnitude = length(pressureAcceleration);
431
+ float pressureLimit = storedParticle ? 18.0 : 18.0;
432
+ if (pressureMagnitude > pressureLimit) pressureAcceleration *= pressureLimit / pressureMagnitude;
433
+ velocity += pressureAcceleration * uDelta;
434
+
435
+ float particleDensityCenter = particleDensity(positionLife.xyz);
436
+ vec4 localMomentum = sampleSPHField(uSPHMomentum, positionLife.xyz);
437
+ float neighborCoupling = smoothstep(0.18, uSPHRestDensity * 1.25, particleDensityCenter)
438
+ * smoothstep(0.06, 0.42, localMomentum.a);
439
+ vec3 smoothedVelocity = localMomentum.rgb / max(localMomentum.a, 0.08);
440
+ // Water should exchange momentum without moving as one viscous blob.
441
+ float viscosityMultiplier = storedParticle ? 0.32 : 1.0;
442
+ velocity += (smoothedVelocity - velocity)
443
+ * uSPHViscosity * viscosityMultiplier * neighborCoupling * uDelta;
444
+
445
+ vec3 surfaceGradient = vec3(
446
+ particleDensity(positionLife.xyz + vec3(uSPHCell.x, 0.0, 0.0))
447
+ - particleDensity(positionLife.xyz - vec3(uSPHCell.x, 0.0, 0.0)),
448
+ particleDensity(positionLife.xyz + vec3(0.0, uSPHCell.y, 0.0))
449
+ - particleDensity(positionLife.xyz - vec3(0.0, uSPHCell.y, 0.0)),
450
+ particleDensity(positionLife.xyz + vec3(0.0, 0.0, uSPHCell.z))
451
+ - particleDensity(positionLife.xyz - vec3(0.0, 0.0, uSPHCell.z))
452
+ );
453
+ float surfaceGradientLength = length(surfaceGradient);
454
+ float freeSurface = smoothstep(0.22, uSPHRestDensity, particleDensityCenter)
455
+ * (1.0 - smoothstep(uSPHRestDensity * 1.8, uSPHRestDensity * 4.2, particleDensityCenter));
456
+ if (surfaceGradientLength > 0.0001) {
457
+ velocity += surfaceGradient / surfaceGradientLength
458
+ * uSPHSurfaceTension * (storedParticle ? 0.42 : 1.0) * freeSurface * uDelta;
459
+ }
460
+
461
+ // A clean free jet conserves axial momentum along its own direction until it touches water or a wall.
462
+ if (!storedParticle && velocityImpact.w < 0.045 && positionLife.y > uBasin.w + 0.045) {
463
+ velocity += jetAxis * max(0.0, uSpeed * 0.96 - dot(velocity, jetAxis));
464
+ }
465
+
466
+ // Grid pressure is deliberately coarse, so deep retained particles need
467
+ // hydrostatic support to preserve volume. The free-surface band keeps
468
+ // substantially more gravity for natural wave falloff and overtopping.
469
+ if (storedParticle) {
470
+ float columnHeight = max(uBasin.w - uContainerFloorY, 0.04);
471
+ float normalizedDepth = clamp(
472
+ (uBasin.w - positionLife.y) / columnHeight,
473
+ 0.0,
474
+ 1.0
475
+ );
476
+ float hydrostaticSupport = mix(
477
+ 0.72,
478
+ 0.995,
479
+ smoothstep(0.045, 0.27, normalizedDepth)
480
+ );
481
+ float belowTargetSurface = 1.0 - smoothstep(
482
+ 0.0,
483
+ uSPHKernel * 0.8,
484
+ positionLife.y - uBasin.w
485
+ );
486
+ velocity.y += 9.81 * hydrostaticSupport * belowTargetSurface * uDelta;
487
+ velocity.x -= uContainerAcceleration.x * uDelta;
488
+ velocity.z -= uContainerAcceleration.y * uDelta;
489
+ }
490
+ velocity.y -= 9.81 * uDelta;
491
+ velocity *= exp(-(storedParticle ? 0.055 : 0.028) * uDelta);
492
+
493
+ if (!storedParticle) {
494
+ float diameter = 0.48 * uAperture;
495
+ float breakup = smoothstep(uNozzle.x + diameter * 4.0, uNozzle.x + diameter * 11.0, positionLife.x);
496
+ float swirlPhase = positionLife.x * 8.1 + uTime * 3.0 + seed * 28.0;
497
+ vec3 curlForce = vec3(0.0, sin(swirlPhase), cos(swirlPhase * 0.87 + 1.4));
498
+ velocity += curlForce * uTurbulence * breakup * uDelta;
499
+ }
500
+ positionLife.xyz += velocity * uDelta;
501
+
502
+ bool insideTray = positionLife.x > uBasin.x && positionLife.x < uBasin.y && abs(positionLife.z - uBasinCenterZ) < uBasin.z;
503
+ bool crossedWater = insideTray && previousPosition.y > uBasin.w && positionLife.y <= uBasin.w && velocity.y < 0.0;
504
+
505
+ if (uCollision > 0.5) {
506
+ if (crossedWater && !storedParticle) {
507
+ float impact = -velocity.y;
508
+ vec2 scatter = hash22(vUv * 817.0 + floor(uTime * 37.0)) * 2.0 - 1.0;
509
+ float splashChance = hash12(vUv * 331.0 + floor(uTime * 19.0));
510
+ if (splashChance < 0.31 + clamp(impact * 0.035, 0.0, 0.16)) {
511
+ positionLife.y = uBasin.w + 0.012;
512
+ velocity.x = velocity.x * (0.16 + seed * 0.12) + scatter.x * impact * 0.28;
513
+ velocity.y = impact * (0.18 + hash12(vUv * 97.0) * 0.25);
514
+ velocity.z = velocity.z * 0.2 + scatter.y * impact * 0.34;
515
+ velocityImpact.w = clamp(impact / 6.0, 0.25, 1.0);
516
+ } else {
517
+ positionLife.w = maxLife + 1.0;
518
+ }
519
+ }
520
+
521
+ bool insideZSpan = abs(positionLife.z - uBasinCenterZ) < uBasin.z + uRimWidth;
522
+ bool insideXSpan = positionLife.x > uBasin.x - uRimWidth && positionLife.x < uBasin.y + uRimWidth;
523
+ bool crossedMinWall = (previousPosition.x < uBasin.x && positionLife.x >= uBasin.x)
524
+ || (previousPosition.x > uBasin.x && positionLife.x <= uBasin.x);
525
+ bool crossedMaxWall = (previousPosition.x < uBasin.y && positionLife.x >= uBasin.y)
526
+ || (previousPosition.x > uBasin.y && positionLife.x <= uBasin.y);
527
+ bool crossedNearZ = (previousPosition.z < uBasinCenterZ - uBasin.z && positionLife.z >= uBasinCenterZ - uBasin.z)
528
+ || (previousPosition.z > uBasinCenterZ - uBasin.z && positionLife.z <= uBasinCenterZ - uBasin.z);
529
+ bool crossedFarZ = (previousPosition.z < uBasinCenterZ + uBasin.z && positionLife.z >= uBasinCenterZ + uBasin.z)
530
+ || (previousPosition.z > uBasinCenterZ + uBasin.z && positionLife.z <= uBasinCenterZ + uBasin.z);
531
+ bool wallHit = positionLife.y < uWallTop && (
532
+ (insideZSpan && (crossedMinWall || crossedMaxWall))
533
+ || (insideXSpan && (crossedNearZ || crossedFarZ))
534
+ );
535
+
536
+ bool outsideOpening = positionLife.x <= uBasin.x || positionLife.x >= uBasin.y
537
+ || abs(positionLife.z - uBasinCenterZ) >= uBasin.z;
538
+ bool crossedRimTop = previousPosition.y > uWallTop && positionLife.y <= uWallTop
539
+ && outsideOpening && insideXSpan && insideZSpan;
540
+
541
+ if ((wallHit || crossedRimTop) && !crossedWater) {
542
+ vec3 collisionNormal = vec3(0.0, 1.0, 0.0);
543
+ vec2 scatter = hash22(vUv * 527.0 + floor(uTime * 43.0)) * 2.0 - 1.0;
544
+ if (crossedRimTop) {
545
+ positionLife.y = uWallTop + 0.008;
546
+ } else if (crossedMinWall || crossedMaxWall) {
547
+ float wallPlane = crossedMinWall ? uBasin.x : uBasin.y;
548
+ float wallSign = crossedMinWall
549
+ ? (previousPosition.x < uBasin.x ? -1.0 : 1.0)
550
+ : (previousPosition.x > uBasin.y ? 1.0 : -1.0);
551
+ collisionNormal = vec3(wallSign, 0.0, 0.0);
552
+ positionLife.x = wallPlane + wallSign * uRimWidth;
553
+ } else {
554
+ float wallPlane = crossedNearZ ? uBasinCenterZ - uBasin.z : uBasinCenterZ + uBasin.z;
555
+ float wallSign = crossedNearZ
556
+ ? (previousPosition.z < wallPlane ? -1.0 : 1.0)
557
+ : (previousPosition.z > wallPlane ? 1.0 : -1.0);
558
+ collisionNormal = vec3(0.0, 0.0, wallSign);
559
+ positionLife.z = wallPlane + wallSign * uRimWidth;
560
+ }
561
+
562
+ float normalVelocity = dot(velocity, collisionNormal);
563
+ float impact = max(0.0, -normalVelocity);
564
+ float restitution = mix(0.12, 0.23, seed);
565
+ if (normalVelocity < 0.0) {
566
+ velocity -= (1.0 + restitution) * normalVelocity * collisionNormal;
567
+ }
568
+ vec3 tangentVelocity = velocity - dot(velocity, collisionNormal) * collisionNormal;
569
+ float collisionFriction = storedParticle
570
+ ? mix(0.015, 0.04, compression)
571
+ : mix(0.16, 0.28, compression);
572
+ velocity -= tangentVelocity * collisionFriction;
573
+ float roughSplash = smoothstep(1.2, 5.5, impact);
574
+ velocity += vec3(scatter.x, 0.18 + seed * 0.18, scatter.y)
575
+ * impact * roughSplash * 0.11;
576
+ velocityImpact.w = storedParticle
577
+ ? -1.0
578
+ : clamp(max(impact, pressureMagnitude * 0.16) / 6.5, 0.18, 1.0);
579
+ }
580
+
581
+ bool crossedFloor = previousPosition.y > uFloorY && positionLife.y <= uFloorY && velocity.y < 0.0;
582
+ if (crossedFloor && !crossedWater) {
583
+ if (storedParticle) {
584
+ positionLife.y = uFloorY + max(0.018, uSPHKernel * 0.24);
585
+ float impact = -velocity.y;
586
+ velocity.y = impact * 0.12;
587
+ velocity.x *= 0.97;
588
+ velocity.z *= 0.97;
589
+ velocityImpact.w = -1.0;
590
+ } else if (velocityImpact.w > 0.12) {
591
+ positionLife.w = maxLife + 1.0;
592
+ } else {
593
+ float impact = -velocity.y;
594
+ vec2 scatter = hash22(vUv * 691.0 + floor(uTime * 31.0)) * 2.0 - 1.0;
595
+ positionLife.y = uFloorY + 0.009;
596
+ velocity.x = velocity.x * 0.12 + scatter.x * impact * 0.38;
597
+ velocity.y = impact * (0.18 + hash12(vUv * 229.0) * 0.22);
598
+ velocity.z = velocity.z * 0.12 + scatter.y * impact * 0.42;
599
+ velocityImpact.w = clamp(impact / 5.8, 0.35, 1.0);
600
+ }
601
+ }
602
+
603
+ if (storedParticle) {
604
+ float containmentInset = max(0.018, uSPHKernel * 0.24);
605
+ float minX = uBasin.x + containmentInset;
606
+ float maxX = uBasin.y - containmentInset;
607
+ float minZ = uBasinCenterZ - uBasin.z + containmentInset;
608
+ float maxZ = uBasinCenterZ + uBasin.z - containmentInset;
609
+ float bottom = uContainerFloorY + containmentInset;
610
+
611
+ bool retainedBelowOverflow = uReservoirFill < 0.985;
612
+ if (retainedBelowOverflow || positionLife.y < uWallTop + 0.075) {
613
+ if (positionLife.x < minX) {
614
+ positionLife.x = minX;
615
+ velocity.x = abs(velocity.x) * 0.28;
616
+ velocity.z *= 0.97;
617
+ } else if (positionLife.x > maxX) {
618
+ positionLife.x = maxX;
619
+ velocity.x = -abs(velocity.x) * 0.28;
620
+ velocity.z *= 0.97;
621
+ }
622
+ if (positionLife.z < minZ) {
623
+ positionLife.z = minZ;
624
+ velocity.z = abs(velocity.z) * 0.28;
625
+ velocity.x *= 0.97;
626
+ } else if (positionLife.z > maxZ) {
627
+ positionLife.z = maxZ;
628
+ velocity.z = -abs(velocity.z) * 0.28;
629
+ velocity.x *= 0.97;
630
+ }
631
+ }
632
+ if (positionLife.y < bottom) {
633
+ positionLife.y = bottom;
634
+ velocity.y = abs(velocity.y) * 0.06;
635
+ velocity.xz *= 0.97;
636
+ }
637
+
638
+ // Retained water may only leave through the rim after the vessel is physically full.
639
+ // At lower levels, pressure impulses become internal slosh instead of phantom overflow.
640
+ float retainedCeiling = mix(
641
+ uBasin.w + 0.065,
642
+ uWallTop + 0.085,
643
+ smoothstep(0.96, 1.0, uReservoirFill)
644
+ );
645
+ if (uReservoirFill > 0.985
646
+ && positionLife.y > uWallTop + 0.12
647
+ && velocity.y > 1.35
648
+ && length(velocity) > 1.75) {
649
+ storedParticle = false;
650
+ positionLife.w = maxLife - 0.72;
651
+ velocityImpact.w = clamp(length(velocity) / 5.5, 0.18, 0.72);
652
+ } else {
653
+ if (positionLife.y > retainedCeiling) {
654
+ positionLife.y = retainedCeiling;
655
+ velocity.y = min(velocity.y, -0.08);
656
+ velocity.xz *= 0.97;
657
+ }
658
+ velocityImpact.w = -1.0;
659
+ }
660
+ }
661
+ }
662
+
663
+ velocityImpact.xyz = velocity;
664
+ bool expired = !storedParticle && positionLife.w > maxLife;
665
+ bool escaped = !storedParticle && (positionLife.y < uFloorY - 0.36
666
+ || positionLife.x > uBasin.y + 2.8
667
+ || positionLife.x < uNozzle.x - 0.8
668
+ || abs(positionLife.z - uBasinCenterZ) > uBasin.z + 2.2);
669
+ if (expired || escaped) {
670
+ positionLife = vec4(uNozzle, uEmission > 0.5 ? -seed * 0.85 : -10.0 - seed);
671
+ velocityImpact = vec4(0.0);
672
+ }
673
+
674
+ outPosition = positionLife;
675
+ outVelocity = velocityImpact;
676
+ }
677
+ `,
678
+ depthTest: false,
679
+ depthWrite: false,
680
+ toneMapped: false,
681
+ });
682
+
683
+ this.updateSPHDomain();
684
+ this.reset();
685
+ }
686
+
687
+ hash(value: number): number {
688
+ const x = Math.sin(value * 12.9898) * 43758.5453;
689
+ return x - Math.floor(x);
690
+ }
691
+
692
+ reset(): void {
693
+ if (!this.supported) return;
694
+ const previousTarget = this.renderer.getRenderTarget();
695
+ this.mesh.material = this.initMaterial;
696
+ this.renderer.setRenderTarget(this.read);
697
+ this.renderer.render(this.scene, this.camera);
698
+ this.renderer.setRenderTarget(this.write);
699
+ this.renderer.render(this.scene, this.camera);
700
+ this.renderer.setRenderTarget(previousTarget);
701
+ }
702
+
703
+ update(delta: number, elapsed: number): void {
704
+ if (!this.supported) return;
705
+ this.sphField.build(this.read.textures[0], this.read.textures[1]);
706
+ this.updateMaterial.uniforms.uPosition.value = this.read.textures[0];
707
+ this.updateMaterial.uniforms.uVelocity.value = this.read.textures[1];
708
+ this.updateMaterial.uniforms.uDelta.value = delta;
709
+ this.updateMaterial.uniforms.uTime.value = elapsed;
710
+ this.mesh.material = this.updateMaterial;
711
+
712
+ const previousTarget = this.renderer.getRenderTarget();
713
+ this.renderer.setRenderTarget(this.write);
714
+ this.renderer.render(this.scene, this.camera);
715
+ this.renderer.setRenderTarget(previousTarget);
716
+ [this.read, this.write] = [this.write, this.read];
717
+ }
718
+
719
+ setParameters({ speed, aperture, fillRatio, emitting, turbulence }: FluidParameters): void {
720
+ if (!this.supported) return;
721
+ if (speed !== undefined) this.updateMaterial.uniforms.uSpeed.value = speed;
722
+ if (aperture !== undefined) this.updateMaterial.uniforms.uAperture.value = aperture;
723
+ if (fillRatio !== undefined) this.updateMaterial.uniforms.uFillRatio.value = fillRatio;
724
+ if (emitting !== undefined) this.updateMaterial.uniforms.uEmission.value = emitting ? 1 : 0;
725
+ if (turbulence !== undefined) this.updateMaterial.uniforms.uTurbulence.value = turbulence;
726
+ }
727
+
728
+ setReservoirState({ active = false, fillRatio = 0 }: ReservoirState = {}): void {
729
+ if (!this.supported) return;
730
+ this.updateMaterial.uniforms.uReservoirActive.value = active ? 1 : 0;
731
+ this.updateMaterial.uniforms.uReservoirFill.value = THREE.MathUtils.clamp(fillRatio, 0, 1);
732
+ }
733
+
734
+ setContainerMotion(accelerationX = 0, accelerationZ = 0): void {
735
+ if (!this.supported) return;
736
+ this.updateMaterial.uniforms.uContainerAcceleration.value.set(
737
+ THREE.MathUtils.clamp(accelerationX, -22, 22),
738
+ THREE.MathUtils.clamp(accelerationZ, -22, 22),
739
+ );
740
+ }
741
+
742
+ setBasin({
743
+ minX,
744
+ maxX,
745
+ halfZ,
746
+ centerZ = 0,
747
+ waterY,
748
+ wallTop,
749
+ floorY = -0.17,
750
+ containerFloorY = 0.06,
751
+ rimWidth = 0.055,
752
+ }: BasinConfig): void {
753
+ if (!this.supported) return;
754
+ this.basin.set(minX, maxX, halfZ, waterY);
755
+ this.updateMaterial.uniforms.uBasinCenterZ.value = centerZ;
756
+ this.updateMaterial.uniforms.uWallTop.value = wallTop;
757
+ this.updateMaterial.uniforms.uFloorY.value = floorY;
758
+ this.updateMaterial.uniforms.uContainerFloorY.value = containerFloorY;
759
+ this.updateMaterial.uniforms.uRimWidth.value = rimWidth;
760
+ this.updateSPHDomain();
761
+ }
762
+
763
+ updateSPHDomain(): void {
764
+ if (!this.supported) return;
765
+ const centerZ = this.updateMaterial.uniforms.uBasinCenterZ.value;
766
+ const floorY = this.updateMaterial.uniforms.uFloorY.value;
767
+ const marginZ = this.basin.z + 0.72;
768
+ this.sphDomainMinimum.set(
769
+ this.nozzle.x - 0.34,
770
+ floorY - 0.16,
771
+ Math.min(-1.45, centerZ - marginZ),
772
+ );
773
+ this.sphDomainMaximum.set(
774
+ Math.max(this.nozzle.x + 3.9, this.basin.y + 0.82),
775
+ this.nozzle.y + 0.38,
776
+ Math.max(1.45, centerZ + marginZ),
777
+ );
778
+ this.sphField.setDomain(this.sphDomainMinimum, this.sphDomainMaximum);
779
+ }
780
+
781
+ /** 运行时改喷流起点(喷嘴)坐标;会重算 SPH 域,已发射粒子在下一次 respawn 时回到新起点 */
782
+ setNozzle(x: number, y: number, z: number): void {
783
+ if (!this.supported) return;
784
+ this.nozzle.set(x, y, z);
785
+ this.updateMaterial.uniforms.uNozzle.value.set(x, y, z);
786
+ this.updateSPHDomain();
787
+ }
788
+
789
+ /** 运行时改喷流方向(轴向,会被归一化) */
790
+ setJetDirection(dx: number, dy: number, dz: number): void {
791
+ if (!this.supported) return;
792
+ this.jetDirection.set(dx, dy, dz).normalize();
793
+ this.updateMaterial.uniforms.uJetDir.value.copy(this.jetDirection);
794
+ }
795
+
796
+ /** 运行时开关粒子与 basin/地面碰撞回弹(默认开启) */
797
+ setCollision(enabled: boolean): void {
798
+ if (!this.supported) return;
799
+ this.collision = enabled ? 1 : 0;
800
+ this.updateMaterial.uniforms.uCollision.value = this.collision;
801
+ }
802
+
803
+ setSPHActiveCount(count: number): void {
804
+ if (!this.supported) return;
805
+ this.sphField.setActiveCount(count);
806
+ }
807
+
808
+ get positionTexture(): THREE.Texture | null {
809
+ return this.supported ? this.read.textures[0] : null;
810
+ }
811
+
812
+ get velocityTexture(): THREE.Texture | null {
813
+ return this.supported ? this.read.textures[1] : null;
814
+ }
815
+
816
+ dispose(): void {
817
+ if (!this.supported) return;
818
+ this.read.dispose();
819
+ this.write.dispose();
820
+ this.initialPosition.dispose();
821
+ this.initialVelocity.dispose();
822
+ this.sphField.dispose();
823
+ this.initMaterial.dispose();
824
+ this.updateMaterial.dispose();
825
+ this.mesh.geometry.dispose();
826
+ }
827
+ }