|
@@ -0,0 +1,1023 @@
|
|
|
|
|
+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 {import('three/tsl').Node<vec3>} c
|
|
|
|
|
+ * @returns {import('three/tsl').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 {import('three/tsl').Node<vec3>} c
|
|
|
|
|
+ * @returns {import('three/tsl').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 {import('three/tsl').Node<vec3>} rgb
|
|
|
|
|
+ * @param {import('three/tsl').Node<float>} flickerSuppression
|
|
|
|
|
+ * @returns {import('three/tsl').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 {import('three/tsl').Node<ivec2>} tapOffset
|
|
|
|
|
+ * @param {import('three/tsl').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 {import('three').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 {import('three').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 {import('three/tsl').TextureNode} beautyNode
|
|
|
|
|
+ * @param {import('three/tsl').TextureNode} depthNode
|
|
|
|
|
+ * @param {import('three/tsl').TextureNode} normalNode
|
|
|
|
|
+ * @param {import('three/tsl').TextureNode} velocityNode
|
|
|
|
|
+ * @param {import('three').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|import('three').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 {import('three/tsl').TextureNode} beautyNode
|
|
|
|
|
+ * @param {import('three/tsl').TextureNode} depthNode
|
|
|
|
|
+ * @param {import('three/tsl').TextureNode} normalNode
|
|
|
|
|
+ * @param {import('three/tsl').TextureNode} velocityNode
|
|
|
|
|
+ * @param {import('three').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
|
|
|
|
|
+) );
|