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