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

Nodes: Add `VelocityNode` and `MotionBlurNode`. (#29058)

* Nodes: Add VelocityNode.

* E2E: Update screenshot.

* apply immutable node

* updates

* update

* Enable damping.

* Updated MRT example.

* Nodes: Add `MotionBlur` node.

* Examples: Clean up

* E2E: Update screenshot.

* updates

* update

* ReflectorNode: Add MRT support

* cleanup

* updates

* camera angle

* cleanup

* updates

* update imports

* Revert "updates"

This reverts commit 5b39722d63527eba882db4de6fd091346e6da580.

* Revert "cleanup"

This reverts commit 213bf028194a4d264a4c2276b2148318ab812ae7.

* updates

* update limits

* update

---------

Co-authored-by: sunag <sunagbrasil@gmail.com>
Michael Herzog 1 год назад
Родитель
Сommit
6b126f1eb5

+ 1 - 0
examples/files.json

@@ -386,6 +386,7 @@
 		"webgpu_postprocessing_pixel",
 		"webgpu_postprocessing_fxaa",
 		"webgpu_postprocessing_masking",
+		"webgpu_postprocessing_motion_blur",
 		"webgpu_postprocessing_sobel",
 		"webgpu_postprocessing_transition",
 		"webgpu_postprocessing",

BIN
examples/screenshots/webgpu_postprocessing_motion_blur.jpg


+ 1 - 1
examples/webgpu_postprocessing_bloom_selective.html

@@ -114,7 +114,7 @@
 
 					const material = intersects[ 0 ].object.material;
 
-					const bloomIntensity = material.mrtNode.getNode( 'bloomIntensity' );
+					const bloomIntensity = material.mrtNode.get( 'bloomIntensity' );
 					bloomIntensity.value = bloomIntensity.value === 0 ? 1 : 0;
 
 				}

+ 244 - 0
examples/webgpu_postprocessing_motion_blur.html

@@ -0,0 +1,244 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - motion blur</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 - motion blur
+		</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 { pass, texture, motionBlur, uniform, output, mrt, mix, velocity, uv, viewportTopLeft } from 'three/tsl';
+
+			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			import Stats from 'three/addons/libs/stats.module.js';
+
+			let camera, scene, renderer;
+			let boxLeft, boxRight, model, mixer, clock;
+			let postProcessing;
+			let controls;
+			let stats;
+
+			const params = {
+				speed: 1.0
+			};
+
+			init();
+
+			function init() {
+
+				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.25, 30 );
+				camera.position.set( 0, 1.5, 4.5 );
+
+				scene = new THREE.Scene();
+				scene.fog = new THREE.Fog( 0x0487e2, 7, 25 );
+
+				const sunLight = new THREE.DirectionalLight( 0xFFE499, 5 );
+				sunLight.castShadow = true;
+				sunLight.shadow.camera.near = .1;
+				sunLight.shadow.camera.far = 10;
+				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( 4, 4, 2 );
+
+				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/Xbot.glb', function ( gltf ) {
+
+					model = gltf.scene;
+
+					model.rotation.y = Math.PI / 2;
+
+					model.traverse( function ( child ) {
+
+						if ( child.isMesh ) {
+
+							child.castShadow = true;
+							child.receiveShadow = true;
+
+						}
+
+					} );
+
+					mixer = new THREE.AnimationMixer( model );
+
+					const action = mixer.clipAction( gltf.animations[ 3 ] );
+					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( 5 );
+
+				const floorMaterial = new THREE.MeshPhongNodeMaterial();
+				floorMaterial.colorNode = texture( floorColor, floorUV );
+
+				const floor = new THREE.Mesh( new THREE.BoxGeometry( 15, .001, 15 ), floorMaterial );
+				floor.receiveShadow = true;
+
+				floor.position.set( 0, 0, 0 );
+				scene.add( floor );
+
+				const walls = new THREE.Mesh( new THREE.BoxGeometry( 15, 15, 15 ), new THREE.MeshPhongNodeMaterial( { colorNode: floorMaterial.colorNode, side: THREE.BackSide } ) );
+				scene.add( walls );
+
+				const map = new THREE.TextureLoader().load( 'textures/uv_grid_opengl.jpg' );
+				map.colorSpace = THREE.SRGBColorSpace;
+
+				const geometry = new THREE.TorusGeometry( .8 );
+				const material = new THREE.MeshBasicMaterial( { map } );
+
+				boxRight = new THREE.Mesh( geometry, material );
+				boxRight.position.set( 3.5, 1.5, - 4 );
+				scene.add( boxRight );
+
+				boxLeft = new THREE.Mesh( geometry, material );
+				boxLeft.position.set( - 3.5, 1.5, - 4 );
+				scene.add( boxLeft );
+
+				// renderer
+
+				renderer = new THREE.WebGPURenderer();
+				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, 1, 0 );
+				controls.enableDamping = true;
+				controls.dampingFactor = 0.05;
+				controls.update();
+
+				// post-processing
+
+				const blurAmount = uniform( 1 );
+				const showVelocity = uniform( 0 );
+
+				const scenePass = pass( scene, camera );
+
+				scenePass.setMRT( mrt( {
+					output,
+					velocity
+				} ) );
+
+				const beauty = scenePass.getTextureNode();
+				const vel = scenePass.getTextureNode( 'velocity' ).mul( blurAmount );
+
+				const mBlur = motionBlur( beauty, vel );
+
+				const vignet = viewportTopLeft.distance( .5 ).remap( .6, 1 ).mul( 2 ).clamp().oneMinus();
+
+				postProcessing = new THREE.PostProcessing( renderer );
+				postProcessing.outputNode = mix( mBlur, vel, showVelocity ).mul( vignet );
+
+				//
+
+				const gui = new GUI();
+				gui.title( 'Motion Blur Settings' );
+				gui.add( controls, 'autoRotate' );
+				gui.add( blurAmount, 'value', 0, 3 ).name( 'blur amount' );
+				gui.add( params, 'speed', 0, 2 );
+				gui.add( showVelocity, 'value', 0, 1 ).name( 'show velocity' );
+
+				//
+
+				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();
+				const speed = params.speed;
+
+				boxRight.rotation.y += delta * 4 * speed;
+				boxLeft.scale.setScalar( 1 + Math.sin( clock.elapsedTime * 10 * speed ) * .2 );
+
+				if ( model ) {
+
+					mixer.update( delta * speed );
+
+				}
+
+				postProcessing.render();
+
+			}
+
+		</script>
+	</body>
+</html>

