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 + + + + + + +
+ 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;