Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,8 @@
"webgpu_tsl_vfx_tornado",
"webgpu_video_panorama",
"webgpu_volume_cloud",
"webgpu_volume_perlin"
"webgpu_volume_perlin",
"webgpu_water"
],
"webaudio": [
"webaudio_orientation",
Expand Down
160 changes: 160 additions & 0 deletions examples/jsm/objects/Water2Mesh.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import {
Color,
Mesh,
NodeMaterial,
Vector2,
Vector3
} from 'three';
import { vec2, viewportSafeUV, viewportSharedTexture, reflector, pow, float, abs, texture, uniform, TempNode, NodeUpdateType, vec4, tslFn, cameraPosition, positionWorld, uv, mix, vec3, normalize, max, dot, viewportTopLeft } from 'three/tsl';

/**
* References:
* https://alex.vlachos.com/graphics/Vlachos-SIGGRAPH10-WaterFlow.pdf
* http://graphicsrunner.blogspot.de/2010/08/water-using-flow-maps.html
*
*/

class WaterMesh extends Mesh {

constructor( geometry, options = {} ) {

const material = new NodeMaterial();

super( geometry, material );

this.isWater = true;

material.normals = false;
material.fragmentNode = new WaterNode( options, this );

}

}

class WaterNode extends TempNode {

constructor( options, waterBody ) {

super( 'vec4' );

this.waterBody = waterBody;

this.normalMap0 = texture( options.normalMap0 );
this.normalMap1 = texture( options.normalMap1 );
this.flowMap = texture( options.flowMap !== undefined ? options.flowMap : null );

this.color = uniform( options.color !== undefined ? new Color( options.color ) : new Color( 0xffffff ) );
this.flowDirection = uniform( options.flowDirection !== undefined ? options.flowDirection : new Vector2( 1, 0 ) );
this.flowSpeed = uniform( options.flowSpeed !== undefined ? options.flowSpeed : 0.03 );
this.reflectivity = uniform( options.reflectivity !== undefined ? options.reflectivity : 0.02 );
this.scale = uniform( options.scale !== undefined ? options.scale : 1 );
this.flowConfig = uniform( new Vector3() );

this.updateBeforeType = NodeUpdateType.RENDER;

this._cycle = 0.15; // a cycle of a flow map phase
this._halfCycle = this._cycle * 0.5;

this._USE_FLOW = options.flowMap !== undefined;

}

updateFlow( delta ) {

this.flowConfig.value.x += this.flowSpeed.value * delta; // flowMapOffset0
this.flowConfig.value.y = this.flowConfig.value.x + this._halfCycle; // flowMapOffset1

// Important: The distance between offsets should be always the value of "halfCycle".
// Moreover, both offsets should be in the range of [ 0, cycle ].
// This approach ensures a smooth water flow and avoids "reset" effects.

if ( this.flowConfig.value.x >= this._cycle ) {

this.flowConfig.value.x = 0;
this.flowConfig.value.y = this._halfCycle;

} else if ( this.flowConfig.value.y >= this._cycle ) {

this.flowConfig.value.y = this.flowConfig.value.y - this._cycle;

}

this.flowConfig.value.z = this._halfCycle;

}

updateBefore( frame ) {

this.updateFlow( frame.deltaTime );

}

setup() {

const outputNode = tslFn( () => {

const flowMapOffset0 = this.flowConfig.x;
const flowMapOffset1 = this.flowConfig.y;
const halfCycle = this.flowConfig.z;

const toEye = normalize( cameraPosition.sub( positionWorld ) );

let flow;

if ( this._USE_FLOW === true ) {

flow = this.flowMap.rg.mul( 2 ).sub( 1 );

} else {

flow = vec2( this.flowDirection.x, this.flowDirection.y );

}

flow.x.mulAssign( - 1 );

// sample normal maps (distort uvs with flowdata)

const uvs = uv();

const normalUv0 = uvs.mul( this.scale ).add( flow.mul( flowMapOffset0 ) );
const normalUv1 = uvs.mul( this.scale ).add( flow.mul( flowMapOffset1 ) );

const normalColor0 = this.normalMap0.uv( normalUv0 );
const normalColor1 = this.normalMap1.uv( normalUv1 );

// linear interpolate to get the final normal color
const flowLerp = abs( halfCycle.sub( flowMapOffset0 ) ).div( halfCycle );
const normalColor = mix( normalColor0, normalColor1, flowLerp );

// calculate normal vector
const normal = normalize( vec3( normalColor.r.mul( 2 ).sub( 1 ), normalColor.b, normalColor.g.mul( 2 ).sub( 1 ) ) );

// calculate the fresnel term to blend reflection and refraction maps
const theta = max( dot( toEye, normal ), 0 );
const reflectance = pow( float( 1.0 ).sub( theta ), 5.0 ).mul( float( 1.0 ).sub( this.reflectivity ) ).add( this.reflectivity );

// reflector, refractor

const offset = normal.xz.mul( 0.05 ).toVar();

const reflectionSampler = reflector();
this.waterBody.add( reflectionSampler.target );
reflectionSampler.uvNode = reflectionSampler.uvNode.add( offset );

const refractorUV = viewportTopLeft.add( offset );
const refractionSampler = viewportSharedTexture( viewportSafeUV( refractorUV ) );
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sunag Works like a charm! 🎉


// calculate final uv coords

return vec4( this.color, 1.0 ).mul( mix( refractionSampler, reflectionSampler, reflectance ) );

} )();

return outputNode;

}

}

export { WaterMesh };
Binary file added examples/screenshots/webgpu_water.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
212 changes: 212 additions & 0 deletions examples/webgpu_water.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js - water</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
</head>
<body>

<div id="container"></div>
<div id="info">
<a href="https://threejs.org" target="_blank" rel="noopener noreferrer">three.js</a> - water
</div>

<script type="importmap">
{
"imports": {
"three": "../build/three.webgpu.js",
"three/tsl": "../build/three.webgpu.js",
"three/addons/": "./jsm/"
}
}
</script>

<script type="module">

import * as THREE from 'three';

import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { WaterMesh } from 'three/addons/objects/Water2Mesh.js';

let scene, camera, clock, renderer, water;

let torusKnot;

const params = {
color: '#ffffff',
scale: 4,
flowX: 1,
flowY: 1
};

init();

function init() {

// scene

scene = new THREE.Scene();

// camera

camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 200 );
camera.position.set( - 15, 7, 15 );
camera.lookAt( scene.position );

// clock

clock = new THREE.Clock();

// mesh

const torusKnotGeometry = new THREE.TorusKnotGeometry( 3, 1, 256, 32 );
const torusKnotMaterial = new THREE.MeshNormalMaterial();

torusKnot = new THREE.Mesh( torusKnotGeometry, torusKnotMaterial );
torusKnot.position.y = 4;
torusKnot.scale.set( 0.5, 0.5, 0.5 );
scene.add( torusKnot );

// ground

const groundGeometry = new THREE.PlaneGeometry( 20, 20 );
const groundMaterial = new THREE.MeshStandardMaterial( { roughness: 0.8, metalness: 0.4 } );
const ground = new THREE.Mesh( groundGeometry, groundMaterial );
ground.rotation.x = Math.PI * - 0.5;
scene.add( ground );

const textureLoader = new THREE.TextureLoader();
textureLoader.load( 'textures/hardwood2_diffuse.jpg', function ( map ) {

map.wrapS = THREE.RepeatWrapping;
map.wrapT = THREE.RepeatWrapping;
map.anisotropy = 16;
map.repeat.set( 4, 4 );
map.colorSpace = THREE.SRGBColorSpace;
groundMaterial.map = map;
groundMaterial.needsUpdate = true;

} );

//

const normalMap0 = textureLoader.load( 'textures/water/Water_1_M_Normal.jpg' );
const normalMap1 = textureLoader.load( 'textures/water/Water_2_M_Normal.jpg' );

normalMap0.wrapS = normalMap0.wrapT = THREE.RepeatWrapping;
normalMap1.wrapS = normalMap1.wrapT = THREE.RepeatWrapping;

// water

const waterGeometry = new THREE.PlaneGeometry( 20, 20 );

water = new WaterMesh( waterGeometry, {
color: params.color,
scale: params.scale,
flowDirection: new THREE.Vector2( params.flowX, params.flowY ),
normalMap0: normalMap0,
normalMap1: normalMap1
} );

water.position.y = 1;
water.rotation.x = Math.PI * - 0.5;
scene.add( water );

// skybox

const cubeTextureLoader = new THREE.CubeTextureLoader();
cubeTextureLoader.setPath( 'textures/cube/Park2/' );

const cubeTexture = cubeTextureLoader.load( [
'posx.jpg', 'negx.jpg',
'posy.jpg', 'negy.jpg',
'posz.jpg', 'negz.jpg'
] );

scene.background = cubeTexture;

// light

const ambientLight = new THREE.AmbientLight( 0xe7e7e7, 1.2 );
scene.add( ambientLight );

const directionalLight = new THREE.DirectionalLight( 0xffffff, 2 );
directionalLight.position.set( - 1, 1, 1 );
scene.add( directionalLight );

// renderer

renderer = new THREE.WebGPURenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setAnimationLoop( animate );
document.body.appendChild( renderer.domElement );

// gui

const gui = new GUI();
const waterNode = water.material.fragmentNode;

gui.addColor( params, 'color' ).onChange( function ( value ) {

waterNode.color.value.set( value );

} );
gui.add( params, 'scale', 1, 10 ).onChange( function ( value ) {

waterNode.scale.value = value;

} );
gui.add( params, 'flowX', - 1, 1 ).step( 0.01 ).onChange( function ( value ) {

waterNode.flowDirection.value.x = value;
waterNode.flowDirection.value.normalize();

} );
gui.add( params, 'flowY', - 1, 1 ).step( 0.01 ).onChange( function ( value ) {

waterNode.flowDirection.value.y = value;
waterNode.flowDirection.value.normalize();

} );

gui.open();

//

const controls = new OrbitControls( camera, renderer.domElement );
controls.minDistance = 5;
controls.maxDistance = 50;

//

window.addEventListener( 'resize', onWindowResize );

}

function onWindowResize() {

camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );

}

function animate() {

const delta = clock.getDelta();

torusKnot.rotation.x += delta;
torusKnot.rotation.y += delta * 0.5;

renderer.render( scene, camera );

}

</script>

</body>
</html>