diff --git a/examples/files.json b/examples/files.json
index cf9b42b5120fd9..d9695141f9c514 100644
--- a/examples/files.json
+++ b/examples/files.json
@@ -383,6 +383,7 @@
"webgpu_postprocessing_pixel",
"webgpu_postprocessing_fxaa",
"webgpu_postprocessing_masking",
+ "webgpu_postprocessing_motion_blur",
"webgpu_postprocessing_sobel",
"webgpu_postprocessing_transition",
"webgpu_postprocessing",
diff --git a/examples/screenshots/webgpu_postprocessing_motion_blur.jpg b/examples/screenshots/webgpu_postprocessing_motion_blur.jpg
new file mode 100644
index 00000000000000..56ca1afabde396
Binary files /dev/null and b/examples/screenshots/webgpu_postprocessing_motion_blur.jpg differ
diff --git a/examples/webgpu_postprocessing_bloom_selective.html b/examples/webgpu_postprocessing_bloom_selective.html
index 17ea5f4659ff7d..73de7bc62e0faf 100644
--- a/examples/webgpu_postprocessing_bloom_selective.html
+++ b/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;
}
diff --git a/examples/webgpu_postprocessing_motion_blur.html b/examples/webgpu_postprocessing_motion_blur.html
new file mode 100644
index 00000000000000..533e1d0839efcf
--- /dev/null
+++ b/examples/webgpu_postprocessing_motion_blur.html
@@ -0,0 +1,244 @@
+
+
+
+ three.js webgpu - motion blur
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/nodes/Nodes.js b/src/nodes/Nodes.js
index 9e12906e29057f..1d094bc7eb3a7d 100644
--- a/src/nodes/Nodes.js
+++ b/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';
diff --git a/src/nodes/accessors/PositionNode.js b/src/nodes/accessors/PositionNode.js
index f77d84563b01af..2dc268853de244 100644
--- a/src/nodes/accessors/PositionNode.js
+++ b/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' );
diff --git a/src/nodes/accessors/SkinningNode.js b/src/nodes/accessors/SkinningNode.js
index a32c2de9b07313..71fc9c3bd17611 100644
--- a/src/nodes/accessors/SkinningNode.js
+++ b/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();
}
diff --git a/src/nodes/accessors/VelocityNode.js b/src/nodes/accessors/VelocityNode.js
new file mode 100644
index 00000000000000..89358b444db389
--- /dev/null
+++ b/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 );
diff --git a/src/nodes/core/MRTNode.js b/src/nodes/core/MRTNode.js
index 56e63577f8b36e..b5d66e8e2c6ec2 100644
--- a/src/nodes/core/MRTNode.js
+++ b/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 ];
diff --git a/src/nodes/display/MotionBlurNode.js b/src/nodes/display/MotionBlurNode.js
new file mode 100644
index 00000000000000..b4666088423f54
--- /dev/null
+++ b/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;
+
+} );
diff --git a/src/nodes/display/PassNode.js b/src/nodes/display/PassNode.js
index 6b768794e77911..1c07e777676b27 100644
--- a/src/nodes/display/PassNode.js
+++ b/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();
}
diff --git a/src/nodes/utils/ReflectorNode.js b/src/nodes/utils/ReflectorNode.js
index 68c13fd68477c6..31ab3136124be6 100644
--- a/src/nodes/utils/ReflectorNode.js
+++ b/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;
diff --git a/src/renderers/common/QuadMesh.js b/src/renderers/common/QuadMesh.js
index 34822ebe695ddf..1274e4ba75276f 100644
--- a/src/renderers/common/QuadMesh.js
+++ b/src/renderers/common/QuadMesh.js
@@ -34,6 +34,8 @@ class QuadMesh extends Mesh {
this.camera = _camera;
+ this.isQuadMesh = true;
+
}
renderAsync( renderer ) {
diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js
index 58c66fc32171c9..0c864c4e3717eb 100644
--- a/src/renderers/webgpu/WebGPUBackend.js
+++ b/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;