2
0
Эх сурвалжийг харах

WebGPURenderer: ReflectorNode (#27611)

* Add first prevent update

* GaussianBlurNode: Fix type

* Rename lightNodes -> lightsNode

* Add ReflectorNode

* add reflector examples

* cleanup

* rev

* revision

* rev

* update reflection resolution

* Add prevent update
sunag 1 жил өмнө
parent
commit
7e226d00e2

+ 2 - 0
examples/files.json

@@ -356,6 +356,7 @@
 		"webgpu_occlusion",
 		"webgpu_particles",
 		"webgpu_portal",
+		"webgpu_reflection",
 		"webgpu_rtt",
 		"webgpu_sandbox",
 		"webgpu_shadertoy",
@@ -369,6 +370,7 @@
 		"webgpu_tsl_transpiler",
 		"webgpu_video_panorama",
 		"webgpu_postprocessing_afterimage",
+		"webgpu_mirror",
 		"webgpu_multisampled_renderbuffers",
 		"webgpu_materials_texture_anisotropy"
 	],

+ 2 - 1
examples/jsm/nodes/Nodes.js

@@ -68,6 +68,7 @@ export { default as SplitNode } from './utils/SplitNode.js';
 export { default as SpriteSheetUVNode, spritesheetUV } from './utils/SpriteSheetUVNode.js';
 export { default as TimerNode, timerLocal, timerGlobal, timerDelta, frameId } from './utils/TimerNode.js';
 export { default as TriplanarTexturesNode, triplanarTextures, triplanarTexture } from './utils/TriplanarTexturesNode.js';
+export { default as ReflectorNode, reflector } from './utils/ReflectorNode.js';
 
 // shadernode
 export * from './shadernode/ShaderNode.js';
@@ -146,7 +147,7 @@ export { default as DirectionalLightNode } from './lighting/DirectionalLightNode
 export { default as SpotLightNode } from './lighting/SpotLightNode.js';
 export { default as IESSpotLightNode } from './lighting/IESSpotLightNode.js';
 export { default as AmbientLightNode } from './lighting/AmbientLightNode.js';
-export { default as LightsNode, lights, lightNodes, addLightNode } from './lighting/LightsNode.js';
+export { default as LightsNode, lights, lightsNode, addLightNode } from './lighting/LightsNode.js';
 export { default as LightingNode /* @TODO: lighting (abstract), light */ } from './lighting/LightingNode.js';
 export { default as LightingContextNode, lightingContext } from './lighting/LightingContextNode.js';
 export { default as HemisphereLightNode } from './lighting/HemisphereLightNode.js';

+ 16 - 8
examples/jsm/nodes/core/NodeFrame.js

