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