Skip to content

Commit 7505b21

Browse files
authored
Examples: Fix WebGPU water simulation on mobile GPUs (#32411)
1 parent 82c64b0 commit 7505b21

File tree

1 file changed

+60
-28
lines changed

1 file changed

+60
-28
lines changed

examples/webgpu_compute_water.html

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
<script type="module">
3636

3737
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';
3939

4040
import { Inspector } from 'three/addons/inspector/Inspector.js';
4141

@@ -83,7 +83,9 @@
8383
let waterMesh;
8484
let poolBorder;
8585
let meshRay;
86-
let computeHeight, computeDucks;
86+
let computeHeightAtoB, computeHeightBtoA, computeDucks;
87+
let pingPong = 0;
88+
const readFromA = uniform( 1 );
8789
let duckModel = null;
8890

8991
const NUM_DUCKS = 100;
@@ -159,7 +161,9 @@
159161

160162
}
161163

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' );
163167
const prevHeightStorage = instancedArray( prevHeightArray ).setName( 'PrevHeight' );
164168

165169
// Get Indices of Neighbor Values of an Index in the Simulation Grid
@@ -209,26 +213,15 @@
209213

210214
};
211215

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( () => {
225218

226219
const { viscosity, mousePos, mouseSize, mouseDeep, mouseSpeed } = effectController;
227220

228-
const height = heightStorage.element( instanceIndex ).toVar();
221+
const height = readBuffer.element( instanceIndex ).toVar();
229222
const prevHeight = prevHeightStorage.element( instanceIndex ).toVar();
230223

231-
const { north, south, east, west } = getNeighborValuesTSL( instanceIndex, heightStorage );
224+
const { north, south, east, west } = getNeighborValuesTSL( instanceIndex, readBuffer );
232225

233226
const neighborHeight = north.add( south ).add( east ).add( west );
234227
neighborHeight.mulAssign( 0.5 );
@@ -251,9 +244,13 @@
251244
newHeight.addAssign( cos( mousePhase ).add( 1.0 ).mul( mouseDeep ).mul( mouseSpeed.length() ) );
252245

253246
prevHeightStorage.element( instanceIndex ).assign( height );
254-
heightStorage.element( instanceIndex ).assign( newHeight );
247+
writeBuffer.element( instanceIndex ).assign( newHeight );
255248

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' );
257254

258255
// Water Geometry corresponds with buffered compute grid.
259256
const waterGeometry = new THREE.PlaneGeometry( BOUNDS, BOUNDS, WIDTH - 1, WIDTH - 1 );
@@ -267,18 +264,42 @@
267264
side: THREE.DoubleSide
268265
} );
269266

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+
270291
waterMaterial.normalNode = Fn( () => {
271292

272293
// 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 );
274295

275296
return transformNormalToView( vec3( normalX, normalY.negate(), 1.0 ) ).toVertexStage();
276297

277298
} )();
278299

279300
waterMaterial.positionNode = Fn( () => {
280301

281-
return vec3( positionLocal.x, positionLocal.y, heightStorage.element( vertexIndex ) );
302+
return vec3( positionLocal.x, positionLocal.y, getCurrentHeight( vertexIndex ) );
282303

283304
} )();
284305

@@ -351,9 +372,9 @@
351372
const zCoord = uint( clamp( floor( gridCoordZ ), 0, WIDTH - 1 ) );
352373
const heightInstanceIndex = zCoord.mul( WIDTH ).add( xCoord );
353374

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 );
357378

358379
// Calculate the target Y position based on the water height and the duck's vertical offset
359380
const targetY = waterHeight.add( yOffset );
@@ -449,8 +470,6 @@
449470

450471
controls = new OrbitControls( camera, container );
451472

452-
container.style.touchAction = 'none';
453-
454473
//
455474

456475
container.style.touchAction = 'none';
@@ -586,7 +605,20 @@
586605

587606
if ( frame >= 7 - effectController.speed ) {
588607

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;
590622

591623
if ( effectController.ducksEnabled ) {
592624

0 commit comments

Comments
 (0)