@@ -53,9 +53,11 @@ class NodeFrame {
 
 			if ( frameMap.get( node ) !== this.frameId ) {
 
-				frameMap.set( node, this.frameId );
+				if ( node.updateBefore( this ) !== false ) {
 
-				node.updateBefore( this );
+					frameMap.set( node, this.frameId );
+
+				}
 
 			}
 
@@ -65,9 +67,11 @@ class NodeFrame {
 
 			if ( renderMap.get( node ) !== this.renderId ) {
 
-				renderMap.set( node, this.renderId );
+				if ( node.updateBefore( this ) !== false ) {
+
+					renderMap.set( node, this.renderId );
 
-				node.updateBefore( this );
+				}
 
 			}
 
@@ -90,9 +94,11 @@ class NodeFrame {
 
 			if ( frameMap.get( node ) !== this.frameId ) {
 
-				frameMap.set( node, this.frameId );
+				if ( node.update( this ) !== false ) {
 
-				node.update( this );
+					frameMap.set( node, this.frameId );
+
+				}
 
 			}
 
@@ -102,9 +108,11 @@ class NodeFrame {
 
 			if ( renderMap.get( node ) !== this.renderId ) {
 
-				renderMap.set( node, this.renderId );
+				if ( node.update( this ) !== false ) {
+
+					renderMap.set( node, this.renderId );
 
-				node.update( this );
+				}
 
 			}
 

+ 1 - 1
examples/jsm/nodes/display/GaussianBlurNode.js

@@ -18,7 +18,7 @@ class GaussianBlurNode extends TempNode {
 
 	constructor( textureNode, sigma = 2 ) {
 
-		super( textureNode );
+		super( 'vec4' );
 
 		this.textureNode = textureNode;
 		this.sigma = sigma;

+ 1 - 1
examples/jsm/nodes/lighting/LightsNode.js

@@ -169,7 +169,7 @@ class LightsNode extends Node {
 export default LightsNode;
 
 export const lights = ( lights ) => nodeObject( new LightsNode().fromLights( lights ) );
-export const lightNodes = nodeProxy( LightsNode );
+export const lightsNode = nodeProxy( LightsNode );
 
 export function addLightNode( lightClass, lightNodeClass ) {
 

+ 4 - 4
examples/jsm/nodes/materials/NodeMaterial.js

@@ -11,7 +11,7 @@ import { skinning } from '../accessors/SkinningNode.js';
 import { morph } from '../accessors/MorphNode.js';
 import { texture } from '../accessors/TextureNode.js';
 import { cubeTexture } from '../accessors/CubeTextureNode.js';
-import { lightNodes } from '../lighting/LightsNode.js';
+import { lightsNode } from '../lighting/LightsNode.js';
 import { mix } from '../math/MathNode.js';
 import { float, vec3, vec4 } from '../shadernode/ShaderNode.js';
 import AONode from '../lighting/AONode.js';
@@ -289,15 +289,15 @@ class NodeMaterial extends ShaderMaterial {
 
 		}
 
-		let lightsNode = this.lightsNode || builder.lightsNode;
+		let lightsN = this.lightsNode || builder.lightsNode;
 
 		if ( materialLightsNode.length > 0 ) {
 
-			lightsNode = lightNodes( [ ...lightsNode.lightNodes, ...materialLightsNode ] );
+			lightsN = lightsNode( [ ...lightsN.lightNodes, ...materialLightsNode ] );
 
 		}
 
-		return lightsNode;
+		return lightsN;
 
 	}
 

+ 227 - 0
examples/jsm/nodes/utils/ReflectorNode.js

@@ -0,0 +1,227 @@
+import TextureNode from '../accessors/TextureNode.js';
+import { nodeObject, vec2 } from '../shadernode/ShaderNode.js';
+import { NodeUpdateType } from '../core/constants.js';
+import { viewportTopLeft } from '../display/ViewportNode.js';
+import { Matrix4, Vector2, Vector3, Vector4, Object3D, Plane, RenderTarget, HalfFloatType, LinearMipMapLinearFilter } from 'three';
+
+const _reflectorPlane = new Plane();
+const _normal = new Vector3();
+const _reflectorWorldPosition = new Vector3();
+const _cameraWorldPosition = new Vector3();
+const _rotationMatrix = new Matrix4();
+const _lookAtPosition = new Vector3( 0, 0, - 1 );
+const clipPlane = new Vector4();
+
+const _view = new Vector3();
+const _target = new Vector3();
+const _q = new Vector4();
+
+const _size = new Vector2();
+
+const _defaultRT = new RenderTarget();
+const _defaultUV = vec2( viewportTopLeft.x.oneMinus(), viewportTopLeft.y );
+
+let _inReflector = false;
+
+class ReflectorNode extends TextureNode {
+
+	constructor( parameters = {} ) {
+
+		super( _defaultRT.texture, _defaultUV );
+
+		const {
+			target = new Object3D(),
+			resolution = 1,
+			generateMipmaps = false,
+			bounces = true
+		} = parameters;
+
+		//
+
+		this.target = target;
+		this.resolution = resolution;
+		this.generateMipmaps = generateMipmaps;
+		this.bounces = bounces;
+
+		this.updateBeforeType = bounces ? NodeUpdateType.RENDER : NodeUpdateType.FRAME;
+
+		this.virtualCameras = new WeakMap();
+		this.renderTargets = new WeakMap();
+
+
+	}
+
+	_updateResolution( renderTarget, renderer ) {
+
+		const resolution = this.resolution;
+
+		renderer.getDrawingBufferSize( _size );
+
+		renderTarget.setSize( Math.round( _size.width * resolution ), Math.round( _size.height * resolution ) );
+
+	}
+
+	setup( builder ) {
+
+		this._updateResolution( _defaultRT, builder.renderer );
+
+		return super.setup( builder );
+
+	}
+
+	getTextureNode() {
+
+		return this.textureNode;
+
+	}
+
+	getVirtualCamera( camera ) {
+
+		let virtualCamera = this.virtualCameras.get( camera );
+
+		if ( virtualCamera === undefined ) {
+
+			virtualCamera = camera.clone();
+
+			this.virtualCameras.set( camera, virtualCamera );
+
+		}
+
+		return virtualCamera;
+
+	}
+
+	getRenderTarget( camera ) {
+
+		let renderTarget = this.renderTargets.get( camera );
+
+		if ( renderTarget === undefined ) {
+
+			renderTarget = new RenderTarget( 0, 0, { type: HalfFloatType } );
+
+			if ( this.generateMipmaps === true ) {
+
+			    renderTarget.texture.minFilter = LinearMipMapLinearFilter;
+			    renderTarget.texture.generateMipmaps = true;
+
+			}
+
+			this.renderTargets.set( camera, renderTarget );
+
+		}
+
+		return renderTarget;
+
+	}
+
+	updateBefore( frame ) {
+
+		if ( this.bounces === false && _inReflector ) return false;
+
+		_inReflector = true;
+
+		const { scene, camera, renderer, material } = frame;
+		const { target } = this;
+
+		const virtualCamera = this.getVirtualCamera( camera );
+		const renderTarget = this.getRenderTarget( virtualCamera );
+
+		renderer.getDrawingBufferSize( _size );
+
+		this._updateResolution( renderTarget, renderer );
+
+		//
+
+		_reflectorWorldPosition.setFromMatrixPosition( target.matrixWorld );
+		_cameraWorldPosition.setFromMatrixPosition( camera.matrixWorld );
+
+		_rotationMatrix.extractRotation( target.matrixWorld );
+
+		_normal.set( 0, 0, 1 );
+		_normal.applyMatrix4( _rotationMatrix );
+
+		_view.subVectors( _reflectorWorldPosition, _cameraWorldPosition );
+
+		// Avoid rendering when reflector is facing away
+
+		if ( _view.dot( _normal ) > 0 ) return;
+
+		_view.reflect( _normal ).negate();
+		_view.add( _reflectorWorldPosition );
+
+		_rotationMatrix.extractRotation( camera.matrixWorld );
+
+		_lookAtPosition.set( 0, 0, - 1 );
+		_lookAtPosition.applyMatrix4( _rotationMatrix );
+		_lookAtPosition.add( _cameraWorldPosition );
+
+		_target.subVectors( _reflectorWorldPosition, _lookAtPosition );
+		_target.reflect( _normal ).negate();
+		_target.add( _reflectorWorldPosition );
+
+		//
+
+		virtualCamera.coordinateSystem = camera.coordinateSystem;
+		virtualCamera.position.copy( _view );
+		virtualCamera.up.set( 0, 1, 0 );
+		virtualCamera.up.applyMatrix4( _rotationMatrix );
+		virtualCamera.up.reflect( _normal );
+		virtualCamera.lookAt( _target );
+
+		virtualCamera.near = camera.near;
+		virtualCamera.far = camera.far;
+
+		virtualCamera.updateMatrixWorld();
+		virtualCamera.projectionMatrix.copy( camera.projectionMatrix );
+
+		// Now update projection matrix with new clip plane, implementing code from: http://www.terathon.com/code/oblique.html
+		// Paper explaining this technique: http://www.terathon.com/lengyel/Lengyel-Oblique.pdf
+		_reflectorPlane.setFromNormalAndCoplanarPoint( _normal, _reflectorWorldPosition );
+		_reflectorPlane.applyMatrix4( virtualCamera.matrixWorldInverse );
+
+		clipPlane.set( _reflectorPlane.normal.x, _reflectorPlane.normal.y, _reflectorPlane.normal.z, _reflectorPlane.constant );
+
+		const projectionMatrix = virtualCamera.projectionMatrix;
+
+		_q.x = ( Math.sign( clipPlane.x ) + projectionMatrix.elements[ 8 ] ) / projectionMatrix.elements[ 0 ];
+		_q.y = ( Math.sign( clipPlane.y ) + projectionMatrix.elements[ 9 ] ) / projectionMatrix.elements[ 5 ];
+		_q.z = - 1.0;
+		_q.w = ( 1.0 + projectionMatrix.elements[ 10 ] ) / projectionMatrix.elements[ 14 ];
+
+		// Calculate the scaled plane vector
+		clipPlane.multiplyScalar( 1.0 / clipPlane.dot( _q ) );
+
+		const clipBias = 0;
+
+		// Replacing the third row of the projection matrix
+		projectionMatrix.elements[ 2 ] = clipPlane.x;
+		projectionMatrix.elements[ 6 ] = clipPlane.y;
+		projectionMatrix.elements[ 10 ] = clipPlane.z - clipBias;
+		projectionMatrix.elements[ 14 ] = clipPlane.w;
+
+		//
+
+		this.value = renderTarget.texture;
+
+		material.visible = false;
+
+		const currentRenderTarget = renderer.getRenderTarget();
+
+		renderer.setRenderTarget( renderTarget );
+
+		renderer.render( scene, virtualCamera );
+
+		renderer.setRenderTarget( currentRenderTarget );
+
+		material.visible = true;
+
+		_inReflector = false;
+
+	}
+
+}
+
+export const reflector = ( parameters ) => nodeObject( new ReflectorNode( parameters ) );
+
+export default ReflectorNode;
+

BIN
examples/screenshots/webgpu_mirror.jpg


BIN
examples/screenshots/webgpu_reflection.jpg


+ 224 - 0
examples/webgpu_mirror.html

@@ -0,0 +1,224 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - mirror</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="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - mirror
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/",
+					"three/nodes": "./jsm/nodes/Nodes.js"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { MeshPhongNodeMaterial, reflector, uv, texture, color } from 'three/nodes';
+
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			let camera, scene, renderer;
+
+			let cameraControls;
+
+			let sphereGroup, smallSphere;
+
+			init();
+			animate();
+
+			function init() {
+
+				// scene
+				scene = new THREE.Scene();
+
+				// camera
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 500 );
+				camera.position.set( 0, 75, 160 );
+
+				//
+
+				let geometry, material;
+
+				//
+
+				sphereGroup = new THREE.Object3D();
+				scene.add( sphereGroup );
+
+				geometry = new THREE.CylinderGeometry( 0.1, 15 * Math.cos( Math.PI / 180 * 30 ), 0.1, 24, 1 );
+				material = new THREE.MeshPhongMaterial( { color: 0xffffff, emissive: 0x8d8d8d } );
+				const sphereCap = new THREE.Mesh( geometry, material );
+				sphereCap.position.y = - 15 * Math.sin( Math.PI / 180 * 30 ) - 0.05;
+				sphereCap.rotateX( - Math.PI );
+
+				geometry = new THREE.SphereGeometry( 15, 24, 24, Math.PI / 2, Math.PI * 2, 0, Math.PI / 180 * 120 );
+				const halfSphere = new THREE.Mesh( geometry, material );
+				halfSphere.add( sphereCap );
+				halfSphere.rotateX( - Math.PI / 180 * 135 );
+				halfSphere.rotateZ( - Math.PI / 180 * 20 );
+				halfSphere.position.y = 7.5 + 15 * Math.sin( Math.PI / 180 * 30 );
+
+				sphereGroup.add( halfSphere );
+
+				geometry = new THREE.IcosahedronGeometry( 5, 0 );
+				material = new THREE.MeshPhongMaterial( { color: 0xffffff, emissive: 0x7b7b7b, flatShading: true } );
+				smallSphere = new THREE.Mesh( geometry, material );
+				scene.add( smallSphere );
+
+				// textures
+
+				const textureLoader = new THREE.TextureLoader();
+
+				const floorNormal = textureLoader.load( 'textures/floors/FloorsCheckerboard_S_Normal.jpg' );
+				floorNormal.wrapS = THREE.RepeatWrapping;
+				floorNormal.wrapT = THREE.RepeatWrapping;
+
+				const decalDiffuse = textureLoader.load( 'textures/decal/decal-diffuse.png' );
+				decalDiffuse.colorSpace = THREE.SRGBColorSpace;
+
+				const decalNormal = textureLoader.load( 'textures/decal/decal-normal.jpg' );
+
+				// reflectors / mirrors
+
+				const groundReflector = reflector();
+				const verticalReflector = reflector();
+
+				const groundNormalScale = - 0.08;
+				const verticalNormalScale = 0.1;
+
+				const groundUVOffset = texture( decalNormal ).xy.mul( 2 ).sub( 1 ).mul( groundNormalScale );
+				const verticalUVOffset = texture( floorNormal, uv().mul( 5 ) ).xy.mul( 2 ).sub( 1 ).mul( verticalNormalScale );
+
+				groundReflector.uvNode = groundReflector.uvNode.add( groundUVOffset );
+				verticalReflector.uvNode = verticalReflector.uvNode.add( verticalUVOffset );
+
+				const groundNode = texture( decalDiffuse ).a.mix( color( 0xffffff ), groundReflector );
+				const verticalNode = color( 0x0000ff ).mul( .1 ).add( verticalReflector );
+
+				// walls
+
+				const planeGeo = new THREE.PlaneGeometry( 100.1, 100.1 );
+
+				//
+
+				const planeBottom = new THREE.Mesh( planeGeo, new MeshPhongNodeMaterial( {
+					colorNode: groundNode
+				} ) );
+				planeBottom.rotateX( - Math.PI / 2 );
+				planeBottom.add( groundReflector.target );
+				scene.add( planeBottom );
+
+				const planeBack = new THREE.Mesh( planeGeo, new MeshPhongNodeMaterial( {
+					colorNode: verticalNode
+				} ) );
+				planeBack.position.z = - 50;
+				planeBack.position.y = 50;
+				planeBack.add( verticalReflector.target );
+				scene.add( planeBack );
+
+				//
+
+				const planeTop = new THREE.Mesh( planeGeo, new THREE.MeshPhongMaterial( { color: 0xffffff } ) );
+				planeTop.position.y = 100;
+				planeTop.rotateX( Math.PI / 2 );
+				scene.add( planeTop );
+
+				const planeFront = new THREE.Mesh( planeGeo, new THREE.MeshPhongMaterial( { color: 0x7f7fff } ) );
+				planeFront.position.z = 50;
+				planeFront.position.y = 50;
+				planeFront.rotateY( Math.PI );
+				scene.add( planeFront );
+
+				const planeRight = new THREE.Mesh( planeGeo, new THREE.MeshPhongMaterial( { color: 0x00ff00 } ) );
+				planeRight.position.x = 50;
+				planeRight.position.y = 50;
+				planeRight.rotateY( - Math.PI / 2 );
+				scene.add( planeRight );
+
+				const planeLeft = new THREE.Mesh( planeGeo, new THREE.MeshPhongMaterial( { color: 0xff0000 } ) );
+				planeLeft.position.x = - 50;
+				planeLeft.position.y = 50;
+				planeLeft.rotateY( Math.PI / 2 );
+				scene.add( planeLeft );
+
+				// lights
+
+				const mainLight = new THREE.PointLight( 0xe7e7e7, 2.5, 250, 0 );
+				mainLight.position.y = 60;
+				scene.add( mainLight );
+
+				const greenLight = new THREE.PointLight( 0x00ff00, 0.5, 1000, 0 );
+				greenLight.position.set( 550, 50, 0 );
+				scene.add( greenLight );
+
+				const redLight = new THREE.PointLight( 0xff0000, 0.5, 1000, 0 );
+				redLight.position.set( - 550, 50, 0 );
+				scene.add( redLight );
+
+				const blueLight = new THREE.PointLight( 0xbbbbfe, 0.5, 1000, 0 );
+				blueLight.position.set( 0, 50, 550 );
+				scene.add( blueLight );
+
+				// renderer
+
+				renderer = new WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				document.body.appendChild( renderer.domElement );
+
+				// controls
+
+				cameraControls = new OrbitControls( camera, renderer.domElement );
+				cameraControls.target.set( 0, 40, 0 );
+				cameraControls.maxDistance = 400;
+				cameraControls.minDistance = 10;
+				cameraControls.update();
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function animate() {
+
+				const timer = Date.now() * 0.01;
+
+				sphereGroup.rotation.y -= 0.002;
+
+				smallSphere.position.set(
+					Math.cos( timer * 0.1 ) * 30,
+					Math.abs( Math.cos( timer * 0.2 ) ) * 20 + 5,
+					Math.sin( timer * 0.1 ) * 30
+				);
+				smallSphere.rotation.y = ( Math.PI / 2 ) - timer * 0.1;
+				smallSphere.rotation.z = timer * 0.8;
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+	</body>
+</html>

+ 206 - 0
examples/webgpu_reflection.html

@@ -0,0 +1,206 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - reflection</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="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - reflection
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/",
+					"three/nodes": "./jsm/nodes/Nodes.js"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { MeshPhongNodeMaterial, color, pass, reflector, normalWorld, texture, uv, viewportTopLeft } from 'three/nodes';
+
+			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+
+			import WebGPU from 'three/addons/capabilities/WebGPU.js';
+			import WebGL from 'three/addons/capabilities/WebGL.js';
+
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+			import PostProcessing from 'three/addons/renderers/common/PostProcessing.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			import Stats from 'three/addons/libs/stats.module.js';
+
+			let camera, scene, renderer;
+			let model, mixer, clock;
+			let postProcessing;
+			let controls;
+			let stats;
+
+			init();
+
+			function init() {
+
+				if ( WebGPU.isAvailable() === false && WebGL.isWebGL2Available() === false ) {
+
+					document.body.appendChild( WebGPU.getErrorMessage() );
+
+					throw new Error( 'No WebGPU or WebGL2 support' );
+
+				}
+
+				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.25, 30 );
+				camera.position.set( 2, 2.5, 3 );
+
+				scene = new THREE.Scene();
+				scene.fog = new THREE.Fog( 0x0487e2, 7, 25 );
+				scene.backgroundNode = normalWorld.y.mix( color( 0x0487e2 ), color( 0x0066ff ) );
+				camera.lookAt( 0, 1, 0 );
+
+				const sunLight = new THREE.DirectionalLight( 0xFFE499, 5 );
+				sunLight.castShadow = true;
+				sunLight.shadow.camera.near = .1;
+				sunLight.shadow.camera.far = 5;
+				sunLight.shadow.camera.right = 2;
+				sunLight.shadow.camera.left = - 2;
+				sunLight.shadow.camera.top = 2;
+				sunLight.shadow.camera.bottom = - 2;
+				sunLight.shadow.mapSize.width = 2048;
+				sunLight.shadow.mapSize.height = 2048;
+				sunLight.shadow.bias = - 0.001;
+				sunLight.position.set( .5, 3, .5 );
+
+				const waterAmbientLight = new THREE.HemisphereLight( 0x333366, 0x74ccf4, 5 );
+				const skyAmbientLight = new THREE.HemisphereLight( 0x74ccf4, 0, 1 );
+
+				scene.add( sunLight );
+				scene.add( skyAmbientLight );
+				scene.add( waterAmbientLight );
+
+				clock = new THREE.Clock();
+
+				// animated model
+
+				const loader = new GLTFLoader();
+				loader.load( 'models/gltf/Michelle.glb', function ( gltf ) {
+
+					model = gltf.scene;
+					model.children[ 0 ].children[ 0 ].castShadow = true;
+
+					mixer = new THREE.AnimationMixer( model );
+
+					const action = mixer.clipAction( gltf.animations[ 0 ] );
+					action.play();
+
+					scene.add( model );
+
+				} );
+
+				// textures
+
+				const textureLoader = new THREE.TextureLoader();
+
+				const floorColor = textureLoader.load( 'textures/floors/FloorsCheckerboard_S_Diffuse.jpg' );
+				floorColor.wrapS = THREE.RepeatWrapping;
+				floorColor.wrapT = THREE.RepeatWrapping;
+				floorColor.colorSpace = THREE.SRGBColorSpace;
+
+				const floorNormal = textureLoader.load( 'textures/floors/FloorsCheckerboard_S_Normal.jpg' );
+				floorNormal.wrapS = THREE.RepeatWrapping;
+				floorNormal.wrapT = THREE.RepeatWrapping;
+
+				// floor
+
+				const floorUV = uv().mul( 15 );
+				const floorNormalOffset = texture( floorNormal, floorUV ).xy.mul( 2 ).sub( 1 ).mul( .02 );
+
+				const reflection = reflector( { resolution: 0.5 } ); // 0.5 is half of the rendering view
+				reflection.target.rotateX( - Math.PI / 2 );
+				reflection.uvNode = reflection.uvNode.add( floorNormalOffset );
+				scene.add( reflection.target );
+
+				const floorMaterial = new MeshPhongNodeMaterial();
+				floorMaterial.colorNode = texture( floorColor, floorUV ).add( reflection );
+
+				const floor = new THREE.Mesh( new THREE.BoxGeometry( 50, .001, 50 ), floorMaterial );
+				floor.position.set( 0, 0, 0 );
+				scene.add( floor );
+
+				// renderer
+
+				renderer = new WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				document.body.appendChild( renderer.domElement );
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.minDistance = 1;
+				controls.maxDistance = 10;
+				controls.maxPolarAngle = Math.PI / 2;
+				controls.autoRotate = true;
+				controls.autoRotateSpeed = 1;
+				controls.target.set( 0, .5, 0 );
+				controls.update();
+
+				// post-processing
+
+				const scenePass = pass( scene, camera );
+				const scenePassColor = scenePass.getTextureNode();
+				const scenePassDepth = scenePass.getDepthNode().remapClamp( .3, .5 );
+
+				const scenePassColorBlurred = scenePassColor.gaussianBlur();
+				scenePassColorBlurred.directionNode = scenePassDepth;
+
+				const vignet = viewportTopLeft.distance( .5 ).mul( 1.35 ).clamp().oneMinus();
+
+				postProcessing = new PostProcessing( renderer );
+				postProcessing.outputNode = scenePassColorBlurred.mul( vignet );
+
+				//
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function animate() {
+
+				stats.update();
+
+				controls.update();
+
+				const delta = clock.getDelta();
+
+				if ( model ) {
+
+					mixer.update( delta );
+
+				}
+
+				postProcessing.render();
+
+			}
+
+		</script>
+	</body>
+</html>