|
|
@@ -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<vec3> + velocity<vec2> + unused<vec3> = 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 );
|