Просмотр исходного кода

Water2: Add `WebGPURenderer` version. (#29027)

* Water2: Add `WebGPURenderer` version.

* E2E: Update screenshot.

* Fix typo.
Michael Herzog 1 год назад
Родитель
Сommit
b331d8e6be

+ 2 - 1
examples/files.json

@@ -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",

+ 160 - 0
examples/jsm/objects/Water2Mesh.js

@@ -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 ) );
+
+			// calculate final uv coords
+
+			return vec4( this.color, 1.0 ).mul( mix( refractionSampler, reflectionSampler, reflectance ) );
+
+		} )();
+
+		return outputNode;
+
+	}
+
+}
+
+export { WaterMesh };

BIN
examples/screenshots/webgpu_water.jpg


+ 212 - 0
examples/webgpu_water.html

@@ -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>

粤ICP备19079148号