+ 2 - 0
src/nodes/Nodes.js

@@ -113,6 +113,7 @@ export { default as StorageTextureNode, storageTexture, textureStore } from './a
 export { default as Texture3DNode, texture3D } from './accessors/Texture3DNode.js';
 export * from './accessors/UVNode.js';
 export { default as UserDataNode, userData } from './accessors/UserDataNode.js';
+export * from './accessors/VelocityNode.js';
 
 // display
 export { default as BlendModeNode, burn, dodge, overlay, screen } from './display/BlendModeNode.js';
@@ -137,6 +138,7 @@ export { default as DotScreenNode, dotScreen } from './display/DotScreenNode.js'
 export { default as RGBShiftNode, rgbShift } from './display/RGBShiftNode.js';
 export { default as FilmNode, film } from './display/FilmNode.js';
 export { default as Lut3DNode, lut3D } from './display/Lut3DNode.js';
+export * from './display/MotionBlurNode.js';
 export { default as GTAONode, ao } from './display/GTAONode.js';
 export { default as DenoiseNode, denoise } from './display/DenoiseNode.js';
 export { default as FXAANode, fxaa } from './display/FXAANode.js';

+ 2 - 1
src/nodes/accessors/PositionNode.js

@@ -3,7 +3,8 @@ import { varying } from '../core/VaryingNode.js';
 import { modelWorldMatrix, modelViewMatrix } from './ModelNode.js';
 
 export const positionGeometry = /*#__PURE__*/ attribute( 'position', 'vec3' );
-export const positionLocal = /*#__PURE__*/ positionGeometry.toVar( 'positionLocal' );
+export const positionLocal = /*#__PURE__*/ positionGeometry.varying( 'positionLocal' );
+export const positionPrevious = /*#__PURE__*/ positionGeometry.varying( 'positionPrevious' );
 export const positionWorld = /*#__PURE__*/ varying( modelWorldMatrix.mul( positionLocal ).xyz, 'v_positionWorld' );
 export const positionWorldDirection = /*#__PURE__*/ varying( positionLocal.transformDirection( modelWorldMatrix ), 'v_positionWorldDirection' ).normalize().toVar( 'positionWorldDirection' );
 export const positionView = /*#__PURE__*/ varying( modelViewMatrix.mul( positionLocal ).xyz, 'v_positionView' );

