diff --git a/examples/webgpu_compute_water.html b/examples/webgpu_compute_water.html
index da5b40946fb40f..bacb6690b1e2b7 100644
--- a/examples/webgpu_compute_water.html
+++ b/examples/webgpu_compute_water.html
@@ -31,28 +31,48 @@
import { color, instanceIndex, struct, If, varyingProperty, uint, int, negate, floor, float, length, clamp, vec2, cos, vec3, vertexIndex, Fn, uniform, instancedArray, min, max, positionLocal, transformNormalToView } from 'three/tsl';
import { SimplexNoise } from 'three/addons/math/SimplexNoise.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+ import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
+ import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import Stats from 'three/addons/libs/stats.module.js';
// Dimensions of simulation grid.
const WIDTH = 128;
// Water size in system units.
- const BOUNDS = 512;
+ const BOUNDS = 6;
const BOUNDS_HALF = BOUNDS * 0.5;
+ const limit = BOUNDS_HALF - 0.2;
- const waterMaxHeight = 10;
+ const waterMaxHeight = 0.1;
let container, stats;
- let camera, scene, renderer;
+ let camera, scene, renderer, controls;
let mouseMoved = false;
+ let mouseDown = false;
const mouseCoords = new THREE.Vector2();
const raycaster = new THREE.Raycaster();
- let effectController;
-
- let waterMesh, meshRay;
+ let frame = 0;
+
+ const effectController = {
+ mousePos: uniform( new THREE.Vector2( 10000, 10000 ) ).label( 'mousePos' ),
+ mouseDeep: uniform( 0.01 ).label( 'mouseDeep' ),
+ mouseSize: uniform( 0.2 ).label( 'mouseSize' ),
+ viscosity: uniform( 0.93 ).label( 'viscosity' ),
+ ducksEnabled: true,
+ wireframe: false,
+ speed: 5,
+ };
+
+ let sun;
+ let waterMesh;
+ let poolBorder;
+ let meshRay;
let computeHeight, computeSmooth, computeSphere;
+ let duckModel = null;
- const NUM_SPHERES = 100;
+ const NUM_DUCKS = 100;
const simplex = new SimplexNoise();
@@ -75,35 +95,23 @@
}
- function init() {
+ async function init() {
container = document.createElement( 'div' );
document.body.appendChild( container );
camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, 3000 );
- camera.position.set( 0, 200, 350 );
+ camera.position.set( 0, 2.00, 4 );
camera.lookAt( 0, 0, 0 );
scene = new THREE.Scene();
- const sun = new THREE.DirectionalLight( 0xFFFFFF, 3.0 );
- sun.position.set( 300, 400, 175 );
+ sun = new THREE.DirectionalLight( 0xFFFFFF, 4.0 );
+ sun.position.set( - 1, 2.6, 1.4 );
scene.add( sun );
- const sun2 = new THREE.DirectionalLight( 0x40A040, 2.0 );
- sun2.position.set( - 100, 350, - 200 );
- scene.add( sun2 );
-
//
- effectController = {
- mousePos: uniform( new THREE.Vector2( 10000, 10000 ) ).label( 'mousePos' ),
- mouseSize: uniform( 30.0 ).label( 'mouseSize' ),
- viscosity: uniform( 0.95 ).label( 'viscosity' ),
- spheresEnabled: true,
- wireframe: false
- };
-
// Initialize height storage buffers
const heightArray = new Float32Array( WIDTH * WIDTH );
const prevHeightArray = new Float32Array( WIDTH * WIDTH );
@@ -191,7 +199,7 @@
computeHeight = Fn( () => {
- const { viscosity, mousePos, mouseSize } = effectController;
+ const { viscosity, mousePos, mouseSize, mouseDeep } = effectController;
const height = heightStorage.element( instanceIndex ).toVar();
const prevHeight = prevHeightStorage.element( instanceIndex ).toVar();
@@ -213,7 +221,8 @@
// Get length of position in range [ -BOUNDS / 2, BOUNDS / 2 ], offset by mousePos, then scale.
const mousePhase = clamp( length( ( vec2( x, y ).sub( centerVec ) ).mul( BOUNDS ).sub( mousePos ) ).mul( Math.PI ).div( mouseSize ), 0.0, Math.PI );
- newHeight.addAssign( cos( mousePhase ).add( 1.0 ).mul( 0.28 ) );
+ // "Indent" water down by scaled distance from center of mouse impact
+ newHeight.subAssign( cos( mousePhase ).add( 1.0 ).mul( mouseDeep ) );
prevHeightStorage.element( instanceIndex ).assign( height );
heightStorage.element( instanceIndex ).assign( newHeight );
@@ -242,12 +251,16 @@
// Water Geometry corresponds with buffered compute grid.
const waterGeometry = new THREE.PlaneGeometry( BOUNDS, BOUNDS, WIDTH - 1, WIDTH - 1 );
// material: make a THREE.ShaderMaterial clone of THREE.MeshPhongMaterial, with customized position shader.
- const waterMaterial = new THREE.MeshPhongNodeMaterial();
+ const waterMaterial = new THREE.MeshStandardNodeMaterial( {
+ color: 0x9bd2ec,
+ metalness: 0.9,
+ roughness: 0,
+ transparent: true,
+ opacity: 0.8,
+ side: THREE.DoubleSide
+ } );
waterMaterial.lights = true;
- waterMaterial.colorNode = color( 0x0040C0 );
- waterMaterial.specularNode = color( 0x111111 );
- waterMaterial.shininess = Math.max( 50, 1e-4 );
waterMaterial.positionNode = Fn( () => {
// To correct the lighting as our mesh undulates, we have to reassign the normals in the position shader.
@@ -260,12 +273,23 @@
} )();
waterMesh = new THREE.Mesh( waterGeometry, waterMaterial );
- waterMesh.rotation.x = - Math.PI / 2;
+ waterMesh.rotation.x = - Math.PI * 0.5;
waterMesh.matrixAutoUpdate = false;
waterMesh.updateMatrix();
+ waterMesh.receiveShadow = true;
+ waterMesh.castShadow = true;
scene.add( waterMesh );
+ // Pool border
+ const borderGeom = new THREE.TorusGeometry( 4.2, 0.1, 12, 4 );
+ borderGeom.rotateX( Math.PI * 0.5 );
+ borderGeom.rotateY( Math.PI * 0.25 );
+ poolBorder = new THREE.Mesh( borderGeom, new THREE.MeshStandardMaterial( { color: 0x908877, roughness: 0.2 } ) );
+ scene.add( poolBorder );
+ borderGeom.receiveShadow = true;
+ borderGeom.castShadow = true;
+
// THREE.Mesh just for mouse raycasting
const geometryRay = new THREE.PlaneGeometry( BOUNDS, BOUNDS, 1, 1 );
meshRay = new THREE.Mesh( geometryRay, new THREE.MeshBasicMaterial( { color: 0xFFFFFF, visible: false } ) );
@@ -273,55 +297,53 @@
meshRay.matrixAutoUpdate = false;
meshRay.updateMatrix();
scene.add( meshRay );
-
- // Create sphere THREE.InstancedMesh
- const sphereGeometry = new THREE.SphereGeometry( 4, 24, 12 );
- const sphereMaterial = new THREE.MeshPhongMaterial( { color: 0xFFFF00 } );
// Initialize sphere mesh instance position and velocity.
// position + velocity + unused = 8 floats per sphere.
// for structs arrays must be enclosed in multiple of 4
- const sphereStride = 8;
- const sphereArray = new Float32Array( NUM_SPHERES * sphereStride );
+ const duckStride = 8;
+ const duckInstanceDataArray = new Float32Array( NUM_DUCKS * duckStride );
// Only hold velocity in x and z directions.
// The sphere is wedded to the surface of the water, and will only move vertically with the water.
- for ( let i = 0; i < NUM_SPHERES; i ++ ) {
+ for ( let i = 0; i < NUM_DUCKS; i ++ ) {
- sphereArray[ i * sphereStride + 0 ] = ( Math.random() - 0.5 ) * BOUNDS * 0.7;
- sphereArray[ i * sphereStride + 1 ] = 0;
- sphereArray[ i * sphereStride + 2 ] = ( Math.random() - 0.5 ) * BOUNDS * 0.7;
+ duckInstanceDataArray[ i * duckStride + 0 ] = ( Math.random() - 0.5 ) * BOUNDS * 0.7;
+ duckInstanceDataArray[ i * duckStride + 1 ] = 0;
+ duckInstanceDataArray[ i * duckStride + 2 ] = ( Math.random() - 0.5 ) * BOUNDS * 0.7;
}
- const SphereStruct = struct( {
+ const DuckStruct = struct( {
position: 'vec3',
velocity: 'vec2'
} );
// Sphere Instance Storage
- const sphereVelocityStorage = instancedArray( sphereArray, SphereStruct ).label( 'SphereData' );
+ const duckInstanceDataStorage = instancedArray( duckInstanceDataArray, DuckStruct ).label( 'DuckInstanceData' );
computeSphere = Fn( () => {
- const instancePosition = sphereVelocityStorage.element( instanceIndex ).get( 'position' );
- const velocity = sphereVelocityStorage.element( instanceIndex ).get( 'velocity' );
+ const instancePosition = duckInstanceDataStorage.element( instanceIndex ).get( 'position' );
+ const velocity = duckInstanceDataStorage.element( instanceIndex ).get( 'velocity' );
// Bring position from range of [ -BOUNDS/2, BOUNDS/2 ] to [ 0, BOUNDS ]
- const tempX = instancePosition.x.add( BOUNDS_HALF );
- const tempZ = instancePosition.z.add( BOUNDS_HALF );
- // Bring position from range [ 0, BOUNDS ] to [ 0, WIDTH ]
- // ( i.e bring geometry range into 'heightmap' range )
- // WIDTH = 128, BOUNDS = 512... same as dividing by 4
- tempX.mulAssign( WIDTH / BOUNDS );
- tempZ.mulAssign( WIDTH / BOUNDS );
+ // Bring position from range [ -BOUNDS/2, BOUNDS/2 ] to [ 0, WIDTH ]
+
+ const tempX = instancePosition.x.mul( 0.5 ).div( BOUNDS_HALF );
+ tempX.addAssign( 0.5 );
+ const newX = tempX.mul( WIDTH );
+
+ const tempZ = instancePosition.z.mul( 0.5 ).div( BOUNDS_HALF );
+ tempZ.addAssign( 0.5 );
+ const newZ = tempZ.mul( WIDTH );
// Can only access storage buffers with uints
- const xCoord = uint( floor( tempX ) );
- const zCoord = uint( floor( tempZ ) );
+ const xCoord = uint( floor( newX ) );
+ const zCoord = uint( floor( newZ ) );
// Get one dimensional index
const heightInstanceIndex = zCoord.mul( WIDTH ).add( xCoord );
@@ -345,27 +367,29 @@
const newPosition = instancePosition.add( newVelocity ).toVar();
+ const decal = float( 0.001 ).toVar( 'decal' );
+
// Reverse velocity and reset position when exceeding bounds.
- If( newPosition.x.lessThan( - BOUNDS_HALF ), () => {
+ If( newPosition.x.lessThan( - limit ), () => {
- newPosition.x = float( - BOUNDS_HALF ).add( 0.001 );
+ newPosition.x = float( - limit ).add( decal );
newVelocity.x.mulAssign( - 0.3 );
- } ).ElseIf( newPosition.x.greaterThan( BOUNDS_HALF ), () => {
+ } ).ElseIf( newPosition.x.greaterThan( limit ), () => {
- newPosition.x = float( BOUNDS_HALF ).sub( 0.001 );
+ newPosition.x = float( limit ).sub( decal );
newVelocity.x.mulAssign( - 0.3 );
} );
- If( newPosition.z.lessThan( - BOUNDS_HALF ), () => {
+ If( newPosition.z.lessThan( - limit ), () => {
- newPosition.z = float( - BOUNDS_HALF ).add( 0.001 );
+ newPosition.z = float( - limit ).add( decal );
newVelocity.z.mulAssign( - 0.3 );
- } ).ElseIf( newPosition.z.greaterThan( BOUNDS_HALF ), () => {
+ } ).ElseIf( newPosition.z.greaterThan( limit ), () => {
- newPosition.z = float( BOUNDS_HALF ).sub( 0.001 );
+ newPosition.z = float( limit ).sub( decal );
newVelocity.z.mulAssign( - 0.3 );
} );
@@ -373,37 +397,61 @@
instancePosition.assign( newPosition );
velocity.assign( vec2( newVelocity.x, newVelocity.z ) );
- } )().compute( NUM_SPHERES );
+ } )().compute( NUM_DUCKS );
+
+ const rgbeLoader = new RGBELoader().setPath( './textures/equirectangular/' );
+ const glbloader = new GLTFLoader().setPath( 'models/gltf/' );
+ glbloader.setDRACOLoader( new DRACOLoader().setDecoderPath( 'jsm/libs/draco/gltf/' ) );
+
+ const [ env, model ] = await Promise.all( [ rgbeLoader.loadAsync( 'blouberg_sunrise_2_1k.hdr' ), glbloader.loadAsync( 'duck.glb' ) ] );
+ env.mapping = THREE.EquirectangularReflectionMapping;
+ scene.environment = env;
+ scene.background = env;
+ scene.backgroundBlurriness = 0.3;
+ scene.environmentIntensity = 1.25;
- sphereMaterial.positionNode = Fn( () => {
+ duckModel = model.scene.children[ 0 ];
+ duckModel.receiveShadow = true;
+ duckModel.castShadow = true;
+ duckModel.material.positionNode = Fn( () => {
- const instancePosition = sphereVelocityStorage.element( instanceIndex ).get( 'position' );
+ const instancePosition = duckInstanceDataStorage.element( instanceIndex ).get( 'position' );
const newPosition = positionLocal.add( instancePosition );
return newPosition;
-
+
} )();
- const sphereMesh = new THREE.InstancedMesh( sphereGeometry, sphereMaterial, NUM_SPHERES );
- scene.add( sphereMesh );
+ const duckMesh = new THREE.InstancedMesh( duckModel.geometry, duckModel.material, NUM_DUCKS );
+
+ scene.add( duckMesh );
renderer = new THREE.WebGPURenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
+ renderer.toneMappingExposure = 0.5;
renderer.setAnimationLoop( animate );
container.appendChild( renderer.domElement );
+ controls = new OrbitControls( camera, container );
+
+ container.style.touchAction = 'none';
+
stats = new Stats();
container.appendChild( stats.dom );
container.style.touchAction = 'none';
container.addEventListener( 'pointermove', onPointerMove );
+ container.addEventListener( 'pointerdown', onPointerDown );
+ container.addEventListener( 'pointerup', onPointerUp );
window.addEventListener( 'resize', onWindowResize );
const gui = new GUI();
- gui.add( effectController.mouseSize, 'value', 1.0, 100.0, 1.0 ).name( 'Mouse Size' );
+ gui.add( effectController.mouseSize, 'value', 0.1, 1, 0.1 ).name( 'Mouse Size' );
+ gui.add( effectController.mouseDeep, 'value', 0.01, 1, 0.01 ).name( 'Mouse Deep' );
gui.add( effectController.viscosity, 'value', 0.9, 0.999, 0.001 ).name( 'viscosity' );
const buttonCompute = {
smoothWater: function () {
@@ -412,16 +460,20 @@
}
};
- gui.add( buttonCompute, 'smoothWater' );
- gui.add( effectController, 'spheresEnabled' ).onChange( () => {
+ //gui.add( buttonCompute, 'smoothWater' );
+ gui.add( effectController, 'speed', 1, 6, 1 );
+ gui.add( effectController, 'ducksEnabled' ).onChange( () => {
- sphereMesh.visible = effectController.spheresEnabled;
+ duckMesh.visible = effectController.ducksEnabled;
} );
gui.add( effectController, 'wireframe' ).onChange( () => {
waterMesh.material.wireframe = ! waterMesh.material.wireframe;
+ poolBorder.material.wireframe = ! poolBorder.material.wireframe;
+ duckModel.material.wireframe = ! duckModel.material.wireframe;
waterMesh.material.needsUpdate = true;
+ poolBorder.material.needsUpdate = true;
} );
@@ -443,6 +495,19 @@
}
+ function onPointerDown( event ) {
+
+ mouseDown = true;
+
+ }
+
+ function onPointerUp( event ) {
+
+ mouseDown = false;
+ controls.enabled = true;
+
+ }
+
function onPointerMove( event ) {
if ( event.isPrimary === false ) return;
@@ -458,9 +523,9 @@
}
- function render() {
+ function raycast() {
- if ( mouseMoved ) {
+ if ( mouseDown ) {
raycaster.setFromCamera( mouseCoords, camera );
@@ -470,27 +535,43 @@
const point = intersects[ 0 ].point;
effectController.mousePos.value.set( point.x, point.z );
+ if ( controls.enabled ) {
+
+ controls.enabled = false;
+
+ }
} else {
effectController.mousePos.value.set( 10000, 10000 );
}
-
- mouseMoved = false;
-
+
} else {
effectController.mousePos.value.set( 10000, 10000 );
-
- }
- renderer.computeAsync( computeHeight );
+ }
+
+ }
+
+ function render() {
+
+ raycast();
+ frame ++;
- if ( effectController.spheresEnabled ) {
+ if ( frame >= 7 - effectController.speed ) {
- renderer.computeAsync( computeSphere );
+ renderer.computeAsync( computeHeight );
+ if ( effectController.ducksEnabled ) {
+
+ renderer.computeAsync( computeSphere );
+
+ }
+
+ frame = 0;
+
}
renderer.render( scene, camera );