TemporalReprojectNode.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023
  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 } 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 ).transformDirection( 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 renderPipeline = builder.context.renderPipeline;
  561. if ( renderPipeline ) {
  562. this._needsPostProcessingSync = true;
  563. renderPipeline.context.onBeforeRenderPipeline = () => {
  564. this.setViewOffset();
  565. };
  566. renderPipeline.context.onAfterRenderPipeline = () => {
  567. this.clearViewOffset();
  568. };
  569. }
  570. this._resolveMaterial.fragmentNode = this._buildResolve( builder );
  571. this._resolveMaterial.needsUpdate = true;
  572. this._buildSeed( builder );
  573. return this._textureNode;
  574. }
  575. _buildSeed( builder ) {
  576. const seed = Fn( () => {
  577. const screenTexel = ivec2( floor( screenCoordinate.xy.sub( 0.5 ) ) );
  578. const beautySize = this.beautyNode.size();
  579. const beautyTexel = beautyTexelFromScreen( screenTexel, beautySize, this._resolution );
  580. return textureLoad( this.beautyNode, beautyTexel ).max( 0 );
  581. } );
  582. this._seedMaterial.fragmentNode = seed().context( builder.getSharedContext() );
  583. this._seedMaterial.needsUpdate = true;
  584. }
  585. _buildResolve( builder ) {
  586. const isSpecular = this.mode === 'specular';
  587. const cameraUniforms = this._cameraUniforms;
  588. const resolve = Fn( () => {
  589. const uvNode = uv();
  590. const screenTexel = ivec2( floor( screenCoordinate.xy.sub( 0.5 ) ) );
  591. const depth = textureLoad( this.depthNode, screenTexel ).r.toVar();
  592. depth.greaterThanEqual( 1.0 ).discard();
  593. const beautySize = this.beautyNode.size();
  594. const beautyTexel = beautyTexelFromScreen( screenTexel, beautySize, this._resolution );
  595. const inputColor = textureLoad( this.beautyNode, beautyTexel ).max( 0 ).toVar();
  596. const viewNormal = unpackRGBToNormal( textureLoad( this.normalNode, screenTexel ).rgb ).toVar();
  597. // Shared 3×3 beauty fetch: feeds both the variance-clip box and the SSR ray-length stats.
  598. const neighborhood = collectNeighborhood( this.beautyNode, beautyTexel, inputColor, this.flickerSuppression );
  599. const worldNormal = viewNormal.transformDirection( cameraUniforms.viewMatrix ).toVar();
  600. const viewPosition = getViewPosition( uvNode, depth, cameraUniforms.projectionMatrixInverse ).toVar();
  601. const worldPosition = cameraUniforms.worldMatrix.mul( vec4( viewPosition, 1.0 ) ).xyz.toVar();
  602. const sampleHistory = ( reprojUV ) => sampleHistory4Tap(
  603. this._historyTextureNode,
  604. this._previousDepthNode,
  605. this._previousNormalNode,
  606. this._resolution,
  607. cameraUniforms.previousProjectionMatrixInverse,
  608. cameraUniforms.previousWorldMatrix,
  609. cameraUniforms.previousViewMatrix,
  610. reprojUV,
  611. worldPosition,
  612. worldNormal,
  613. inputColor.rgb
  614. );
  615. // Surface-velocity reprojection — the base history for both modes. `historyUV` is
  616. // reused below for the stretch guard, so it is computed once here.
  617. const velocityOff = velocityToUVOffset( textureLoad( this.velocityNode, screenTexel ).xy ).toVar();
  618. const motionFactor = velocityOff.mul( this._resolution ).length().div( float( this.maxVelocityLength ) ).saturate();
  619. const historyUV = uvNode.sub( velocityOff ).toVar();
  620. const surf = sampleHistory( historyUV );
  621. const historyColor = surf.get( 'color' ).toVar();
  622. const totalConfidence = float( 1 ).toVar();
  623. const historyTrust = float( 0 ).toVar();
  624. // Specular: blend parallax hit-point history on top of the surface result. Returns the resolved
  625. // color (rgb from the blend, alpha from the surface tap), its confidence, and the hit-vs-surface trust.
  626. const resolveSpecularHistory = () => {
  627. const surfValid = historyUV.x.greaterThanEqual( 0 ).and( historyUV.x.lessThanEqual( 1 ) )
  628. .and( historyUV.y.greaterThanEqual( 0 ) ).and( historyUV.y.lessThanEqual( 1 ) );
  629. const historyUV_hit = reprojectHitPoint(
  630. worldPosition,
  631. neighborhood.get( 'rayLength' ),
  632. cameraUniforms.worldPosition,
  633. cameraUniforms.previousViewMatrix,
  634. cameraUniforms.previousProjectionMatrix
  635. ).toVar();
  636. const hitValid = historyUV_hit.x.greaterThanEqual( 0 ).and( historyUV_hit.x.lessThanEqual( 1 ) )
  637. .and( historyUV_hit.y.greaterThanEqual( 0 ) ).and( historyUV_hit.y.lessThanEqual( 1 ) )
  638. .and( this.hitPointReprojection );
  639. const hit = sampleHistory( historyUV_hit );
  640. const hcHit = hit.get( 'color' ).rgb.max( 0 );
  641. const hcSurf = surf.get( 'color' ).rgb.max( 0 );
  642. const confHit = hitValid.select( hit.get( 'tapConfidence' ), float( 0 ) );
  643. const confSurf = surfValid.select( surf.get( 'tapConfidence' ), float( 0 ) );
  644. const minConfHit = hit.get( 'minConfidence' );
  645. const reflectionEdgeFactor = neighborhood.get( 'stdDevRayLength' );
  646. reflectionEdgeFactor.assign( reflectionEdgeFactor.mul( motionFactor.mul( 100 ).min( 1 ) ).mul( 3.5 ).min( 1 ).oneMinus() );
  647. const curvatureFactor = fwidth( worldNormal.xyz ).length().mul( 50 ).clamp();
  648. const envProbability = neighborhood.get( 'envProbability' );
  649. const wHitRaw = minConfHit
  650. .mul( reflectionEdgeFactor )
  651. .mul( curvatureFactor.oneMinus() )
  652. .mul( confHit ).toConst();
  653. const wHit = wHitRaw.mul( envProbability.pow2().oneMinus() );
  654. const wSurf = wHit.oneMinus().mul( confSurf );
  655. const wSum = max( wHit.add( wSurf ), float( EPSILON ) );
  656. const color = vec4(
  657. hcHit.mul( wHit ).add( hcSurf.mul( wSurf ) ).div( wSum ),
  658. surf.get( 'color' ).a
  659. ).toVar();
  660. const confidence = confHit.mul( wHit ).add( confSurf.mul( wSurf ) ).div( wSum );
  661. // Near-black blend means neither tap was usable — fall back to the current frame.
  662. If( color.rgb.length().lessThan( EPSILON ), () => {
  663. color.assign( vec4( inputColor.rgb, 1 ) );
  664. } );
  665. return { color, confidence, trust: wHitRaw }; // without env probability
  666. };
  667. if ( isSpecular ) {
  668. const spec = resolveSpecularHistory();
  669. historyColor.assign( spec.color );
  670. totalConfidence.assign( spec.confidence );
  671. historyTrust.assign( spec.trust );
  672. }
  673. const a = historyColor.a.max( EPSILON );
  674. // Universal stretch guard: reduce confidence where a "small area" is projected over a "large area".
  675. const stretchConfidence = reprojectionStretchConfidence( historyUV, this._resolution );
  676. totalConfidence.mulAssign( stretchConfidence.pow( 2 ) );
  677. const varianceGamma = mix( float( VARIANCE_GAMMA_MIN ), float( VARIANCE_GAMMA_MAX ), motionFactor.oneMinus().pow2() );
  678. const clippedRGB = applyVarianceClipping(
  679. historyColor,
  680. neighborhood.get( 'mean' ),
  681. neighborhood.get( 'stdColor' ),
  682. varianceGamma,
  683. this.flickerSuppression
  684. ).toVar();
  685. const clampIntensity = this.clampIntensity.mul( max( motionFactor.mul( 10 ).min( 1 ), 0.25 ) ).mul(
  686. float( 1 ).add( stretchConfidence.oneMinus().add( historyTrust.oneMinus() ).clamp() )
  687. );
  688. const originalHistoryColor = vec3( historyColor.rgb );
  689. historyColor.rgb.assign( mix( historyColor.rgb, clippedRGB, clampIntensity ) );
  690. totalConfidence.mulAssign( exp( originalHistoryColor.sub( clippedRGB ).length().mul( clampIntensity ).mul( 30 ).negate() ) );
  691. totalConfidence.mulAssign( mix( float( 1 ), historyTrust.mul( 0.05 ).add( 0.95 ), motionFactor.mul( 100 ).clamp() ) );
  692. If( totalConfidence.lessThan( EPSILON ), () => {
  693. historyColor.assign( vec4( inputColor.rgb, 1 ) );
  694. } );
  695. const currentFrameCount = float( 1 ).div( a ).mul( totalConfidence ).add( 1 ).min( this.maxFrames ).toVar();
  696. if ( isSpecular ) {
  697. // A black current sample means no reflection was found this frame (a miss, not dark).
  698. // Since no valid sample was found, decrement the frame count (as the next accumulating pass will increase it).
  699. If( inputColor.rgb.length().lessThan( EPSILON ), () => {
  700. currentFrameCount.assign( currentFrameCount.sub( 1 ).max( 1 ) );
  701. } );
  702. }
  703. return vec4( historyColor.rgb, float( 1 ).div( currentFrameCount ) );
  704. } );
  705. return resolve().context( builder.getSharedContext() );
  706. }
  707. _syncHistoryTextureBinding() {
  708. if ( this.accumulate === true || this._externalHistoryTexture === null ) {
  709. this._historyTextureNode.value = this._historyRenderTarget.texture;
  710. } else {
  711. this._historyTextureNode.value = this._externalHistoryTexture;
  712. }
  713. }
  714. /**
  715. * Supplies an external history source (e.g. a {@link RecurrentDenoiseNode} or its
  716. * texture). Only used when {@link TemporalReprojectNode#accumulate} is `false`.
  717. *
  718. * @param {?(Object|Texture)} source
  719. */
  720. setHistoryTexture( source ) {
  721. this._externalHistoryTexture = ( source && typeof source.getRenderTarget === 'function' )
  722. ? source.getRenderTarget().texture
  723. : source;
  724. this._syncHistoryTextureBinding();
  725. }
  726. dispose() {
  727. this._previousNormalTexture.dispose();
  728. if ( this._previousDepthNode.value !== this._historyRenderTarget.depthTexture ) {
  729. this._previousDepthNode.value.dispose();
  730. }
  731. if ( this._placeholderPreviousDepthTexture !== this._historyRenderTarget.depthTexture ) {
  732. this._placeholderPreviousDepthTexture.dispose();
  733. }
  734. this._historyRenderTarget.dispose();
  735. this._resolveRenderTarget.dispose();
  736. this._resolveMaterial.dispose();
  737. this._seedMaterial.dispose();
  738. }
  739. }
  740. export default TemporalReprojectNode;
  741. /**
  742. * @param {TextureNode} beautyNode
  743. * @param {TextureNode} depthNode
  744. * @param {TextureNode} normalNode
  745. * @param {TextureNode} velocityNode
  746. * @param {Camera} camera
  747. * @param {TemporalReprojectNodeOptions} [options]
  748. * @returns {TemporalReprojectNode}
  749. */
  750. export const temporalReproject = ( beautyNode, depthNode, normalNode, velocityNode, camera, options = {} ) => nodeObject( new TemporalReprojectNode(
  751. convertToTexture( beautyNode ),
  752. nodeObject( depthNode ),
  753. nodeObject( normalNode ),
  754. nodeObject( velocityNode ),
  755. camera,
  756. options
  757. ) );
粤ICP备19079148号