+ 68 - 12
src/nodes/accessors/SkinningNode.js

@@ -5,11 +5,13 @@ import { attribute } from '../core/AttributeNode.js';
 import { reference, referenceBuffer } from './ReferenceNode.js';
 import { add } from '../math/OperatorNode.js';
 import { normalLocal } from './NormalNode.js';
-import { positionLocal } from './PositionNode.js';
+import { positionLocal, positionPrevious } from './PositionNode.js';
 import { tangentLocal } from './TangentNode.js';
 import { uniform } from '../core/UniformNode.js';
 import { buffer } from './BufferNode.js';
 
+const _frameId = new WeakMap();
+
 class SkinningNode extends Node {
 
 	constructor( skinnedMesh, useReference = false ) {
@@ -45,21 +47,22 @@ class SkinningNode extends Node {
 		this.bindMatrixNode = bindMatrixNode;
 		this.bindMatrixInverseNode = bindMatrixInverseNode;
 		this.boneMatricesNode = boneMatricesNode;
+		this.previousBoneMatricesNode = null;
 
 	}
 
-	setup( builder ) {
+	getSkinnedPosition( boneMatrices = this.boneMatricesNode, position = positionLocal ) {
 
-		const { skinIndexNode, skinWeightNode, bindMatrixNode, bindMatrixInverseNode, boneMatricesNode } = this;
+		const { skinIndexNode, skinWeightNode, bindMatrixNode, bindMatrixInverseNode } = this;
 
-		const boneMatX = boneMatricesNode.element( skinIndexNode.x );
-		const boneMatY = boneMatricesNode.element( skinIndexNode.y );
-		const boneMatZ = boneMatricesNode.element( skinIndexNode.z );
-		const boneMatW = boneMatricesNode.element( skinIndexNode.w );
+		const boneMatX = boneMatrices.element( skinIndexNode.x );
+		const boneMatY = boneMatrices.element( skinIndexNode.y );
+		const boneMatZ = boneMatrices.element( skinIndexNode.z );
+		const boneMatW = boneMatrices.element( skinIndexNode.w );
 
 		// POSITION
 
-		const skinVertex = bindMatrixNode.mul( positionLocal );
+		const skinVertex = bindMatrixNode.mul( position );
 
 		const skinned = add(
 			boneMatX.mul( skinWeightNode.x ).mul( skinVertex ),
@@ -68,7 +71,18 @@ class SkinningNode extends Node {
 			boneMatW.mul( skinWeightNode.w ).mul( skinVertex )
 		);
 
-		const skinPosition = bindMatrixInverseNode.mul( skinned ).xyz;
+		return bindMatrixInverseNode.mul( skinned ).xyz;
+
+	}
+
+	getSkinnedNormal( boneMatrices = this.boneMatricesNode, normal = normalLocal ) {
+
+		const { skinIndexNode, skinWeightNode, bindMatrixNode, bindMatrixInverseNode } = this;
+
+		const boneMatX = boneMatrices.element( skinIndexNode.x );
+		const boneMatY = boneMatrices.element( skinIndexNode.y );
+		const boneMatZ = boneMatrices.element( skinIndexNode.z );
+		const boneMatW = boneMatrices.element( skinIndexNode.w );
 
 		// NORMAL
 
@@ -81,9 +95,44 @@ class SkinningNode extends Node {
 
 		skinMatrix = bindMatrixInverseNode.mul( skinMatrix ).mul( bindMatrixNode );
 
-		const skinNormal = skinMatrix.transformDirection( normalLocal ).xyz;
+		return skinMatrix.transformDirection( normal ).xyz;
+
+	}
 
-		// ASSIGNS
+	getPreviousSkinnedPosition( builder ) {
+
+		const skinnedMesh = builder.object;
+
+		if ( this.previousBoneMatricesNode === null ) {
+
+			skinnedMesh.skeleton.previousBoneMatrices = new Float32Array( skinnedMesh.skeleton.boneMatrices );
+
+			this.previousBoneMatricesNode = referenceBuffer( 'skeleton.previousBoneMatrices', 'mat4', skinnedMesh.skeleton.bones.length );
+
+		}
+
+		return this.getSkinnedPosition( this.previousBoneMatricesNode, positionPrevious );
+
+	}
+
+	needsPreviousBoneMatrices( builder ) {
+
+		const mrt = builder.renderer.getMRT();
+
+		return mrt && mrt.has( 'velocity' );
+
+	}
+
+	setup( builder ) {
+
+		if ( this.needsPreviousBoneMatrices( builder ) ) {
+
+			positionPrevious.assign( this.getPreviousSkinnedPosition( builder ) );
+
+		}
+
+		const skinPosition = this.getSkinnedPosition();
+		const skinNormal = this.getSkinnedNormal();
 
 		positionLocal.assign( skinPosition );
 		normalLocal.assign( skinNormal );
@@ -109,8 +158,15 @@ class SkinningNode extends Node {
 	update( frame ) {
 
 		const object = this.useReference ? frame.object : this.skinnedMesh;
+		const skeleton = object.skeleton;
+
+		if ( _frameId.get( skeleton ) === frame.frameId ) return;
+
+		_frameId.set( skeleton, frame.frameId );
+
+		if ( this.previousBoneMatricesNode !== null ) skeleton.previousBoneMatrices.set( skeleton.boneMatrices );
 
-		object.skeleton.update();
+		skeleton.update();
 
 	}
 

+ 83 - 0
src/nodes/accessors/VelocityNode.js

@@ -0,0 +1,83 @@
+import { addNodeClass } from '../core/Node.js';
+import TempNode from '../core/TempNode.js';
+import { modelViewMatrix } from './ModelNode.js';
+import { positionLocal, positionPrevious } from './PositionNode.js';
+import { nodeImmutable } from '../shadernode/ShaderNode.js';
+import { NodeUpdateType } from '../core/constants.js';
+import { Matrix4 } from '../../math/Matrix4.js';
+import { uniform } from '../core/UniformNode.js';
+import { sub } from '../math/OperatorNode.js';
+import { cameraProjectionMatrix } from './CameraNode.js';
+
+const _matrixCache = new WeakMap();
+
+class VelocityNode extends TempNode {
+
+	constructor() {
+
+		super( 'vec2' );
+
+		this.updateType = NodeUpdateType.OBJECT;
+		this.updateAfterType = NodeUpdateType.OBJECT;
+
+		this.previousProjectionMatrix = uniform( new Matrix4() );
+		this.previousModelViewMatrix = uniform( new Matrix4() );
+
+	}
+
+	update( { camera, object } ) {
+
+		const previousModelMatrix = getPreviousMatrix( object );
+		const previousCameraMatrix = getPreviousMatrix( camera );
+
+		this.previousModelViewMatrix.value.copy( previousModelMatrix );
+		this.previousProjectionMatrix.value.copy( previousCameraMatrix );
+
+	}
+
+	updateAfter( { camera, object } ) {
+
+		const previousModelMatrix = getPreviousMatrix( object );
+		const previousCameraMatrix = getPreviousMatrix( camera );
+
+		previousModelMatrix.copy( object.modelViewMatrix );
+		previousCameraMatrix.copy( camera.projectionMatrix );
+
+	}
+
+	setup( /*builder*/ ) {
+
+		const clipPositionCurrent = cameraProjectionMatrix.mul( modelViewMatrix ).mul( positionLocal );
+		const clipPositionPrevious = this.previousProjectionMatrix.mul( this.previousModelViewMatrix ).mul( positionPrevious );
+
+		const ndcPositionCurrent = clipPositionCurrent.xy.div( clipPositionCurrent.w );
+		const ndcPositionPrevious = clipPositionPrevious.xy.div( clipPositionPrevious.w );
+
+		const velocity = sub( ndcPositionCurrent, ndcPositionPrevious );
+
+		return velocity;
+
+	}
+
+}
+
+function getPreviousMatrix( object ) {
+
+	let previousMatrix = _matrixCache.get( object );
+
+	if ( previousMatrix === undefined ) {
+
+		previousMatrix = new Matrix4();
+		_matrixCache.set( object, previousMatrix );
+
+	}
+
+	return previousMatrix;
+
+}
+
+export default VelocityNode;
+
+export const velocity = nodeImmutable( VelocityNode );
+
+addNodeClass( 'VelocityNode', VelocityNode );

+ 7 - 1
src/nodes/core/MRTNode.js

@@ -30,7 +30,13 @@ class MRTNode extends OutputStructNode {
 
 	}
 
-	getNode( name ) {
+	has( name ) {
+
+		return this.outputNodes[ name ] !== undefined;
+
+	}
+
+	get( name ) {
 
 		return this.outputNodes[ name ];
 

+ 25 - 0
src/nodes/display/MotionBlurNode.js

@@ -0,0 +1,25 @@
+import { float, int, Fn } from '../shadernode/ShaderNode.js';
+import { Loop } from '../utils/LoopNode.js';
+import { uv } from '../accessors/UVNode.js';
+
+export const motionBlur = /*#__PURE__*/ Fn( ( [ inputNode, velocity, numSamples = int( 16 ) ] ) => {
+
+	const sampleColor = ( uv ) => inputNode.uv( uv );
+
+	const uvs = uv();
+
+	const colorResult = sampleColor( uvs ).toVar();
+	const fSamples = float( numSamples );
+
+	Loop( { start: int( 1 ), end: numSamples, type: 'int', condition: '<=' }, ( { i } ) => {
+
+		const offset = velocity.mul( float( i ).div( fSamples.sub( 1 ) ).sub( 0.5 ) );
+		colorResult.addAssign( sampleColor( uvs.add( offset ) ) );
+
+	} );
+
+	colorResult.divAssign( fSamples );
+
+	return colorResult;
+
+} );

+ 5 - 1
src/nodes/display/PassNode.js

@@ -27,7 +27,7 @@ class PassTextureNode extends TextureNode {
 
 	setup( builder ) {
 
-		this.passNode.build( builder );
+		if ( builder.object.isQuadMesh ) this.passNode.build( builder );
 
 		return super.setup( builder );
 
@@ -210,6 +210,7 @@ class PassNode extends TempNode {
 		if ( textureNode === undefined ) {
 
 			this._textureNodes[ name ] = textureNode = nodeObject( new PassMultipleTextureNode( this, name ) );
+			this._textureNodes[ name ].updateTexture();
 
 		}
 
@@ -223,7 +224,10 @@ class PassNode extends TempNode {
 
 		if ( textureNode === undefined ) {
 
+			if ( this._textureNodes[ name ] === undefined ) this.getTextureNode( name );
+
 			this._previousTextureNodes[ name ] = textureNode = nodeObject( new PassMultipleTextureNode( this, name, true ) );
+			this._previousTextureNodes[ name ].updateTexture();
 
 		}
 

+ 3 - 0
src/nodes/utils/ReflectorNode.js

@@ -214,11 +214,14 @@ class ReflectorNode extends TextureNode {
 		material.visible = false;
 
 		const currentRenderTarget = renderer.getRenderTarget();
+		const currentMRT = renderer.getMRT();
 
+		renderer.setMRT( null );
 		renderer.setRenderTarget( renderTarget );
 
 		renderer.render( scene, virtualCamera );
 
+		renderer.setMRT( currentMRT );
 		renderer.setRenderTarget( currentRenderTarget );
 
 		material.visible = true;

+ 2 - 0
src/renderers/common/QuadMesh.js

@@ -34,6 +34,8 @@ class QuadMesh extends Mesh {
 
 		this.camera = _camera;
 
+		this.isQuadMesh = true;
+
 	}
 
 	renderAsync( renderer ) {

+ 1 - 1
src/renderers/webgpu/WebGPUBackend.js

@@ -345,7 +345,7 @@ class WebGPUBackend extends Backend {
 
 				if ( renderContext.clearColor ) {
 
-					colorAttachment.clearValue = renderContext.clearColorValue;
+					colorAttachment.clearValue = i === 0 ? renderContext.clearColorValue : { r: 0, g: 0, b: 0, a: 1 };
 					colorAttachment.loadOp = GPULoadOp.Clear;
 					colorAttachment.storeOp = GPUStoreOp.Store;
 

粤ICP备19079148号