|
35 | 35 | <script type="module"> |
36 | 36 |
|
37 | 37 | import * as THREE from 'three/webgpu'; |
38 | | - import { instanceIndex, struct, If, uint, int, floor, float, length, clamp, vec2, cos, vec3, vertexIndex, Fn, uniform, instancedArray, min, max, positionLocal, transformNormalToView, globalId } from 'three/tsl'; |
| 38 | + import { instanceIndex, struct, If, uint, int, floor, float, length, clamp, vec2, cos, vec3, vertexIndex, Fn, uniform, instancedArray, min, max, positionLocal, transformNormalToView, select, globalId } from 'three/tsl'; |
39 | 39 |
|
40 | 40 | import { Inspector } from 'three/addons/inspector/Inspector.js'; |
41 | 41 |
|
|
83 | 83 | let waterMesh; |
84 | 84 | let poolBorder; |
85 | 85 | let meshRay; |
86 | | - let computeHeight, computeDucks; |
| 86 | + let computeHeightAtoB, computeHeightBtoA, computeDucks; |
| 87 | + let pingPong = 0; |
| 88 | + const readFromA = uniform( 1 ); |
87 | 89 | let duckModel = null; |
88 | 90 |
|
89 | 91 | const NUM_DUCKS = 100; |
|
159 | 161 |
|
160 | 162 | } |
161 | 163 |
|
162 | | - const heightStorage = instancedArray( heightArray ).setName( 'Height' ); |
| 164 | + // Ping-pong height storage buffers |
| 165 | + const heightStorageA = instancedArray( heightArray ).setName( 'HeightA' ); |
| 166 | + const heightStorageB = instancedArray( new Float32Array( heightArray ) ).setName( 'HeightB' ); |
163 | 167 | const prevHeightStorage = instancedArray( prevHeightArray ).setName( 'PrevHeight' ); |
164 | 168 |
|
165 | 169 | // Get Indices of Neighbor Values of an Index in the Simulation Grid |
|
209 | 213 |
|
210 | 214 | }; |
211 | 215 |
|
212 | | - // Get new normals of simulation area. |
213 | | - const getNormalsFromHeightTSL = ( index, store ) => { |
214 | | - |
215 | | - const { north, south, east, west } = getNeighborValuesTSL( index, store ); |
216 | | - |
217 | | - const normalX = ( west.sub( east ) ).mul( WIDTH / BOUNDS ); |
218 | | - const normalY = ( south.sub( north ) ).mul( WIDTH / BOUNDS ); |
219 | | - |
220 | | - return { normalX, normalY }; |
221 | | - |
222 | | - }; |
223 | | - |
224 | | - computeHeight = Fn( () => { |
| 216 | + // Create compute shader for height simulation with explicit read/write buffers |
| 217 | + const createComputeHeight = ( readBuffer, writeBuffer ) => Fn( () => { |
225 | 218 |
|
226 | 219 | const { viscosity, mousePos, mouseSize, mouseDeep, mouseSpeed } = effectController; |
227 | 220 |
|
228 | | - const height = heightStorage.element( instanceIndex ).toVar(); |
| 221 | + const height = readBuffer.element( instanceIndex ).toVar(); |
229 | 222 | const prevHeight = prevHeightStorage.element( instanceIndex ).toVar(); |
230 | 223 |
|
231 | | - const { north, south, east, west } = getNeighborValuesTSL( instanceIndex, heightStorage ); |
| 224 | + const { north, south, east, west } = getNeighborValuesTSL( instanceIndex, readBuffer ); |
232 | 225 |
|
233 | 226 | const neighborHeight = north.add( south ).add( east ).add( west ); |
234 | 227 | neighborHeight.mulAssign( 0.5 ); |
|
251 | 244 | newHeight.addAssign( cos( mousePhase ).add( 1.0 ).mul( mouseDeep ).mul( mouseSpeed.length() ) ); |
252 | 245 |
|
253 | 246 | prevHeightStorage.element( instanceIndex ).assign( height ); |
254 | | - heightStorage.element( instanceIndex ).assign( newHeight ); |
| 247 | + writeBuffer.element( instanceIndex ).assign( newHeight ); |
255 | 248 |
|
256 | | - } )().compute( WIDTH * WIDTH, [ 16, 16 ] ).setName( 'Update Height' ); |
| 249 | + } )().compute( WIDTH * WIDTH, [ 16, 16 ] ); |
| 250 | + |
| 251 | + // Create both ping-pong compute shaders |
| 252 | + computeHeightAtoB = createComputeHeight( heightStorageA, heightStorageB ).setName( 'Update Height A→B' ); |
| 253 | + computeHeightBtoA = createComputeHeight( heightStorageB, heightStorageA ).setName( 'Update Height B→A' ); |
257 | 254 |
|
258 | 255 | // Water Geometry corresponds with buffered compute grid. |
259 | 256 | const waterGeometry = new THREE.PlaneGeometry( BOUNDS, BOUNDS, WIDTH - 1, WIDTH - 1 ); |
|
267 | 264 | side: THREE.DoubleSide |
268 | 265 | } ); |
269 | 266 |
|
| 267 | + // Helper to get height from the current read buffer |
| 268 | + const getCurrentHeight = ( index ) => { |
| 269 | + |
| 270 | + return select( readFromA, heightStorageA.element( index ), heightStorageB.element( index ) ); |
| 271 | + |
| 272 | + }; |
| 273 | + |
| 274 | + // Helper to get normals from the current read buffer |
| 275 | + const getCurrentNormals = ( index ) => { |
| 276 | + |
| 277 | + const { northIndex, southIndex, eastIndex, westIndex } = getNeighborIndicesTSL( index ); |
| 278 | + |
| 279 | + const north = getCurrentHeight( northIndex ); |
| 280 | + const south = getCurrentHeight( southIndex ); |
| 281 | + const east = getCurrentHeight( eastIndex ); |
| 282 | + const west = getCurrentHeight( westIndex ); |
| 283 | + |
| 284 | + const normalX = ( west.sub( east ) ).mul( WIDTH / BOUNDS ); |
| 285 | + const normalY = ( south.sub( north ) ).mul( WIDTH / BOUNDS ); |
| 286 | + |
| 287 | + return { normalX, normalY }; |
| 288 | + |
| 289 | + }; |
| 290 | + |
270 | 291 | waterMaterial.normalNode = Fn( () => { |
271 | 292 |
|
272 | 293 | // To correct the lighting as our mesh undulates, we have to reassign the normals in the normal shader. |
273 | | - const { normalX, normalY } = getNormalsFromHeightTSL( vertexIndex, heightStorage ); |
| 294 | + const { normalX, normalY } = getCurrentNormals( vertexIndex ); |
274 | 295 |
|
275 | 296 | return transformNormalToView( vec3( normalX, normalY.negate(), 1.0 ) ).toVertexStage(); |
276 | 297 |
|
277 | 298 | } )(); |
278 | 299 |
|
279 | 300 | waterMaterial.positionNode = Fn( () => { |
280 | 301 |
|
281 | | - return vec3( positionLocal.x, positionLocal.y, heightStorage.element( vertexIndex ) ); |
| 302 | + return vec3( positionLocal.x, positionLocal.y, getCurrentHeight( vertexIndex ) ); |
282 | 303 |
|
283 | 304 | } )(); |
284 | 305 |
|
|
351 | 372 | const zCoord = uint( clamp( floor( gridCoordZ ), 0, WIDTH - 1 ) ); |
352 | 373 | const heightInstanceIndex = zCoord.mul( WIDTH ).add( xCoord ); |
353 | 374 |
|
354 | | - // Get height of water at the duck's position |
355 | | - const waterHeight = heightStorage.element( heightInstanceIndex ); |
356 | | - const { normalX, normalY } = getNormalsFromHeightTSL( heightInstanceIndex, heightStorage ); |
| 375 | + // Get height of water at the duck's position (use current read buffer) |
| 376 | + const waterHeight = getCurrentHeight( heightInstanceIndex ); |
| 377 | + const { normalX, normalY } = getCurrentNormals( heightInstanceIndex ); |
357 | 378 |
|
358 | 379 | // Calculate the target Y position based on the water height and the duck's vertical offset |
359 | 380 | const targetY = waterHeight.add( yOffset ); |
|
449 | 470 |
|
450 | 471 | controls = new OrbitControls( camera, container ); |
451 | 472 |
|
452 | | - container.style.touchAction = 'none'; |
453 | | - |
454 | 473 | // |
455 | 474 |
|
456 | 475 | container.style.touchAction = 'none'; |
|
586 | 605 |
|
587 | 606 | if ( frame >= 7 - effectController.speed ) { |
588 | 607 |
|
589 | | - renderer.compute( computeHeight, [ 8, 8, 1 ] ); |
| 608 | + // Ping-pong: alternate which buffer we read from and write to |
| 609 | + if ( pingPong === 0 ) { |
| 610 | + |
| 611 | + renderer.compute( computeHeightAtoB, [ 8, 8, 1 ] ); |
| 612 | + readFromA.value = 0; // Material now reads from B (just written) |
| 613 | + |
| 614 | + } else { |
| 615 | + |
| 616 | + renderer.compute( computeHeightBtoA, [ 8, 8, 1 ] ); |
| 617 | + readFromA.value = 1; // Material now reads from A (just written) |
| 618 | + |
| 619 | + } |
| 620 | + |
| 621 | + pingPong = 1 - pingPong; |
590 | 622 |
|
591 | 623 | if ( effectController.ducksEnabled ) { |
592 | 624 |
|
|
0 commit comments