1
0

TemporalReprojectNode.js 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027
  1. 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, context, OnBeforeRenderPipeline, OnAfterRenderPipeline } from 'three/tsl';
  2. import { DepthTexture, HalfFloatType, Matrix4, NodeMaterial, NodeUpdateType, QuadMesh, RenderTarget, RendererUtils, TempNode, Vector2, Vector3 } from 'three/webgpu';
  3. import { ENV_RAY_LENGTH, ENV_RAY_LENGTH_THRESHOLD } from '../utils/SpecularHelpers.js';
  4. // Reprojection helpers
  5. /**
  6. * Maps a resolve (screen) texel to the corresponding beauty-input texel when resolutions differ.
  7. *
  8. * @tsl
  9. */
  10. const beautyTexelFromScreen = Fn( ( [ screenTexel, beautySize, resolveSize ] ) => {
  11. return ivec2( floor( vec2( screenTexel ).mul( beautySize ).div( resolveSize ) ) );
  12. } ).setLayout( {
  13. name: 'beautyTexelFromScreen',
  14. type: 'ivec2',
  15. inputs: [
  16. { name: 'screenTexel', type: 'ivec2' },
  17. { name: 'beautySize', type: 'vec2' },
  18. { name: 'resolveSize', type: 'vec2' }
  19. ]
  20. } );
  21. /**
  22. * Projects a world-space position into previous-frame UV coordinates.
  23. *
  24. * @tsl
  25. */
  26. const projectWorldToUV = Fn( ( [ worldPos, previousViewMatrix, previousProjectionMatrix ] ) => {
  27. const resultUV = vec2( - 1 ).toVar();
  28. const viewSpace = previousViewMatrix.mul( vec4( worldPos, 1.0 ) );
  29. const clipSpace = previousProjectionMatrix.mul( viewSpace ).toVar();
  30. const clipW = clipSpace.w.toVar();
  31. If( abs( clipW ).greaterThan( float( 1e-5 ) ), () => {
  32. const ndc = clipSpace.xyz.div( clipW );
  33. resultUV.assign( ndc.xy.mul( 0.5 ).add( 0.5 ) );
  34. resultUV.y.assign( resultUV.y.oneMinus() );
  35. } );
  36. return resultUV;
  37. } ).setLayout( {
  38. name: 'projectWorldToUV',
  39. type: 'vec2',
  40. inputs: [
  41. { name: 'worldPos', type: 'vec3' },
  42. { name: 'previousViewMatrix', type: 'mat4' },
  43. { name: 'previousProjectionMatrix', type: 'mat4' }
  44. ]
  45. } );
  46. // YCoCg variance clipping
  47. /**
  48. * @param {Node<vec3>} c
  49. * @returns {Node<vec3>}
  50. */
  51. const rgbToYCoCg = ( c ) => vec3(
  52. dot( c, vec3( 0.25, 0.5, 0.25 ) ),
  53. dot( c, vec3( 0.5, 0.0, - 0.5 ) ),
  54. dot( c, vec3( - 0.25, 0.5, - 0.25 ) )
  55. );
  56. /**
  57. * @param {Node<vec3>} c
  58. * @returns {Node<vec3>}
  59. */
  60. const ycocgToRGB = ( c ) => vec3(
  61. c.x.add( c.y ).sub( c.z ),
  62. c.x.add( c.z ),
  63. c.x.sub( c.y ).sub( c.z )
  64. );
  65. const VARIANCE_CLIP_LUMA_SCALE = 10;
  66. /**
  67. * Inverse-luminance compression for HDR variance clipping (Karis-style).
  68. * Bright samples contribute less to neighbourhood moments so sun pixels do not
  69. * inflate the YCoCg AABB and cause aggressive clipping flicker.
  70. *
  71. * @param {Node<vec3>} rgb
  72. * @param {Node<float>} flickerSuppression
  73. * @returns {Node<vec3>}
  74. */
  75. const dampenForVarianceClip = ( rgb, flickerSuppression ) => {
  76. const scale = luminance( rgb ).mul( flickerSuppression ).mul( VARIANCE_CLIP_LUMA_SCALE ).add( 1 );
  77. return rgb.div( scale );
  78. };
  79. /**
  80. * Clips the history sample to the neighbourhood AABB by projecting it toward the box centre.
  81. * Reference: https://github.com/playdeadgames/temporal
  82. *
  83. * @tsl
  84. */
  85. const clipToAABB = Fn( ( [ history, boxMin, boxMax ] ) => {
  86. const pClip = boxMax.add( boxMin ).mul( 0.5 );
  87. const eClip = boxMax.sub( boxMin ).mul( 0.5 ).add( 1e-7 );
  88. const vClip = history.sub( pClip );
  89. const vUnit = vClip.div( eClip );
  90. const absUnit = vUnit.abs();
  91. const maxUnit = max( absUnit.x, absUnit.y, absUnit.z );
  92. return maxUnit.greaterThan( 1 ).select( pClip.add( vClip.div( maxUnit ) ), history );
  93. } ).setLayout( {
  94. name: 'clipToAABB',
  95. type: 'vec3',
  96. inputs: [
  97. { name: 'history', type: 'vec3' },
  98. { name: 'boxMin', type: 'vec3' },
  99. { name: 'boxMax', type: 'vec3' }
  100. ]
  101. } );
  102. const neighborhoodStruct = struct( {
  103. mean: 'vec3',
  104. stdColor: 'vec3',
  105. rayLength: 'float',
  106. envProbability: 'float',
  107. stdDevRayLength: 'float'
  108. } );
  109. /**
  110. * Single 3×3 neighbourhood pass over the beauty buffer. One textureLoad per tap feeds both the
  111. * YCoCg variance-clipping box (colour) and the SSR ray-length statistics (alpha), which previously
  112. * required two separate 3×3 fetches of the same texture.
  113. *
  114. * Sampling is done on the beauty-texel grid (`beautyTexel + offset`), so the taps are distinct
  115. * source texels even when the beauty buffer is lower resolution than the resolve pass (upscaling).
  116. *
  117. * @tsl
  118. */
  119. const collectNeighborhood = Fn( ( [ beautyTexture, beautyTexel, inputColor, flickerSuppression ] ) => {
  120. const offsets = [
  121. [ - 1, - 1 ],
  122. [ - 1, 1 ],
  123. [ 1, - 1 ],
  124. [ 1, 1 ],
  125. [ 1, 0 ],
  126. [ 0, - 1 ],
  127. [ 0, 1 ],
  128. [ - 1, 0 ],
  129. ];
  130. // Colour moments (YCoCg) — centre reuses the already-fetched inputColor.
  131. const center = rgbToYCoCg( dampenForVarianceClip( inputColor.rgb, flickerSuppression ) );
  132. const moment1 = center.toVar();
  133. const moment2 = center.pow2().toVar();
  134. // Ray-length statistics (Welford) over screen-space hits only.
  135. const rayLengthSum = float( 0 ).toVar();
  136. const rayLengthCount = float( 0 ).toVar();
  137. const meanRayLength = float( 0 ).toVar();
  138. const m2RayLength = float( 0 ).toVar();
  139. const accumulateRayLength = ( alpha ) => {
  140. If( alpha.lessThan( ENV_RAY_LENGTH_THRESHOLD ), () => {
  141. rayLengthSum.addAssign( alpha );
  142. rayLengthCount.addAssign( 1 );
  143. const delta = alpha.sub( meanRayLength ).toVar();
  144. meanRayLength.addAssign( delta.div( rayLengthCount ) );
  145. m2RayLength.addAssign( delta.mul( alpha.sub( meanRayLength ) ) );
  146. } );
  147. };
  148. accumulateRayLength( inputColor.a );
  149. for ( const [ x, y ] of offsets ) {
  150. const neighbor = textureLoad( beautyTexture, beautyTexel.add( ivec2( x, y ) ) ).max( 0 ).toVar();
  151. const c = rgbToYCoCg( dampenForVarianceClip( neighbor.rgb, flickerSuppression ) );
  152. moment1.addAssign( c );
  153. moment2.addAssign( c.pow2() );
  154. accumulateRayLength( neighbor.a );
  155. }
  156. const N = float( offsets.length + 1 );
  157. const mean = moment1.div( N );
  158. const stdColor = moment2.div( N ).sub( mean.pow2() ).max( 0 ).sqrt();
  159. // Continuous environment probability: fraction of the 3×3 neighbourhood that missed in screen space
  160. // and fell back to env (0 = all hits, 1 = all env), for smooth reflection/environment transitions.
  161. const envProbability = rayLengthCount.div( float( 9 ) ).oneMinus();
  162. const rayLength = rayLengthCount.lessThan( 0.5 ).select( float( ENV_RAY_LENGTH ), rayLengthSum.div( max( rayLengthCount, float( 1e-4 ) ) ) );
  163. const stdDevRayLength = sqrt( m2RayLength.div( max( rayLengthCount, float( 1.0 ) ) ) ).max( 1e-3 );
  164. return neighborhoodStruct( mean, stdColor, rayLength, envProbability, stdDevRayLength );
  165. } );
  166. /**
  167. * Variance clipping in YCoCg space (Salvi, GDC 2016). Uses the colour moments gathered by
  168. * {@link collectNeighborhood}; `gamma` widens the AABB and is kept out of the gather so the
  169. * neighbourhood pass stays independent of the per-pixel motion factor.
  170. *
  171. * @tsl
  172. */
  173. const applyVarianceClipping = Fn( ( [ historyColor, mean, stdColor, gamma, flickerSuppression ] ) => {
  174. const stddev = stdColor.mul( gamma );
  175. const boxMin = mean.sub( stddev );
  176. const boxMax = mean.add( stddev );
  177. const historyRGB = historyColor.rgb.toVar();
  178. const historyScale = luminance( historyRGB ).mul( flickerSuppression ).mul( VARIANCE_CLIP_LUMA_SCALE ).add( 1 );
  179. const clipped = clipToAABB( rgbToYCoCg( historyRGB.div( historyScale ) ), boxMin, boxMax );
  180. return ycocgToRGB( clipped ).mul( historyScale );
  181. } );
  182. // History sampling
  183. const bilinearTapStruct = struct( { color: 'vec4', weight: 'float', confidence: 'float' } );
  184. const historyResultStruct = struct( { color: 'vec4', tapConfidence: 'float', minConfidence: 'float' } );
  185. /**
  186. * Single bilinear history tap with plane-distance and normal confidence.
  187. *
  188. * @tsl
  189. */
  190. const sampleBilinearTap = Fn( ( [
  191. historyTexture,
  192. previousDepthNode,
  193. previousNormalNode,
  194. resolution,
  195. previousProjectionMatrixInverse,
  196. previousCameraWorldMatrix,
  197. previousCameraViewMatrix,
  198. tapCoord,
  199. bilinearWeight,
  200. worldPosition,
  201. worldNormal
  202. ] ) => {
  203. const color = textureLoad( historyTexture, tapCoord ).max( 0 );
  204. const reprojDepth = textureLoad( previousDepthNode, tapCoord ).r;
  205. const reprojViewPos = getViewPosition( vec2( tapCoord ).add( 0.5 ).div( resolution ), reprojDepth, previousProjectionMatrixInverse );
  206. const reprojWorldPos = previousCameraWorldMatrix.mul( vec4( reprojViewPos, 1.0 ) ).xyz;
  207. const reprojWorldNorm = unpackRGBToNormal( textureLoad( previousNormalNode, tapCoord ).rgb ).transformNormalByInverseViewMatrix( previousCameraViewMatrix );
  208. const planeDiff = abs( dot( reprojWorldPos.sub( worldPosition ), worldNormal ) ).toVar();
  209. planeDiff.divAssign( abs( reprojViewPos.z ) );
  210. const normalConfidence = smoothstep( 0.95, 0.999, reprojWorldNorm.dot( worldNormal ) );
  211. const confidence = smoothstep( 0, 0.01, planeDiff ).oneMinus().mul( normalConfidence );
  212. const weight = bilinearWeight.mul( confidence );
  213. return bilinearTapStruct( color.mul( weight ), weight, confidence );
  214. } );
  215. /**
  216. * @param {Object} ctx - Shared {@link sampleBilinearTap} inputs plus `reprojICoord`.
  217. * @param {Node<ivec2>} tapOffset
  218. * @param {Node<float>} bilinearWeight
  219. */
  220. function bilinearHistoryTap( ctx, tapOffset, bilinearWeight ) {
  221. return sampleBilinearTap(
  222. ctx.historyTexture,
  223. ctx.previousDepthNode,
  224. ctx.previousNormalNode,
  225. ctx.resolution,
  226. ctx.previousProjectionMatrixInverse,
  227. ctx.previousCameraWorldMatrix,
  228. ctx.previousCameraViewMatrix,
  229. ctx.reprojICoord.add( tapOffset ),
  230. bilinearWeight,
  231. ctx.worldPosition,
  232. ctx.worldNormal
  233. );
  234. }
  235. /**
  236. * Geometrically-weighted 4-tap bilinear history sample.
  237. *
  238. * @tsl
  239. */
  240. const sampleHistory4Tap = Fn( ( [
  241. historyTexture,
  242. previousDepthNode,
  243. previousNormalNode,
  244. resolution,
  245. previousProjectionMatrixInverse,
  246. previousCameraWorldMatrix,
  247. previousCameraViewMatrix,
  248. reprojUV,
  249. worldPosition,
  250. worldNormal,
  251. inputColor
  252. ] ) => {
  253. const reprojPixelCoord = reprojUV.mul( resolution ).sub( 0.5 ).toVar();
  254. const reprojICoord = ivec2( floor( reprojPixelCoord ) );
  255. const fCoord = reprojPixelCoord.fract();
  256. const fx = fCoord.x;
  257. const fy = fCoord.y;
  258. const f00 = float( 1 ).sub( fx ).mul( float( 1 ).sub( fy ) );
  259. const f10 = fx.mul( float( 1 ).sub( fy ) );
  260. const f01 = float( 1 ).sub( fx ).mul( fy );
  261. const f11 = fx.mul( fy );
  262. const tapCtx = {
  263. historyTexture,
  264. previousDepthNode,
  265. previousNormalNode,
  266. resolution,
  267. previousProjectionMatrixInverse,
  268. previousCameraWorldMatrix,
  269. previousCameraViewMatrix,
  270. reprojICoord,
  271. worldPosition,
  272. worldNormal
  273. };
  274. const tap00 = bilinearHistoryTap( tapCtx, ivec2( 0, 0 ), f00 );
  275. const tap10 = bilinearHistoryTap( tapCtx, ivec2( 1, 0 ), f10 );
  276. const tap01 = bilinearHistoryTap( tapCtx, ivec2( 0, 1 ), f01 );
  277. const tap11 = bilinearHistoryTap( tapCtx, ivec2( 1, 1 ), f11 );
  278. const colorSum = tap00.get( 'color' ).add( tap10.get( 'color' ) ).add( tap01.get( 'color' ) ).add( tap11.get( 'color' ) );
  279. const weightSum = tap00.get( 'weight' ).add( tap10.get( 'weight' ) ).add( tap01.get( 'weight' ) ).add( tap11.get( 'weight' ) );
  280. const maxConf = max( max( tap00.get( 'confidence' ), tap10.get( 'confidence' ) ), max( tap01.get( 'confidence' ), tap11.get( 'confidence' ) ) );
  281. const minConf = min( min( tap00.get( 'confidence' ), tap10.get( 'confidence' ) ), min( tap01.get( 'confidence' ), tap11.get( 'confidence' ) ) );
  282. return historyResultStruct(
  283. select( weightSum.greaterThan( 0.01 ), colorSum.div( weightSum ), vec4( inputColor.rgb, float( 1 ) ) ),
  284. maxConf,
  285. minConf
  286. );
  287. } );
  288. // Diffuse reprojection
  289. /**
  290. * Reprojection-stretch confidence — detects history magnification (surface stretching).
  291. *
  292. * Differentiates the per-pixel history UV with hardware screen-space derivatives to form the
  293. * reprojection Jacobian `J = ∂(historyPixel)/∂(screenPixel)`, then returns its **minimum
  294. * singular value**, clamped to `[0,1]`.
  295. *
  296. * `σ_min < 1` means the most-stretched axis magnifies history — a few history pixels are smeared
  297. * over many current pixels (e.g. a surface seen at grazing in the previous frame, face-on now), so
  298. * history is undersampled and its confidence should be reduced. `σ_min ≥ 1` (history minified) is
  299. * safe and clamps to 1. Using the minimum singular value rather than the Jacobian determinant
  300. * catches anisotropic 1-D stretch that an area-only measure would smear out.
  301. *
  302. * Works for any reprojection (surface-velocity or parallax hit-point) since it differentiates the
  303. * final history UV, so the same factor applies to both the diffuse and specular paths.
  304. *
  305. * @tsl
  306. */
  307. const reprojectionStretchConfidence = Fn( ( [ historyUV, resolution ] ) => {
  308. // Jacobian columns in pixels: how the history sample position moves per screen pixel.
  309. const jx = dFdx( historyUV ).mul( resolution ).toVar();
  310. const jy = dFdy( historyUV ).mul( resolution ).toVar();
  311. // Singular values of the 2×2 J are sqrt( eigenvalues of JᵀJ ), with
  312. // trace( JᵀJ ) = ‖J‖²_F and det( JᵀJ ) = det( J )².
  313. const det = jx.x.mul( jy.y ).sub( jx.y.mul( jy.x ) );
  314. const fro2 = dot( jx, jx ).add( dot( jy, jy ) );
  315. const disc = fro2.mul( fro2 ).mul( 0.25 ).sub( det.mul( det ) ).max( 0 ).sqrt();
  316. const sigMin = fro2.mul( 0.5 ).sub( disc ).max( 0 ).sqrt();
  317. return sigMin.saturate();
  318. } );
  319. // Specular reprojection
  320. /**
  321. * Parallax-corrected hit-point reprojection into previous-frame UVs.
  322. *
  323. * @tsl
  324. */
  325. const reprojectHitPoint = Fn( ( [
  326. rayOrig,
  327. rayLength,
  328. cameraWorldPosition,
  329. previousViewMatrix,
  330. previousProjectionMatrix
  331. ] ) => {
  332. const cameraRay = normalize( rayOrig.sub( cameraWorldPosition ) ).toVar();
  333. const parallaxHitPoint = rayOrig.add( cameraRay.mul( rayLength ) );
  334. return projectWorldToUV( parallaxHitPoint, previousViewMatrix, previousProjectionMatrix );
  335. } );
  336. /**
  337. * Converts screen-space velocity (NDC derivative) to a UV reprojection offset.
  338. *
  339. * @tsl
  340. */
  341. const velocityToUVOffset = Fn( ( [ velocity ] ) => {
  342. return velocity.mul( vec2( 0.5, - 0.5 ) );
  343. } ).setLayout( {
  344. name: 'velocityToUVOffset',
  345. type: 'vec2',
  346. inputs: [ { name: 'velocity', type: 'vec2' } ]
  347. } );
  348. /**
  349. * Current and previous-frame camera matrices for temporal reprojection passes.
  350. *
  351. * @param {Camera} camera
  352. */
  353. function bindTemporalCameraUniforms( camera ) {
  354. const worldMatrix = uniform( new Matrix4().copy( camera.matrixWorld ) );
  355. const viewMatrix = uniform( new Matrix4().copy( camera.matrixWorldInverse ) );
  356. const projectionMatrix = uniform( new Matrix4().copy( camera.projectionMatrix ) );
  357. const projectionMatrixInverse = uniform( new Matrix4().copy( camera.projectionMatrixInverse ) );
  358. const worldPosition = uniform( new Vector3().copy( camera.position ) );
  359. const previousWorldMatrix = uniform( new Matrix4().copy( camera.matrixWorld ) );
  360. const previousViewMatrix = uniform( new Matrix4().copy( camera.matrixWorldInverse ) );
  361. const previousProjectionMatrix = uniform( new Matrix4().copy( camera.projectionMatrix ) );
  362. const previousProjectionMatrixInverse = uniform( new Matrix4().copy( camera.projectionMatrixInverse ) );
  363. /**
  364. * @param {Camera} cam
  365. */
  366. function updateFromCamera( cam ) {
  367. previousWorldMatrix.value.copy( worldMatrix.value );
  368. previousViewMatrix.value.copy( viewMatrix.value );
  369. previousProjectionMatrix.value.copy( projectionMatrix.value );
  370. previousProjectionMatrixInverse.value.copy( projectionMatrixInverse.value );
  371. worldMatrix.value.copy( cam.matrixWorld );
  372. viewMatrix.value.copy( cam.matrixWorldInverse );
  373. projectionMatrix.value.copy( cam.projectionMatrix );
  374. projectionMatrixInverse.value.copy( cam.projectionMatrixInverse );
  375. worldPosition.value.copy( cam.position );
  376. }
  377. return {
  378. worldMatrix,
  379. viewMatrix,
  380. projectionMatrix,
  381. projectionMatrixInverse,
  382. worldPosition,
  383. previousWorldMatrix,
  384. previousViewMatrix,
  385. previousProjectionMatrix,
  386. previousProjectionMatrixInverse,
  387. updateFromCamera
  388. };
  389. }
  390. const _quadMesh = /*@__PURE__*/ new QuadMesh();
  391. const _size = /*@__PURE__*/ new Vector2();
  392. let _rendererState;
  393. const DEFAULT_MAX_VELOCITY_LENGTH = 128;
  394. const VARIANCE_GAMMA_MIN = 0.5;
  395. const VARIANCE_GAMMA_MAX = 1;
  396. /**
  397. * @typedef {'diffuse' | 'specular'} TemporalReprojectMode
  398. */
  399. /**
  400. * @typedef {Object} TemporalReprojectNodeOptions
  401. * @property {TemporalReprojectMode} [mode='diffuse'] - `diffuse` for SSGI/scene colour; `specular` for SSR reflections.
  402. * @property {boolean} [hitPointReprojection] - Parallax hit-point reprojection (specular mode only). Defaults to `true` in specular mode.
  403. * @property {boolean} [accumulate=false] - When `true`, history is stored in this pass (classic temporal resolve). When `false`,
  404. * use {@link TemporalReprojectNode#setHistoryTexture} to read history from another pass (e.g. denoise output).
  405. */
  406. /**
  407. * Temporal reprojection pass for denoising screen-space effects (SSGI, SSR, etc.).
  408. *
  409. * Both modes share geometrically-weighted 4-tap bilinear history sampling and YCoCg variance clipping.
  410. * Surface velocity reprojection is always sampled first. Specular mode then blends in
  411. * hit-point parallax history on top of that surface result.
  412. * Diffuse mode applies velocity-field divergence to detect surface stretching.
  413. *
  414. * Unlike jitter-based TAA/TAAU, this node does not apply camera sub-pixel jitter — it only
  415. * reprojects and accumulates history using motion vectors.
  416. *
  417. * References:
  418. * - {@link https://alextardif.com/TAA.html}
  419. * - {@link https://www.elopezr.com/temporal-aa-and-the-quest-for-the-holy-trail/}
  420. *
  421. * @augments TempNode
  422. * @three_import import { temporalReproject } from 'three/addons/tsl/display/TemporalReprojectNode.js';
  423. */
  424. class TemporalReprojectNode extends TempNode {
  425. static get type() {
  426. return 'TemporalReprojectNode';
  427. }
  428. /**
  429. * @param {TextureNode} beautyNode
  430. * @param {TextureNode} depthNode
  431. * @param {TextureNode} normalNode
  432. * @param {TextureNode} velocityNode
  433. * @param {Camera} camera
  434. * @param {TemporalReprojectNodeOptions} [options]
  435. */
  436. constructor( beautyNode, depthNode, normalNode, velocityNode, camera, options = {} ) {
  437. super( 'vec4' );
  438. const {
  439. mode = 'diffuse',
  440. hitPointReprojection = mode === 'specular',
  441. accumulate = false
  442. } = options;
  443. if ( mode !== 'specular' && mode !== 'diffuse' ) {
  444. throw new Error( 'TemporalReprojectNode: `mode` must be `diffuse` or `specular`.' );
  445. }
  446. this.isTemporalReprojectNode = true;
  447. this.updateBeforeType = NodeUpdateType.FRAME;
  448. this.beautyNode = beautyNode;
  449. this.depthNode = depthNode;
  450. this.normalNode = normalNode;
  451. this.velocityNode = velocityNode;
  452. this.camera = camera;
  453. /**
  454. * @type {TemporalReprojectMode}
  455. */
  456. this.mode = mode;
  457. /**
  458. * When `true`, resolve output is copied into the internal history buffer each frame.
  459. * When `false`, history is supplied externally via {@link TemporalReprojectNode#setHistoryTexture}.
  460. *
  461. * @type {boolean}
  462. */
  463. this.accumulate = accumulate;
  464. this.maxVelocityLength = DEFAULT_MAX_VELOCITY_LENGTH;
  465. this._resolution = uniform( new Vector2() );
  466. this._cameraUniforms = bindTemporalCameraUniforms( camera );
  467. this.maxFrames = uniform( 32 );
  468. this.hitPointReprojection = uniform( hitPointReprojection, 'bool' );
  469. this.clampIntensity = uniform( 1 );
  470. this.flickerSuppression = uniform( 1 );
  471. this._historyRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType, depthTexture: new DepthTexture() } );
  472. this._historyRenderTarget.texture.name = 'TemporalReprojectNode.history';
  473. this._historyTextureNode = texture( this._historyRenderTarget.texture );
  474. this._resolveRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
  475. this._resolveRenderTarget.texture.name = 'TemporalReprojectNode.resolve';
  476. this._resolveMaterial = new NodeMaterial();
  477. this._resolveMaterial.name = 'TemporalReproject.resolve';
  478. this._seedMaterial = new NodeMaterial();
  479. this._seedMaterial.name = 'TemporalReproject.seed';
  480. this._textureNode = passTexture( this, this._resolveRenderTarget.texture );
  481. this._originalProjectionMatrix = new Matrix4();
  482. this._placeholderPreviousDepthTexture = new DepthTexture( 1, 1 );
  483. this._previousDepthNode = texture( this._placeholderPreviousDepthTexture );
  484. this._previousNormalTexture = normalNode.value.clone();
  485. this._previousNormalNode = texture( this._previousNormalTexture );
  486. this._needsPostProcessingSync = false;
  487. this._externalHistoryTexture = null;
  488. this._syncHistoryTextureBinding();
  489. }
  490. getTextureNode() {
  491. return this._textureNode;
  492. }
  493. setSize( width, height ) {
  494. if ( width === null || height === null ) return;
  495. this._historyRenderTarget.setSize( width, height );
  496. this._resolveRenderTarget.setSize( width, height );
  497. this._resolution.value.set( width, height );
  498. }
  499. setViewOffset() {
  500. this.camera.updateProjectionMatrix();
  501. this._originalProjectionMatrix.copy( this.camera.projectionMatrix );
  502. velocity.setProjectionMatrix( this._originalProjectionMatrix );
  503. }
  504. clearViewOffset() {
  505. velocity.setProjectionMatrix( null );
  506. }
  507. updateBefore( frame ) {
  508. const { renderer } = frame;
  509. this._cameraUniforms.updateFromCamera( this.camera );
  510. const drawingBufferSize = renderer.getDrawingBufferSize( _size );
  511. const width = drawingBufferSize.width;
  512. const height = drawingBufferSize.height;
  513. if ( this._needsPostProcessingSync === true ) {
  514. this.setViewOffset();
  515. this._needsPostProcessingSync = false;
  516. }
  517. _rendererState = RendererUtils.resetRendererState( renderer, _rendererState );
  518. const needsRestart = this._historyRenderTarget.width !== width || this._historyRenderTarget.height !== height;
  519. this.setSize( width, height );
  520. let historySwappedForRestart = false;
  521. if ( needsRestart === true ) {
  522. renderer.initRenderTarget( this._historyRenderTarget );
  523. renderer.initRenderTarget( this._resolveRenderTarget );
  524. this._previousNormalTexture.dispose();
  525. this._previousNormalTexture = this.normalNode.value.clone();
  526. this._previousNormalNode.value = this._previousNormalTexture;
  527. // External history (e.g. denoise feedback) is stale at the old resolution — use
  528. // freshly seeded internal history for this frame instead.
  529. if ( this.accumulate === false && this._externalHistoryTexture !== null ) {
  530. this._historyTextureNode.value = this._historyRenderTarget.texture;
  531. historySwappedForRestart = true;
  532. }
  533. renderer.setRenderTarget( this._historyRenderTarget );
  534. _quadMesh.material = this._seedMaterial;
  535. _quadMesh.name = 'TemporalReproject.seed';
  536. _quadMesh.render( renderer );
  537. renderer.setRenderTarget( null );
  538. }
  539. renderer.setRenderTarget( this._resolveRenderTarget );
  540. _quadMesh.material = this._resolveMaterial;
  541. _quadMesh.name = 'TemporalReproject';
  542. _quadMesh.render( renderer );
  543. renderer.setRenderTarget( null );
  544. if ( historySwappedForRestart === true ) {
  545. this._syncHistoryTextureBinding();
  546. } else if ( this.accumulate === true ) {
  547. renderer.copyTextureToTexture( this._resolveRenderTarget.texture, this._historyRenderTarget.texture );
  548. }
  549. const currentDepth = this.depthNode.value;
  550. const srcW = currentDepth.image !== null && currentDepth.image !== undefined ? currentDepth.image.width : 0;
  551. const srcH = currentDepth.image !== null && currentDepth.image !== undefined ? currentDepth.image.height : 0;
  552. if ( srcW > 0 && srcH > 0 ) {
  553. renderer.copyTextureToTexture( currentDepth, this._historyRenderTarget.depthTexture );
  554. renderer.copyTextureToTexture( this.normalNode.value, this._previousNormalTexture );
  555. this._previousDepthNode.value = this._historyRenderTarget.depthTexture;
  556. }
  557. RendererUtils.restoreRendererState( renderer, _rendererState );
  558. }
  559. setup( builder ) {
  560. const sharedContext = context( builder.getSharedContext() );
  561. if ( builder.renderPipeline && ! builder.context.renderPipelineState.viewOffsetOwner ) {
  562. builder.context.renderPipelineState.viewOffsetOwner = this;
  563. this._needsPostProcessingSync = true;
  564. OnBeforeRenderPipeline( () => {
  565. this.setViewOffset();
  566. } );
  567. OnAfterRenderPipeline( () => {
  568. this.clearViewOffset();
  569. } );
  570. }
  571. this._resolveMaterial.contextNode = sharedContext;
  572. this._resolveMaterial.fragmentNode = this._buildResolve();
  573. this._resolveMaterial.needsUpdate = true;
  574. this._buildSeed( sharedContext );
  575. return this._textureNode;
  576. }
  577. _buildSeed( sharedContext ) {
  578. const seed = Fn( () => {
  579. const screenTexel = ivec2( floor( screenCoordinate.xy.sub( 0.5 ) ) );
  580. const beautySize = this.beautyNode.size();
  581. const beautyTexel = beautyTexelFromScreen( screenTexel, beautySize, this._resolution );
  582. return textureLoad( this.beautyNode, beautyTexel ).max( 0 );
  583. } );
  584. this._seedMaterial.contextNode = sharedContext;
  585. this._seedMaterial.fragmentNode = seed();
  586. this._seedMaterial.needsUpdate = true;
  587. }
  588. _buildResolve() {
  589. const isSpecular = this.mode === 'specular';
  590. const cameraUniforms = this._cameraUniforms;
  591. const resolve = Fn( () => {
  592. const uvNode = uv();
  593. const screenTexel = ivec2( floor( screenCoordinate.xy.sub( 0.5 ) ) );
  594. const depth = textureLoad( this.depthNode, screenTexel ).r.toVar();
  595. depth.greaterThanEqual( 1.0 ).discard();
  596. const beautySize = this.beautyNode.size();
  597. const beautyTexel = beautyTexelFromScreen( screenTexel, beautySize, this._resolution );
  598. const inputColor = textureLoad( this.beautyNode, beautyTexel ).max( 0 ).toVar();
  599. const viewNormal = unpackRGBToNormal( textureLoad( this.normalNode, screenTexel ).rgb ).toVar();
  600. // Shared 3×3 beauty fetch: feeds both the variance-clip box and the SSR ray-length stats.
  601. const neighborhood = collectNeighborhood( this.beautyNode, beautyTexel, inputColor, this.flickerSuppression );
  602. const worldNormal = viewNormal.transformNormalByInverseViewMatrix( cameraUniforms.viewMatrix ).toVar();
  603. const viewPosition = getViewPosition( uvNode, depth, cameraUniforms.projectionMatrixInverse ).toVar();
  604. const worldPosition = cameraUniforms.worldMatrix.mul( vec4( viewPosition, 1.0 ) ).xyz.toVar();
  605. const sampleHistory = ( reprojUV ) => sampleHistory4Tap(
  606. this._historyTextureNode,
  607. this._previousDepthNode,
  608. this._previousNormalNode,
  609. this._resolution,
  610. cameraUniforms.previousProjectionMatrixInverse,
  611. cameraUniforms.previousWorldMatrix,
  612. cameraUniforms.previousViewMatrix,
  613. reprojUV,
  614. worldPosition,
  615. worldNormal,
  616. inputColor.rgb
  617. );
  618. // Surface-velocity reprojection — the base history for both modes. `historyUV` is
  619. // reused below for the stretch guard, so it is computed once here.
  620. const velocityOff = velocityToUVOffset( textureLoad( this.velocityNode, screenTexel ).xy ).toVar();
  621. const motionFactor = velocityOff.mul( this._resolution ).length().div( float( this.maxVelocityLength ) ).saturate();
  622. const historyUV = uvNode.sub( velocityOff ).toVar();
  623. const surf = sampleHistory( historyUV );
  624. const historyColor = surf.get( 'color' ).toVar();
  625. const totalConfidence = float( 1 ).toVar();
  626. const historyTrust = float( 0 ).toVar();
  627. // Specular: blend parallax hit-point history on top of the surface result. Returns the resolved
  628. // color (rgb from the blend, alpha from the surface tap), its confidence, and the hit-vs-surface trust.
  629. const resolveSpecularHistory = () => {
  630. const surfValid = historyUV.x.greaterThanEqual( 0 ).and( historyUV.x.lessThanEqual( 1 ) )
  631. .and( historyUV.y.greaterThanEqual( 0 ) ).and( historyUV.y.lessThanEqual( 1 ) );
  632. const historyUV_hit = reprojectHitPoint(
  633. worldPosition,
  634. neighborhood.get( 'rayLength' ),
  635. cameraUniforms.worldPosition,
  636. cameraUniforms.previousViewMatrix,
  637. cameraUniforms.previousProjectionMatrix
  638. ).toVar();
  639. const hitValid = historyUV_hit.x.greaterThanEqual( 0 ).and( historyUV_hit.x.lessThanEqual( 1 ) )
  640. .and( historyUV_hit.y.greaterThanEqual( 0 ) ).and( historyUV_hit.y.lessThanEqual( 1 ) )
  641. .and( this.hitPointReprojection );
  642. const hit = sampleHistory( historyUV_hit );
  643. const hcHit = hit.get( 'color' ).rgb.max( 0 );
  644. const hcSurf = surf.get( 'color' ).rgb.max( 0 );
  645. const confHit = hitValid.select( hit.get( 'tapConfidence' ), float( 0 ) );
  646. const confSurf = surfValid.select( surf.get( 'tapConfidence' ), float( 0 ) );
  647. const minConfHit = hit.get( 'minConfidence' );
  648. const reflectionEdgeFactor = neighborhood.get( 'stdDevRayLength' );
  649. reflectionEdgeFactor.assign( reflectionEdgeFactor.mul( motionFactor.mul( 100 ).min( 1 ) ).mul( 3.5 ).min( 1 ).oneMinus() );
  650. const curvatureFactor = fwidth( worldNormal.xyz ).length().mul( 50 ).clamp();
  651. const envProbability = neighborhood.get( 'envProbability' );
  652. const wHitRaw = minConfHit
  653. .mul( reflectionEdgeFactor )
  654. .mul( curvatureFactor.oneMinus() )
  655. .mul( confHit ).toConst();
  656. const wHit = wHitRaw.mul( envProbability.pow2().oneMinus() );
  657. const wSurf = wHit.oneMinus().mul( confSurf );
  658. const wSum = max( wHit.add( wSurf ), float( EPSILON ) );
  659. const color = vec4(
  660. hcHit.mul( wHit ).add( hcSurf.mul( wSurf ) ).div( wSum ),
  661. surf.get( 'color' ).a
  662. ).toVar();
  663. const confidence = confHit.mul( wHit ).add( confSurf.mul( wSurf ) ).div( wSum );
  664. // Near-black blend means neither tap was usable — fall back to the current frame.
  665. If( color.rgb.length().lessThan( EPSILON ), () => {
  666. color.assign( vec4( inputColor.rgb, 1 ) );
  667. } );
  668. return { color, confidence, trust: wHitRaw }; // without env probability
  669. };
  670. if ( isSpecular ) {
  671. const spec = resolveSpecularHistory();
  672. historyColor.assign( spec.color );
  673. totalConfidence.assign( spec.confidence );
  674. historyTrust.assign( spec.trust );
  675. }
  676. const a = historyColor.a.max( EPSILON );
  677. // Universal stretch guard: reduce confidence where a "small area" is projected over a "large area".
  678. const stretchConfidence = reprojectionStretchConfidence( historyUV, this._resolution );
  679. totalConfidence.mulAssign( stretchConfidence.pow( 2 ) );
  680. const varianceGamma = mix( float( VARIANCE_GAMMA_MIN ), float( VARIANCE_GAMMA_MAX ), motionFactor.oneMinus().pow2() );
  681. const clippedRGB = applyVarianceClipping(
  682. historyColor,
  683. neighborhood.get( 'mean' ),
  684. neighborhood.get( 'stdColor' ),
  685. varianceGamma,
  686. this.flickerSuppression
  687. ).toVar();
  688. const clampIntensity = this.clampIntensity.mul( max( motionFactor.mul( 10 ).min( 1 ), 0.25 ) ).mul(
  689. float( 1 ).add( stretchConfidence.oneMinus().add( historyTrust.oneMinus() ).clamp() )
  690. );
  691. const originalHistoryColor = vec3( historyColor.rgb );
  692. historyColor.rgb.assign( mix( historyColor.rgb, clippedRGB, clampIntensity ) );
  693. totalConfidence.mulAssign( exp( originalHistoryColor.sub( clippedRGB ).length().mul( clampIntensity ).mul( 30 ).negate() ) );
  694. totalConfidence.mulAssign( mix( float( 1 ), historyTrust.mul( 0.05 ).add( 0.95 ), motionFactor.mul( 100 ).clamp() ) );
  695. If( totalConfidence.lessThan( EPSILON ), () => {
  696. historyColor.assign( vec4( inputColor.rgb, 1 ) );
  697. } );
  698. const currentFrameCount = float( 1 ).div( a ).mul( totalConfidence ).add( 1 ).min( this.maxFrames ).toVar();
  699. if ( isSpecular ) {
  700. // A black current sample means no reflection was found this frame (a miss, not dark).
  701. // Since no valid sample was found, decrement the frame count (as the next accumulating pass will increase it).
  702. If( inputColor.rgb.length().lessThan( EPSILON ), () => {
  703. currentFrameCount.assign( currentFrameCount.sub( 1 ).max( 1 ) );
  704. } );
  705. }
  706. return vec4( historyColor.rgb, float( 1 ).div( currentFrameCount ) );
  707. } );
  708. return resolve();
  709. }
  710. _syncHistoryTextureBinding() {
  711. if ( this.accumulate === true || this._externalHistoryTexture === null ) {
  712. this._historyTextureNode.value = this._historyRenderTarget.texture;
  713. } else {
  714. this._historyTextureNode.value = this._externalHistoryTexture;
  715. }
  716. }
  717. /**
  718. * Supplies an external history source (e.g. a {@link RecurrentDenoiseNode} or its
  719. * texture). Only used when {@link TemporalReprojectNode#accumulate} is `false`.
  720. *
  721. * @param {?(Object|Texture)} source
  722. */
  723. setHistoryTexture( source ) {
  724. this._externalHistoryTexture = ( source && typeof source.getRenderTarget === 'function' )
  725. ? source.getRenderTarget().texture
  726. : source;
  727. this._syncHistoryTextureBinding();
  728. }
  729. dispose() {
  730. this._previousNormalTexture.dispose();
  731. if ( this._previousDepthNode.value !== this._historyRenderTarget.depthTexture ) {
  732. this._previousDepthNode.value.dispose();
  733. }
  734. if ( this._placeholderPreviousDepthTexture !== this._historyRenderTarget.depthTexture ) {
  735. this._placeholderPreviousDepthTexture.dispose();
  736. }
  737. this._historyRenderTarget.dispose();
  738. this._resolveRenderTarget.dispose();
  739. this._resolveMaterial.dispose();
  740. this._seedMaterial.dispose();
  741. }
  742. }
  743. export default TemporalReprojectNode;
  744. /**
  745. * @param {TextureNode} beautyNode
  746. * @param {TextureNode} depthNode
  747. * @param {TextureNode} normalNode
  748. * @param {TextureNode} velocityNode
  749. * @param {Camera} camera
  750. * @param {TemporalReprojectNodeOptions} [options]
  751. * @returns {TemporalReprojectNode}
  752. */
  753. export const temporalReproject = ( beautyNode, depthNode, normalNode, velocityNode, camera, options = {} ) => nodeObject( new TemporalReprojectNode(
  754. convertToTexture( beautyNode ),
  755. nodeObject( depthNode ),
  756. nodeObject( normalNode ),
  757. nodeObject( velocityNode ),
  758. camera,
  759. options
  760. ) );
粤ICP备19079148号