| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023 |
- import { EPSILON, Fn, If, abs, convertToTexture, dFdx, dFdy, dot, exp, float, floor, fwidth, getViewPosition, ivec2, luminance, max, min, mix, nodeObject, normalize, passTexture, screenCoordinate, select, smoothstep, sqrt, struct, texture, textureLoad, uniform, unpackRGBToNormal, uv, vec2, vec3, vec4, velocity } from 'three/tsl';
- import { DepthTexture, HalfFloatType, Matrix4, NodeMaterial, NodeUpdateType, QuadMesh, RenderTarget, RendererUtils, TempNode, Vector2, Vector3 } from 'three/webgpu';
- import { ENV_RAY_LENGTH, ENV_RAY_LENGTH_THRESHOLD } from '../utils/SpecularHelpers.js';
- // Reprojection helpers
- /**
- * Maps a resolve (screen) texel to the corresponding beauty-input texel when resolutions differ.
- *
- * @tsl
- */
- const beautyTexelFromScreen = Fn( ( [ screenTexel, beautySize, resolveSize ] ) => {
- return ivec2( floor( vec2( screenTexel ).mul( beautySize ).div( resolveSize ) ) );
- } ).setLayout( {
- name: 'beautyTexelFromScreen',
- type: 'ivec2',
- inputs: [
- { name: 'screenTexel', type: 'ivec2' },
- { name: 'beautySize', type: 'vec2' },
- { name: 'resolveSize', type: 'vec2' }
- ]
- } );
- /**
- * Projects a world-space position into previous-frame UV coordinates.
- *
- * @tsl
- */
- const projectWorldToUV = Fn( ( [ worldPos, previousViewMatrix, previousProjectionMatrix ] ) => {
- const resultUV = vec2( - 1 ).toVar();
- const viewSpace = previousViewMatrix.mul( vec4( worldPos, 1.0 ) );
- const clipSpace = previousProjectionMatrix.mul( viewSpace ).toVar();
- const clipW = clipSpace.w.toVar();
- If( abs( clipW ).greaterThan( float( 1e-5 ) ), () => {
- const ndc = clipSpace.xyz.div( clipW );
- resultUV.assign( ndc.xy.mul( 0.5 ).add( 0.5 ) );
- resultUV.y.assign( resultUV.y.oneMinus() );
- } );
- return resultUV;
- } ).setLayout( {
- name: 'projectWorldToUV',
- type: 'vec2',
- inputs: [
- { name: 'worldPos', type: 'vec3' },
- { name: 'previousViewMatrix', type: 'mat4' },
- { name: 'previousProjectionMatrix', type: 'mat4' }
- ]
- } );
- // YCoCg variance clipping
- /**
- * @param {Node<vec3>} c
- * @returns {Node<vec3>}
- */
- const rgbToYCoCg = ( c ) => vec3(
- dot( c, vec3( 0.25, 0.5, 0.25 ) ),
- dot( c, vec3( 0.5, 0.0, - 0.5 ) ),
- dot( c, vec3( - 0.25, 0.5, - 0.25 ) )
- );
- /**
- * @param {Node<vec3>} c
- * @returns {Node<vec3>}
- */
- const ycocgToRGB = ( c ) => vec3(
- c.x.add( c.y ).sub( c.z ),
- c.x.add( c.z ),
- c.x.sub( c.y ).sub( c.z )
- );
- const VARIANCE_CLIP_LUMA_SCALE = 10;
- /**
- * Inverse-luminance compression for HDR variance clipping (Karis-style).
- * Bright samples contribute less to neighbourhood moments so sun pixels do not
- * inflate the YCoCg AABB and cause aggressive clipping flicker.
- *
- * @param {Node<vec3>} rgb
- * @param {Node<float>} flickerSuppression
- * @returns {Node<vec3>}
- */
- const dampenForVarianceClip = ( rgb, flickerSuppression ) => {
- const scale = luminance( rgb ).mul( flickerSuppression ).mul( VARIANCE_CLIP_LUMA_SCALE ).add( 1 );
- return rgb.div( scale );
- };
- /**
- * Clips the history sample to the neighbourhood AABB by projecting it toward the box centre.
- * Reference: https://github.com/playdeadgames/temporal
- *
- * @tsl
- */
- const clipToAABB = Fn( ( [ history, boxMin, boxMax ] ) => {
- const pClip = boxMax.add( boxMin ).mul( 0.5 );
- const eClip = boxMax.sub( boxMin ).mul( 0.5 ).add( 1e-7 );
- const vClip = history.sub( pClip );
- const vUnit = vClip.div( eClip );
- const absUnit = vUnit.abs();
- const maxUnit = max( absUnit.x, absUnit.y, absUnit.z );
- return maxUnit.greaterThan( 1 ).select( pClip.add( vClip.div( maxUnit ) ), history );
- } ).setLayout( {
- name: 'clipToAABB',
- type: 'vec3',
- inputs: [
- { name: 'history', type: 'vec3' },
- { name: 'boxMin', type: 'vec3' },
- { name: 'boxMax', type: 'vec3' }
- ]
- } );
- const neighborhoodStruct = struct( {
- mean: 'vec3',
- stdColor: 'vec3',
- rayLength: 'float',
- envProbability: 'float',
- stdDevRayLength: 'float'
- } );
- /**
- * Single 3×3 neighbourhood pass over the beauty buffer. One textureLoad per tap feeds both the
- * YCoCg variance-clipping box (colour) and the SSR ray-length statistics (alpha), which previously
- * required two separate 3×3 fetches of the same texture.
- *
- * Sampling is done on the beauty-texel grid (`beautyTexel + offset`), so the taps are distinct
- * source texels even when the beauty buffer is lower resolution than the resolve pass (upscaling).
- *
- * @tsl
- */
- const collectNeighborhood = Fn( ( [ beautyTexture, beautyTexel, inputColor, flickerSuppression ] ) => {
- const offsets = [
- [ - 1, - 1 ],
- [ - 1, 1 ],
- [ 1, - 1 ],
- [ 1, 1 ],
- [ 1, 0 ],
- [ 0, - 1 ],
- [ 0, 1 ],
- [ - 1, 0 ],
- ];
- // Colour moments (YCoCg) — centre reuses the already-fetched inputColor.
- const center = rgbToYCoCg( dampenForVarianceClip( inputColor.rgb, flickerSuppression ) );
- const moment1 = center.toVar();
- const moment2 = center.pow2().toVar();
- // Ray-length statistics (Welford) over screen-space hits only.
- const rayLengthSum = float( 0 ).toVar();
- const rayLengthCount = float( 0 ).toVar();
- const meanRayLength = float( 0 ).toVar();
- const m2RayLength = float( 0 ).toVar();
- const accumulateRayLength = ( alpha ) => {
- If( alpha.lessThan( ENV_RAY_LENGTH_THRESHOLD ), () => {
- rayLengthSum.addAssign( alpha );
- rayLengthCount.addAssign( 1 );
- const delta = alpha.sub( meanRayLength ).toVar();
- meanRayLength.addAssign( delta.div( rayLengthCount ) );
- m2RayLength.addAssign( delta.mul( alpha.sub( meanRayLength ) ) );
- } );
- };
- accumulateRayLength( inputColor.a );
- for ( const [ x, y ] of offsets ) {
- const neighbor = textureLoad( beautyTexture, beautyTexel.add( ivec2( x, y ) ) ).max( 0 ).toVar();
- const c = rgbToYCoCg( dampenForVarianceClip( neighbor.rgb, flickerSuppression ) );
- moment1.addAssign( c );
- moment2.addAssign( c.pow2() );
- accumulateRayLength( neighbor.a );
- }
- const N = float( offsets.length + 1 );
- const mean = moment1.div( N );
- const stdColor = moment2.div( N ).sub( mean.pow2() ).max( 0 ).sqrt();
- // Continuous environment probability: fraction of the 3×3 neighbourhood that missed in screen space
- // and fell back to env (0 = all hits, 1 = all env), for smooth reflection/environment transitions.
- const envProbability = rayLengthCount.div( float( 9 ) ).oneMinus();
- const rayLength = rayLengthCount.lessThan( 0.5 ).select( float( ENV_RAY_LENGTH ), rayLengthSum.div( max( rayLengthCount, float( 1e-4 ) ) ) );
- const stdDevRayLength = sqrt( m2RayLength.div( max( rayLengthCount, float( 1.0 ) ) ) ).max( 1e-3 );
- return neighborhoodStruct( mean, stdColor, rayLength, envProbability, stdDevRayLength );
- } );
- /**
- * Variance clipping in YCoCg space (Salvi, GDC 2016). Uses the colour moments gathered by
- * {@link collectNeighborhood}; `gamma` widens the AABB and is kept out of the gather so the
- * neighbourhood pass stays independent of the per-pixel motion factor.
- *
- * @tsl
- */
- const applyVarianceClipping = Fn( ( [ historyColor, mean, stdColor, gamma, flickerSuppression ] ) => {
- const stddev = stdColor.mul( gamma );
- const boxMin = mean.sub( stddev );
- const boxMax = mean.add( stddev );
- const historyRGB = historyColor.rgb.toVar();
- const historyScale = luminance( historyRGB ).mul( flickerSuppression ).mul( VARIANCE_CLIP_LUMA_SCALE ).add( 1 );
- const clipped = clipToAABB( rgbToYCoCg( historyRGB.div( historyScale ) ), boxMin, boxMax );
- return ycocgToRGB( clipped ).mul( historyScale );
- } );
- // History sampling
- const bilinearTapStruct = struct( { color: 'vec4', weight: 'float', confidence: 'float' } );
- const historyResultStruct = struct( { color: 'vec4', tapConfidence: 'float', minConfidence: 'float' } );
- /**
- * Single bilinear history tap with plane-distance and normal confidence.
- *
- * @tsl
- */
- const sampleBilinearTap = Fn( ( [
- historyTexture,
- previousDepthNode,
- previousNormalNode,
- resolution,
- previousProjectionMatrixInverse,
- previousCameraWorldMatrix,
- previousCameraViewMatrix,
- tapCoord,
- bilinearWeight,
- worldPosition,
- worldNormal
- ] ) => {
- const color = textureLoad( historyTexture, tapCoord ).max( 0 );
- const reprojDepth = textureLoad( previousDepthNode, tapCoord ).r;
- const reprojViewPos = getViewPosition( vec2( tapCoord ).add( 0.5 ).div( resolution ), reprojDepth, previousProjectionMatrixInverse );
- const reprojWorldPos = previousCameraWorldMatrix.mul( vec4( reprojViewPos, 1.0 ) ).xyz;
- const reprojWorldNorm = unpackRGBToNormal( textureLoad( previousNormalNode, tapCoord ).rgb ).transformDirection( previousCameraViewMatrix );
- const planeDiff = abs( dot( reprojWorldPos.sub( worldPosition ), worldNormal ) ).toVar();
- planeDiff.divAssign( abs( reprojViewPos.z ) );
- const normalConfidence = smoothstep( 0.95, 0.999, reprojWorldNorm.dot( worldNormal ) );
- const confidence = smoothstep( 0, 0.01, planeDiff ).oneMinus().mul( normalConfidence );
- const weight = bilinearWeight.mul( confidence );
- return bilinearTapStruct( color.mul( weight ), weight, confidence );
- } );
- /**
- * @param {Object} ctx - Shared {@link sampleBilinearTap} inputs plus `reprojICoord`.
- * @param {Node<ivec2>} tapOffset
- * @param {Node<float>} bilinearWeight
- */
- function bilinearHistoryTap( ctx, tapOffset, bilinearWeight ) {
- return sampleBilinearTap(
- ctx.historyTexture,
- ctx.previousDepthNode,
- ctx.previousNormalNode,
- ctx.resolution,
- ctx.previousProjectionMatrixInverse,
- ctx.previousCameraWorldMatrix,
- ctx.previousCameraViewMatrix,
- ctx.reprojICoord.add( tapOffset ),
- bilinearWeight,
- ctx.worldPosition,
- ctx.worldNormal
- );
- }
- /**
- * Geometrically-weighted 4-tap bilinear history sample.
- *
- * @tsl
- */
- const sampleHistory4Tap = Fn( ( [
- historyTexture,
- previousDepthNode,
- previousNormalNode,
- resolution,
- previousProjectionMatrixInverse,
- previousCameraWorldMatrix,
- previousCameraViewMatrix,
- reprojUV,
- worldPosition,
- worldNormal,
- inputColor
- ] ) => {
- const reprojPixelCoord = reprojUV.mul( resolution ).sub( 0.5 ).toVar();
- const reprojICoord = ivec2( floor( reprojPixelCoord ) );
- const fCoord = reprojPixelCoord.fract();
- const fx = fCoord.x;
- const fy = fCoord.y;
- const f00 = float( 1 ).sub( fx ).mul( float( 1 ).sub( fy ) );
- const f10 = fx.mul( float( 1 ).sub( fy ) );
- const f01 = float( 1 ).sub( fx ).mul( fy );
- const f11 = fx.mul( fy );
- const tapCtx = {
- historyTexture,
- previousDepthNode,
- previousNormalNode,
- resolution,
- previousProjectionMatrixInverse,
- previousCameraWorldMatrix,
- previousCameraViewMatrix,
- reprojICoord,
- worldPosition,
- worldNormal
- };
- const tap00 = bilinearHistoryTap( tapCtx, ivec2( 0, 0 ), f00 );
- const tap10 = bilinearHistoryTap( tapCtx, ivec2( 1, 0 ), f10 );
- const tap01 = bilinearHistoryTap( tapCtx, ivec2( 0, 1 ), f01 );
- const tap11 = bilinearHistoryTap( tapCtx, ivec2( 1, 1 ), f11 );
- const colorSum = tap00.get( 'color' ).add( tap10.get( 'color' ) ).add( tap01.get( 'color' ) ).add( tap11.get( 'color' ) );
- const weightSum = tap00.get( 'weight' ).add( tap10.get( 'weight' ) ).add( tap01.get( 'weight' ) ).add( tap11.get( 'weight' ) );
- const maxConf = max( max( tap00.get( 'confidence' ), tap10.get( 'confidence' ) ), max( tap01.get( 'confidence' ), tap11.get( 'confidence' ) ) );
- const minConf = min( min( tap00.get( 'confidence' ), tap10.get( 'confidence' ) ), min( tap01.get( 'confidence' ), tap11.get( 'confidence' ) ) );
- return historyResultStruct(
- select( weightSum.greaterThan( 0.01 ), colorSum.div( weightSum ), vec4( inputColor.rgb, float( 1 ) ) ),
- maxConf,
- minConf
- );
- } );
- // Diffuse reprojection
- /**
- * Reprojection-stretch confidence — detects history magnification (surface stretching).
- *
- * Differentiates the per-pixel history UV with hardware screen-space derivatives to form the
- * reprojection Jacobian `J = ∂(historyPixel)/∂(screenPixel)`, then returns its **minimum
- * singular value**, clamped to `[0,1]`.
- *
- * `σ_min < 1` means the most-stretched axis magnifies history — a few history pixels are smeared
- * over many current pixels (e.g. a surface seen at grazing in the previous frame, face-on now), so
- * history is undersampled and its confidence should be reduced. `σ_min ≥ 1` (history minified) is
- * safe and clamps to 1. Using the minimum singular value rather than the Jacobian determinant
- * catches anisotropic 1-D stretch that an area-only measure would smear out.
- *
- * Works for any reprojection (surface-velocity or parallax hit-point) since it differentiates the
- * final history UV, so the same factor applies to both the diffuse and specular paths.
- *
- * @tsl
- */
- const reprojectionStretchConfidence = Fn( ( [ historyUV, resolution ] ) => {
- // Jacobian columns in pixels: how the history sample position moves per screen pixel.
- const jx = dFdx( historyUV ).mul( resolution ).toVar();
- const jy = dFdy( historyUV ).mul( resolution ).toVar();
- // Singular values of the 2×2 J are sqrt( eigenvalues of JᵀJ ), with
- // trace( JᵀJ ) = ‖J‖²_F and det( JᵀJ ) = det( J )².
- const det = jx.x.mul( jy.y ).sub( jx.y.mul( jy.x ) );
- const fro2 = dot( jx, jx ).add( dot( jy, jy ) );
- const disc = fro2.mul( fro2 ).mul( 0.25 ).sub( det.mul( det ) ).max( 0 ).sqrt();
- const sigMin = fro2.mul( 0.5 ).sub( disc ).max( 0 ).sqrt();
- return sigMin.saturate();
- } );
- // Specular reprojection
- /**
- * Parallax-corrected hit-point reprojection into previous-frame UVs.
- *
- * @tsl
- */
- const reprojectHitPoint = Fn( ( [
- rayOrig,
- rayLength,
- cameraWorldPosition,
- previousViewMatrix,
- previousProjectionMatrix
- ] ) => {
- const cameraRay = normalize( rayOrig.sub( cameraWorldPosition ) ).toVar();
- const parallaxHitPoint = rayOrig.add( cameraRay.mul( rayLength ) );
- return projectWorldToUV( parallaxHitPoint, previousViewMatrix, previousProjectionMatrix );
- } );
- /**
- * Converts screen-space velocity (NDC derivative) to a UV reprojection offset.
- *
- * @tsl
- */
- const velocityToUVOffset = Fn( ( [ velocity ] ) => {
- return velocity.mul( vec2( 0.5, - 0.5 ) );
- } ).setLayout( {
- name: 'velocityToUVOffset',
- type: 'vec2',
- inputs: [ { name: 'velocity', type: 'vec2' } ]
- } );
- /**
- * Current and previous-frame camera matrices for temporal reprojection passes.
- *
- * @param {Camera} camera
- */
- function bindTemporalCameraUniforms( camera ) {
- const worldMatrix = uniform( new Matrix4().copy( camera.matrixWorld ) );
- const viewMatrix = uniform( new Matrix4().copy( camera.matrixWorldInverse ) );
- const projectionMatrix = uniform( new Matrix4().copy( camera.projectionMatrix ) );
- const projectionMatrixInverse = uniform( new Matrix4().copy( camera.projectionMatrixInverse ) );
- const worldPosition = uniform( new Vector3().copy( camera.position ) );
- const previousWorldMatrix = uniform( new Matrix4().copy( camera.matrixWorld ) );
- const previousViewMatrix = uniform( new Matrix4().copy( camera.matrixWorldInverse ) );
- const previousProjectionMatrix = uniform( new Matrix4().copy( camera.projectionMatrix ) );
- const previousProjectionMatrixInverse = uniform( new Matrix4().copy( camera.projectionMatrixInverse ) );
- /**
- * @param {Camera} cam
- */
- function updateFromCamera( cam ) {
- previousWorldMatrix.value.copy( worldMatrix.value );
- previousViewMatrix.value.copy( viewMatrix.value );
- previousProjectionMatrix.value.copy( projectionMatrix.value );
- previousProjectionMatrixInverse.value.copy( projectionMatrixInverse.value );
- worldMatrix.value.copy( cam.matrixWorld );
- viewMatrix.value.copy( cam.matrixWorldInverse );
- projectionMatrix.value.copy( cam.projectionMatrix );
- projectionMatrixInverse.value.copy( cam.projectionMatrixInverse );
- worldPosition.value.copy( cam.position );
- }
- return {
- worldMatrix,
- viewMatrix,
- projectionMatrix,
- projectionMatrixInverse,
- worldPosition,
- previousWorldMatrix,
- previousViewMatrix,
- previousProjectionMatrix,
- previousProjectionMatrixInverse,
- updateFromCamera
- };
- }
- const _quadMesh = /*@__PURE__*/ new QuadMesh();
- const _size = /*@__PURE__*/ new Vector2();
- let _rendererState;
- const DEFAULT_MAX_VELOCITY_LENGTH = 128;
- const VARIANCE_GAMMA_MIN = 0.5;
- const VARIANCE_GAMMA_MAX = 1;
- /**
- * @typedef {'diffuse' | 'specular'} TemporalReprojectMode
- */
- /**
- * @typedef {Object} TemporalReprojectNodeOptions
- * @property {TemporalReprojectMode} [mode='diffuse'] - `diffuse` for SSGI/scene colour; `specular` for SSR reflections.
- * @property {boolean} [hitPointReprojection] - Parallax hit-point reprojection (specular mode only). Defaults to `true` in specular mode.
- * @property {boolean} [accumulate=false] - When `true`, history is stored in this pass (classic temporal resolve). When `false`,
- * use {@link TemporalReprojectNode#setHistoryTexture} to read history from another pass (e.g. denoise output).
- */
- /**
- * Temporal reprojection pass for denoising screen-space effects (SSGI, SSR, etc.).
- *
- * Both modes share geometrically-weighted 4-tap bilinear history sampling and YCoCg variance clipping.
- * Surface velocity reprojection is always sampled first. Specular mode then blends in
- * hit-point parallax history on top of that surface result.
- * Diffuse mode applies velocity-field divergence to detect surface stretching.
- *
- * Unlike jitter-based TAA/TAAU, this node does not apply camera sub-pixel jitter — it only
- * reprojects and accumulates history using motion vectors.
- *
- * References:
- * - {@link https://alextardif.com/TAA.html}
- * - {@link https://www.elopezr.com/temporal-aa-and-the-quest-for-the-holy-trail/}
- *
- * @augments TempNode
- * @three_import import { temporalReproject } from 'three/addons/tsl/display/TemporalReprojectNode.js';
- */
- class TemporalReprojectNode extends TempNode {
- static get type() {
- return 'TemporalReprojectNode';
- }
- /**
- * @param {TextureNode} beautyNode
- * @param {TextureNode} depthNode
- * @param {TextureNode} normalNode
- * @param {TextureNode} velocityNode
- * @param {Camera} camera
- * @param {TemporalReprojectNodeOptions} [options]
- */
- constructor( beautyNode, depthNode, normalNode, velocityNode, camera, options = {} ) {
- super( 'vec4' );
- const {
- mode = 'diffuse',
- hitPointReprojection = mode === 'specular',
- accumulate = false
- } = options;
- if ( mode !== 'specular' && mode !== 'diffuse' ) {
- throw new Error( 'TemporalReprojectNode: `mode` must be `diffuse` or `specular`.' );
- }
- this.isTemporalReprojectNode = true;
- this.updateBeforeType = NodeUpdateType.FRAME;
- this.beautyNode = beautyNode;
- this.depthNode = depthNode;
- this.normalNode = normalNode;
- this.velocityNode = velocityNode;
- this.camera = camera;
- /**
- * @type {TemporalReprojectMode}
- */
- this.mode = mode;
- /**
- * When `true`, resolve output is copied into the internal history buffer each frame.
- * When `false`, history is supplied externally via {@link TemporalReprojectNode#setHistoryTexture}.
- *
- * @type {boolean}
- */
- this.accumulate = accumulate;
- this.maxVelocityLength = DEFAULT_MAX_VELOCITY_LENGTH;
- this._resolution = uniform( new Vector2() );
- this._cameraUniforms = bindTemporalCameraUniforms( camera );
- this.maxFrames = uniform( 32 );
- this.hitPointReprojection = uniform( hitPointReprojection, 'bool' );
- this.clampIntensity = uniform( 1 );
- this.flickerSuppression = uniform( 1 );
- this._historyRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType, depthTexture: new DepthTexture() } );
- this._historyRenderTarget.texture.name = 'TemporalReprojectNode.history';
- this._historyTextureNode = texture( this._historyRenderTarget.texture );
- this._resolveRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
- this._resolveRenderTarget.texture.name = 'TemporalReprojectNode.resolve';
- this._resolveMaterial = new NodeMaterial();
- this._resolveMaterial.name = 'TemporalReproject.resolve';
- this._seedMaterial = new NodeMaterial();
- this._seedMaterial.name = 'TemporalReproject.seed';
- this._textureNode = passTexture( this, this._resolveRenderTarget.texture );
- this._originalProjectionMatrix = new Matrix4();
- this._placeholderPreviousDepthTexture = new DepthTexture( 1, 1 );
- this._previousDepthNode = texture( this._placeholderPreviousDepthTexture );
- this._previousNormalTexture = normalNode.value.clone();
- this._previousNormalNode = texture( this._previousNormalTexture );
- this._needsPostProcessingSync = false;
- this._externalHistoryTexture = null;
- this._syncHistoryTextureBinding();
- }
- getTextureNode() {
- return this._textureNode;
- }
- setSize( width, height ) {
- if ( width === null || height === null ) return;
- this._historyRenderTarget.setSize( width, height );
- this._resolveRenderTarget.setSize( width, height );
- this._resolution.value.set( width, height );
- }
- setViewOffset() {
- this.camera.updateProjectionMatrix();
- this._originalProjectionMatrix.copy( this.camera.projectionMatrix );
- velocity.setProjectionMatrix( this._originalProjectionMatrix );
- }
- clearViewOffset() {
- velocity.setProjectionMatrix( null );
- }
- updateBefore( frame ) {
- const { renderer } = frame;
- this._cameraUniforms.updateFromCamera( this.camera );
- const drawingBufferSize = renderer.getDrawingBufferSize( _size );
- const width = drawingBufferSize.width;
- const height = drawingBufferSize.height;
- if ( this._needsPostProcessingSync === true ) {
- this.setViewOffset();
- this._needsPostProcessingSync = false;
- }
- _rendererState = RendererUtils.resetRendererState( renderer, _rendererState );
- const needsRestart = this._historyRenderTarget.width !== width || this._historyRenderTarget.height !== height;
- this.setSize( width, height );
- let historySwappedForRestart = false;
- if ( needsRestart === true ) {
- renderer.initRenderTarget( this._historyRenderTarget );
- renderer.initRenderTarget( this._resolveRenderTarget );
- this._previousNormalTexture.dispose();
- this._previousNormalTexture = this.normalNode.value.clone();
- this._previousNormalNode.value = this._previousNormalTexture;
- // External history (e.g. denoise feedback) is stale at the old resolution — use
- // freshly seeded internal history for this frame instead.
- if ( this.accumulate === false && this._externalHistoryTexture !== null ) {
- this._historyTextureNode.value = this._historyRenderTarget.texture;
- historySwappedForRestart = true;
- }
- renderer.setRenderTarget( this._historyRenderTarget );
- _quadMesh.material = this._seedMaterial;
- _quadMesh.name = 'TemporalReproject.seed';
- _quadMesh.render( renderer );
- renderer.setRenderTarget( null );
- }
- renderer.setRenderTarget( this._resolveRenderTarget );
- _quadMesh.material = this._resolveMaterial;
- _quadMesh.name = 'TemporalReproject';
- _quadMesh.render( renderer );
- renderer.setRenderTarget( null );
- if ( historySwappedForRestart === true ) {
- this._syncHistoryTextureBinding();
- } else if ( this.accumulate === true ) {
- renderer.copyTextureToTexture( this._resolveRenderTarget.texture, this._historyRenderTarget.texture );
- }
- const currentDepth = this.depthNode.value;
- const srcW = currentDepth.image !== null && currentDepth.image !== undefined ? currentDepth.image.width : 0;
- const srcH = currentDepth.image !== null && currentDepth.image !== undefined ? currentDepth.image.height : 0;
- if ( srcW > 0 && srcH > 0 ) {
- renderer.copyTextureToTexture( currentDepth, this._historyRenderTarget.depthTexture );
- renderer.copyTextureToTexture( this.normalNode.value, this._previousNormalTexture );
- this._previousDepthNode.value = this._historyRenderTarget.depthTexture;
- }
- RendererUtils.restoreRendererState( renderer, _rendererState );
- }
- setup( builder ) {
- const renderPipeline = builder.context.renderPipeline;
- if ( renderPipeline ) {
- this._needsPostProcessingSync = true;
- renderPipeline.context.onBeforeRenderPipeline = () => {
- this.setViewOffset();
- };
- renderPipeline.context.onAfterRenderPipeline = () => {
- this.clearViewOffset();
- };
- }
- this._resolveMaterial.fragmentNode = this._buildResolve( builder );
- this._resolveMaterial.needsUpdate = true;
- this._buildSeed( builder );
- return this._textureNode;
- }
- _buildSeed( builder ) {
- const seed = Fn( () => {
- const screenTexel = ivec2( floor( screenCoordinate.xy.sub( 0.5 ) ) );
- const beautySize = this.beautyNode.size();
- const beautyTexel = beautyTexelFromScreen( screenTexel, beautySize, this._resolution );
- return textureLoad( this.beautyNode, beautyTexel ).max( 0 );
- } );
- this._seedMaterial.fragmentNode = seed().context( builder.getSharedContext() );
- this._seedMaterial.needsUpdate = true;
- }
- _buildResolve( builder ) {
- const isSpecular = this.mode === 'specular';
- const cameraUniforms = this._cameraUniforms;
- const resolve = Fn( () => {
- const uvNode = uv();
- const screenTexel = ivec2( floor( screenCoordinate.xy.sub( 0.5 ) ) );
- const depth = textureLoad( this.depthNode, screenTexel ).r.toVar();
- depth.greaterThanEqual( 1.0 ).discard();
- const beautySize = this.beautyNode.size();
- const beautyTexel = beautyTexelFromScreen( screenTexel, beautySize, this._resolution );
- const inputColor = textureLoad( this.beautyNode, beautyTexel ).max( 0 ).toVar();
- const viewNormal = unpackRGBToNormal( textureLoad( this.normalNode, screenTexel ).rgb ).toVar();
- // Shared 3×3 beauty fetch: feeds both the variance-clip box and the SSR ray-length stats.
- const neighborhood = collectNeighborhood( this.beautyNode, beautyTexel, inputColor, this.flickerSuppression );
- const worldNormal = viewNormal.transformDirection( cameraUniforms.viewMatrix ).toVar();
- const viewPosition = getViewPosition( uvNode, depth, cameraUniforms.projectionMatrixInverse ).toVar();
- const worldPosition = cameraUniforms.worldMatrix.mul( vec4( viewPosition, 1.0 ) ).xyz.toVar();
- const sampleHistory = ( reprojUV ) => sampleHistory4Tap(
- this._historyTextureNode,
- this._previousDepthNode,
- this._previousNormalNode,
- this._resolution,
- cameraUniforms.previousProjectionMatrixInverse,
- cameraUniforms.previousWorldMatrix,
- cameraUniforms.previousViewMatrix,
- reprojUV,
- worldPosition,
- worldNormal,
- inputColor.rgb
- );
- // Surface-velocity reprojection — the base history for both modes. `historyUV` is
- // reused below for the stretch guard, so it is computed once here.
- const velocityOff = velocityToUVOffset( textureLoad( this.velocityNode, screenTexel ).xy ).toVar();
- const motionFactor = velocityOff.mul( this._resolution ).length().div( float( this.maxVelocityLength ) ).saturate();
- const historyUV = uvNode.sub( velocityOff ).toVar();
- const surf = sampleHistory( historyUV );
- const historyColor = surf.get( 'color' ).toVar();
- const totalConfidence = float( 1 ).toVar();
- const historyTrust = float( 0 ).toVar();
- // Specular: blend parallax hit-point history on top of the surface result. Returns the resolved
- // color (rgb from the blend, alpha from the surface tap), its confidence, and the hit-vs-surface trust.
- const resolveSpecularHistory = () => {
- const surfValid = historyUV.x.greaterThanEqual( 0 ).and( historyUV.x.lessThanEqual( 1 ) )
- .and( historyUV.y.greaterThanEqual( 0 ) ).and( historyUV.y.lessThanEqual( 1 ) );
- const historyUV_hit = reprojectHitPoint(
- worldPosition,
- neighborhood.get( 'rayLength' ),
- cameraUniforms.worldPosition,
- cameraUniforms.previousViewMatrix,
- cameraUniforms.previousProjectionMatrix
- ).toVar();
- const hitValid = historyUV_hit.x.greaterThanEqual( 0 ).and( historyUV_hit.x.lessThanEqual( 1 ) )
- .and( historyUV_hit.y.greaterThanEqual( 0 ) ).and( historyUV_hit.y.lessThanEqual( 1 ) )
- .and( this.hitPointReprojection );
- const hit = sampleHistory( historyUV_hit );
- const hcHit = hit.get( 'color' ).rgb.max( 0 );
- const hcSurf = surf.get( 'color' ).rgb.max( 0 );
- const confHit = hitValid.select( hit.get( 'tapConfidence' ), float( 0 ) );
- const confSurf = surfValid.select( surf.get( 'tapConfidence' ), float( 0 ) );
- const minConfHit = hit.get( 'minConfidence' );
- const reflectionEdgeFactor = neighborhood.get( 'stdDevRayLength' );
- reflectionEdgeFactor.assign( reflectionEdgeFactor.mul( motionFactor.mul( 100 ).min( 1 ) ).mul( 3.5 ).min( 1 ).oneMinus() );
- const curvatureFactor = fwidth( worldNormal.xyz ).length().mul( 50 ).clamp();
- const envProbability = neighborhood.get( 'envProbability' );
- const wHitRaw = minConfHit
- .mul( reflectionEdgeFactor )
- .mul( curvatureFactor.oneMinus() )
- .mul( confHit ).toConst();
- const wHit = wHitRaw.mul( envProbability.pow2().oneMinus() );
- const wSurf = wHit.oneMinus().mul( confSurf );
- const wSum = max( wHit.add( wSurf ), float( EPSILON ) );
- const color = vec4(
- hcHit.mul( wHit ).add( hcSurf.mul( wSurf ) ).div( wSum ),
- surf.get( 'color' ).a
- ).toVar();
- const confidence = confHit.mul( wHit ).add( confSurf.mul( wSurf ) ).div( wSum );
- // Near-black blend means neither tap was usable — fall back to the current frame.
- If( color.rgb.length().lessThan( EPSILON ), () => {
- color.assign( vec4( inputColor.rgb, 1 ) );
- } );
- return { color, confidence, trust: wHitRaw }; // without env probability
- };
- if ( isSpecular ) {
- const spec = resolveSpecularHistory();
- historyColor.assign( spec.color );
- totalConfidence.assign( spec.confidence );
- historyTrust.assign( spec.trust );
- }
- const a = historyColor.a.max( EPSILON );
- // Universal stretch guard: reduce confidence where a "small area" is projected over a "large area".
- const stretchConfidence = reprojectionStretchConfidence( historyUV, this._resolution );
- totalConfidence.mulAssign( stretchConfidence.pow( 2 ) );
- const varianceGamma = mix( float( VARIANCE_GAMMA_MIN ), float( VARIANCE_GAMMA_MAX ), motionFactor.oneMinus().pow2() );
- const clippedRGB = applyVarianceClipping(
- historyColor,
- neighborhood.get( 'mean' ),
- neighborhood.get( 'stdColor' ),
- varianceGamma,
- this.flickerSuppression
- ).toVar();
- const clampIntensity = this.clampIntensity.mul( max( motionFactor.mul( 10 ).min( 1 ), 0.25 ) ).mul(
- float( 1 ).add( stretchConfidence.oneMinus().add( historyTrust.oneMinus() ).clamp() )
- );
- const originalHistoryColor = vec3( historyColor.rgb );
- historyColor.rgb.assign( mix( historyColor.rgb, clippedRGB, clampIntensity ) );
- totalConfidence.mulAssign( exp( originalHistoryColor.sub( clippedRGB ).length().mul( clampIntensity ).mul( 30 ).negate() ) );
- totalConfidence.mulAssign( mix( float( 1 ), historyTrust.mul( 0.05 ).add( 0.95 ), motionFactor.mul( 100 ).clamp() ) );
- If( totalConfidence.lessThan( EPSILON ), () => {
- historyColor.assign( vec4( inputColor.rgb, 1 ) );
- } );
- const currentFrameCount = float( 1 ).div( a ).mul( totalConfidence ).add( 1 ).min( this.maxFrames ).toVar();
- if ( isSpecular ) {
- // A black current sample means no reflection was found this frame (a miss, not dark).
- // Since no valid sample was found, decrement the frame count (as the next accumulating pass will increase it).
- If( inputColor.rgb.length().lessThan( EPSILON ), () => {
- currentFrameCount.assign( currentFrameCount.sub( 1 ).max( 1 ) );
- } );
- }
- return vec4( historyColor.rgb, float( 1 ).div( currentFrameCount ) );
- } );
- return resolve().context( builder.getSharedContext() );
- }
- _syncHistoryTextureBinding() {
- if ( this.accumulate === true || this._externalHistoryTexture === null ) {
- this._historyTextureNode.value = this._historyRenderTarget.texture;
- } else {
- this._historyTextureNode.value = this._externalHistoryTexture;
- }
- }
- /**
- * Supplies an external history source (e.g. a {@link RecurrentDenoiseNode} or its
- * texture). Only used when {@link TemporalReprojectNode#accumulate} is `false`.
- *
- * @param {?(Object|Texture)} source
- */
- setHistoryTexture( source ) {
- this._externalHistoryTexture = ( source && typeof source.getRenderTarget === 'function' )
- ? source.getRenderTarget().texture
- : source;
- this._syncHistoryTextureBinding();
- }
- dispose() {
- this._previousNormalTexture.dispose();
- if ( this._previousDepthNode.value !== this._historyRenderTarget.depthTexture ) {
- this._previousDepthNode.value.dispose();
- }
- if ( this._placeholderPreviousDepthTexture !== this._historyRenderTarget.depthTexture ) {
- this._placeholderPreviousDepthTexture.dispose();
- }
- this._historyRenderTarget.dispose();
- this._resolveRenderTarget.dispose();
- this._resolveMaterial.dispose();
- this._seedMaterial.dispose();
- }
- }
- export default TemporalReprojectNode;
- /**
- * @param {TextureNode} beautyNode
- * @param {TextureNode} depthNode
- * @param {TextureNode} normalNode
- * @param {TextureNode} velocityNode
- * @param {Camera} camera
- * @param {TemporalReprojectNodeOptions} [options]
- * @returns {TemporalReprojectNode}
- */
- export const temporalReproject = ( beautyNode, depthNode, normalNode, velocityNode, camera, options = {} ) => nodeObject( new TemporalReprojectNode(
- convertToTexture( beautyNode ),
- nodeObject( depthNode ),
- nodeObject( normalNode ),
- nodeObject( velocityNode ),
- camera,
- options
- ) );
|