SVGFNode.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800
  1. import { RenderTarget, Vector2, TempNode, QuadMesh, NodeMaterial, RendererUtils, HalfFloatType, NearestFilter, DepthTexture, FloatType } from 'three/webgpu';
  2. import { Fn, If, float, vec2, vec4, uv, uniform, texture, passTexture, convertToTexture, luminance, dot, min, max, abs, pow, mix, outputStruct, property, sqrt, ivec2, perspectiveDepthToViewZ, getNormalFromDepth, NodeUpdateType } from 'three/tsl';
  3. const _quadMesh = /*@__PURE__*/ new QuadMesh();
  4. const _size = /*@__PURE__*/ new Vector2();
  5. let _rendererState;
  6. // à-trous kernel ( 1/4, 1/2, 1/4 ) used as a 3×3 separable filter. The small per-level tap footprint
  7. // keeps each level cheap, while the increasing per-level step still covers a wide area across levels.
  8. const _kernel = [ 1 / 4, 1 / 2, 1 / 4 ];
  9. // deadzone of the temporal gradient, in units of the local standard deviation: sampling jitter
  10. // moves the neighborhood mean by about one deviation per frame ( the jitter is spatially
  11. // coherent ) while a real lighting change moves it by many
  12. const _gradientDeadzone = 3;
  13. // fixed accumulation weight of the luminance moments, decoupled from the adaptive alpha: if the
  14. // moments followed it, a fully rejected history would collapse the variance to zero, which in
  15. // turn pins the gradient and the alpha at their maximum with no way to recover
  16. const _momentsAlpha = 0.2;
  17. /**
  18. * Post processing node that denoises a noisy screen-space signal (such as the raw output of
  19. * {@link SSGINode}) using a spatiotemporal filter in the spirit of SVGF (Spatiotemporal
  20. * Variance-Guided Filtering).
  21. *
  22. * The pipeline is:
  23. * - **Temporal accumulation**: the current frame is reprojected against history using the
  24. * velocity buffer and blended, with a depth-based disocclusion test that resets history where
  25. * the reprojection is invalid. Luminance moments are accumulated alongside the signal, giving
  26. * a per-pixel variance estimate of the incoming noise.
  27. * - **Adaptive temporal alpha**: a temporal gradient measured in units of the local standard
  28. * deviation raises the accumulation weight towards the current frame where the signal changed,
  29. * rejecting stale history to limit ghosting. Expressing the gradient in deviation units
  30. * separates sampling jitter (about one deviation by construction) from real lighting change
  31. * (many deviations).
  32. * - **Variance-guided à-trous**: a multi-level edge-avoiding wavelet filter (increasing step size
  33. * per level) spatially denoises the accumulated signal. The luminance edge-stop scales with the
  34. * local deviation, so noisy regions are smoothed aggressively while converged regions keep
  35. * their edges; the variance estimate is filtered along with the signal.
  36. * - **Feedback**: the first à-trous level is fed back as the color history for the next frame,
  37. * which keeps the temporal signal denoised without over-blurring (the SVGF feedback trick).
  38. *
  39. * References:
  40. * - {@link https://cg.ivd.kit.edu/publications/2017/svgf/svgf_preprint.pdf} (SVGF, Schied et al.)
  41. * - {@link https://cg.ivd.kit.edu/english/atf.php} (A-SVGF adaptive temporal filtering, Schied et al.)
  42. * - {@link https://jo.dreggn.org/home/2010_atrous.pdf} (Edge-Avoiding À-Trous, Dammertz et al.)
  43. *
  44. * @augments TempNode
  45. * @three_import import { svgf } from 'three/addons/tsl/display/SVGFNode.js';
  46. */
  47. class SVGFNode extends TempNode {
  48. static get type() {
  49. return 'SVGFNode';
  50. }
  51. /**
  52. * Constructs a new SVGF node.
  53. *
  54. * @param {TextureNode} beautyNode - The noisy texture node to denoise (e.g. the SSGI output).
  55. * @param {TextureNode} depthNode - A texture node that represents the scene's depth.
  56. * @param {?TextureNode} normalNode - A texture node that represents the scene's view-space normals.
  57. * @param {TextureNode} velocityNode - A texture node that represents the scene's velocity.
  58. * @param {PerspectiveCamera} camera - The camera the scene is rendered with.
  59. */
  60. constructor( beautyNode, depthNode, normalNode, velocityNode, camera ) {
  61. super( 'vec4' );
  62. /**
  63. * The noisy texture node to denoise.
  64. *
  65. * @type {TextureNode}
  66. */
  67. this.beautyNode = beautyNode;
  68. /**
  69. * A texture node that represents the scene's depth.
  70. *
  71. * @type {TextureNode}
  72. */
  73. this.depthNode = depthNode;
  74. /**
  75. * A texture node that represents the scene's view-space normals. If `null`, normals are
  76. * reconstructed from depth in the shader.
  77. *
  78. * @type {?TextureNode}
  79. */
  80. this.normalNode = normalNode;
  81. /**
  82. * A texture node that represents the scene's velocity.
  83. *
  84. * @type {TextureNode}
  85. */
  86. this.velocityNode = velocityNode;
  87. /**
  88. * The camera the scene is rendered with.
  89. *
  90. * @type {PerspectiveCamera}
  91. */
  92. this.camera = camera;
  93. /**
  94. * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders its
  95. * passes once per frame in `updateBefore()`.
  96. *
  97. * @type {string}
  98. * @default 'frame'
  99. */
  100. this.updateBeforeType = NodeUpdateType.FRAME;
  101. /**
  102. * Number of edge-avoiding à-trous levels. Each level doubles the sampling step.
  103. *
  104. * @type {number}
  105. * @default 5
  106. */
  107. this.atrousIterations = 5;
  108. /**
  109. * Minimum weight given to the current frame during temporal accumulation. Smaller values
  110. * accumulate more history (less noise, more lag). Should be in the range `[0.01, 1]`.
  111. *
  112. * @type {UniformNode<float>}
  113. * @default 0.1
  114. */
  115. this.temporalAlpha = uniform( 0.1 );
  116. /**
  117. * Strength of the adaptive temporal alpha (anti-ghosting). The temporal gradient — how much
  118. * the signal changed versus the reprojected history, in units of the local standard
  119. * deviation — is scaled by this value to raise the accumulation weight towards the current
  120. * frame, rejecting stale history where the lighting changed. `0` disables it (fixed
  121. * `temporalAlpha`); higher reduces ghosting at the cost of more noise on changing regions.
  122. *
  123. * @type {UniformNode<float>}
  124. * @default 4
  125. */
  126. this.antiGhosting = uniform( 4 );
  127. /**
  128. * Relative depth difference above which history is rejected as a disocclusion.
  129. *
  130. * @type {UniformNode<float>}
  131. * @default 0.05
  132. */
  133. this.depthRejection = uniform( 0.05 );
  134. /**
  135. * Depth edge-stopping strength of the à-trous filter (plane distance, in world units).
  136. *
  137. * @type {UniformNode<float>}
  138. * @default 4
  139. */
  140. this.depthPhi = uniform( 4 );
  141. /**
  142. * Normal edge-stopping strength of the à-trous filter.
  143. *
  144. * @type {UniformNode<float>}
  145. * @default 128
  146. */
  147. this.normalPhi = uniform( 128 );
  148. /**
  149. * Luminance edge-stopping strength of the à-trous filter, in units of the local standard
  150. * deviation. Differences below `lumaPhi` deviations are smoothed; larger differences are
  151. * treated as edges and preserved.
  152. *
  153. * @type {UniformNode<float>}
  154. * @default 4
  155. */
  156. this.lumaPhi = uniform( 4 );
  157. /**
  158. * Clamps the luminance of each incoming sample to this multiple of its local
  159. * neighborhood mean before accumulation. Suppresses isolated bright outliers
  160. * ( fireflies ) that would otherwise blink in dark regions. Lower values suppress
  161. * more aggressively at the cost of dimming small bright details.
  162. *
  163. * @type {UniformNode<float>}
  164. * @default 2
  165. */
  166. this.fireflyFactor = uniform( 2 );
  167. // private uniforms
  168. /**
  169. * The inverse resolution of the effect.
  170. *
  171. * @private
  172. * @type {UniformNode<vec2>}
  173. */
  174. this._invSize = uniform( new Vector2() );
  175. /**
  176. * The current à-trous step size, updated per level in `updateBefore()`.
  177. *
  178. * @private
  179. * @type {UniformNode<float>}
  180. */
  181. this._stepSize = uniform( 1 );
  182. /**
  183. * The camera's inverse projection matrix.
  184. *
  185. * @private
  186. * @type {UniformNode<mat4>}
  187. */
  188. this._cameraProjectionMatrixInverse = uniform( camera.projectionMatrixInverse );
  189. /**
  190. * The camera's near and far values.
  191. *
  192. * @private
  193. * @type {UniformNode<vec2>}
  194. */
  195. this._cameraNearFar = uniform( new Vector2() );
  196. // render targets. The signal targets carry two attachments: the filtered signal, and
  197. // the luminance moments with the variance derived from them
  198. const rtOptions = { depthBuffer: false, type: HalfFloatType, count: 2 };
  199. /**
  200. * Holds the previous frame's filtered result and luminance moments (the history fed back
  201. * each frame). Its depth texture stores the previous frame's depth for the disocclusion test.
  202. *
  203. * @private
  204. * @type {RenderTarget}
  205. */
  206. this._historyRenderTarget = new RenderTarget( 1, 1, { ...rtOptions, depthTexture: new DepthTexture() } );
  207. /**
  208. * Holds the temporally accumulated signal and moments before spatial filtering.
  209. *
  210. * @private
  211. * @type {RenderTarget}
  212. */
  213. this._temporalRenderTarget = new RenderTarget( 1, 1, rtOptions );
  214. /**
  215. * Ping-pong targets for the à-trous iterations.
  216. *
  217. * @private
  218. * @type {Array<RenderTarget>}
  219. */
  220. this._atrousRenderTargets = [ new RenderTarget( 1, 1, rtOptions ), new RenderTarget( 1, 1, rtOptions ) ];
  221. /**
  222. * Holds the final filtered result.
  223. *
  224. * @private
  225. * @type {RenderTarget}
  226. */
  227. this._resolveRenderTarget = new RenderTarget( 1, 1, rtOptions );
  228. /**
  229. * Holds a packed geometry buffer ( view-space normal in rgb, linear view-space Z in a )
  230. * computed once per frame so the à-trous filter does not reconstruct it per tap.
  231. *
  232. * @private
  233. * @type {RenderTarget}
  234. */
  235. this._geometryRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
  236. this._geometryRenderTarget.texture.name = 'SVGF.geometry';
  237. this._historyRenderTarget.textures[ 0 ].name = 'SVGF.history';
  238. this._historyRenderTarget.textures[ 1 ].name = 'SVGF.history.moments';
  239. this._temporalRenderTarget.textures[ 0 ].name = 'SVGF.temporal';
  240. this._temporalRenderTarget.textures[ 1 ].name = 'SVGF.temporal.moments';
  241. this._atrousRenderTargets[ 0 ].textures[ 0 ].name = 'SVGF.atrous0';
  242. this._atrousRenderTargets[ 0 ].textures[ 1 ].name = 'SVGF.atrous0.moments';
  243. this._atrousRenderTargets[ 1 ].textures[ 0 ].name = 'SVGF.atrous1';
  244. this._atrousRenderTargets[ 1 ].textures[ 1 ].name = 'SVGF.atrous1.moments';
  245. this._resolveRenderTarget.textures[ 0 ].name = 'SVGF.resolve';
  246. this._resolveRenderTarget.textures[ 1 ].name = 'SVGF.resolve.moments';
  247. // the resolve output keeps linear filtering so the result upsamples smoothly when the
  248. // effect runs at a lower resolution than the output
  249. for ( const rt of [ this._historyRenderTarget, this._temporalRenderTarget, this._geometryRenderTarget, ...this._atrousRenderTargets ] ) {
  250. for ( const tex of rt.textures ) {
  251. tex.minFilter = NearestFilter;
  252. tex.magFilter = NearestFilter;
  253. }
  254. }
  255. // materials
  256. /**
  257. * @private
  258. * @type {NodeMaterial}
  259. */
  260. this._temporalMaterial = new NodeMaterial();
  261. this._temporalMaterial.name = 'SVGF.temporal';
  262. /**
  263. * @private
  264. * @type {NodeMaterial}
  265. */
  266. this._atrousMaterial = new NodeMaterial();
  267. this._atrousMaterial.name = 'SVGF.atrous';
  268. /**
  269. * @private
  270. * @type {NodeMaterial}
  271. */
  272. this._geometryMaterial = new NodeMaterial();
  273. this._geometryMaterial.name = 'SVGF.geometry';
  274. /**
  275. * Texture node for the previous frame's depth, used by the disocclusion test.
  276. *
  277. * @private
  278. * @type {TextureNode}
  279. */
  280. this._previousDepthNode = texture( new DepthTexture( 1, 1 ) );
  281. /**
  282. * Texture node holding the history (feedback) color.
  283. *
  284. * @private
  285. * @type {TextureNode}
  286. */
  287. this._historyNode = texture( this._historyRenderTarget.textures[ 0 ] );
  288. /**
  289. * Texture node holding the history luminance moments.
  290. *
  291. * @private
  292. * @type {TextureNode}
  293. */
  294. this._historyMomentsNode = texture( this._historyRenderTarget.textures[ 1 ] );
  295. /**
  296. * Texture node holding the current à-trous color input (swapped per iteration).
  297. *
  298. * @private
  299. * @type {TextureNode}
  300. */
  301. this._atrousInputNode = texture( this._temporalRenderTarget.textures[ 0 ] );
  302. /**
  303. * Texture node holding the current à-trous variance input (swapped per iteration).
  304. *
  305. * @private
  306. * @type {TextureNode}
  307. */
  308. this._atrousVarianceNode = texture( this._temporalRenderTarget.textures[ 1 ] );
  309. /**
  310. * Texture node holding the packed geometry buffer ( view normal + linear view Z ).
  311. *
  312. * @private
  313. * @type {TextureNode}
  314. */
  315. this._geometryNode = texture( this._geometryRenderTarget.texture );
  316. /**
  317. * The result of the effect as a separate texture node.
  318. *
  319. * @private
  320. * @type {PassTextureNode}
  321. */
  322. this._textureNode = passTexture( this, this._resolveRenderTarget.textures[ 0 ] );
  323. }
  324. /**
  325. * Returns the result of the effect as a texture node.
  326. *
  327. * @return {PassTextureNode} A texture node that represents the result of the effect.
  328. */
  329. getTextureNode() {
  330. return this._textureNode;
  331. }
  332. /**
  333. * Sets the size of the effect.
  334. *
  335. * @param {number} width - The width of the effect.
  336. * @param {number} height - The height of the effect.
  337. */
  338. setSize( width, height ) {
  339. this._invSize.value.set( 1 / width, 1 / height );
  340. this._historyRenderTarget.setSize( width, height );
  341. this._temporalRenderTarget.setSize( width, height );
  342. this._resolveRenderTarget.setSize( width, height );
  343. this._atrousRenderTargets[ 0 ].setSize( width, height );
  344. this._atrousRenderTargets[ 1 ].setSize( width, height );
  345. this._geometryRenderTarget.setSize( width, height );
  346. }
  347. /**
  348. * This method is used to render the effect once per frame.
  349. *
  350. * @param {NodeFrame} frame - The current node frame.
  351. */
  352. updateBefore( frame ) {
  353. const { renderer } = frame;
  354. this._cameraProjectionMatrixInverse.value.copy( this.camera.projectionMatrixInverse );
  355. this._cameraNearFar.value.set( this.camera.near, this.camera.far );
  356. // keep the effect in sync with the dimensions of the beauty texture
  357. const beautyTexture = this.beautyNode.value;
  358. const width = beautyTexture.image.width;
  359. const height = beautyTexture.image.height;
  360. const needsRestart = this._historyRenderTarget.width !== width || this._historyRenderTarget.height !== height;
  361. _rendererState = RendererUtils.resetRendererState( renderer, _rendererState );
  362. this.setSize( width, height );
  363. if ( needsRestart === true ) {
  364. // after a resize, seed the history with the current beauty buffer so the effect does
  365. // not fade in from black
  366. renderer.initRenderTarget( this._historyRenderTarget );
  367. renderer.copyTextureToTexture( beautyTexture, this._historyRenderTarget.textures[ 0 ] );
  368. }
  369. // geometry prepare ( packed view normal + linear view Z ) -> geometry target
  370. renderer.setRenderTarget( this._geometryRenderTarget );
  371. _quadMesh.material = this._geometryMaterial;
  372. _quadMesh.name = 'SVGF.geometry';
  373. _quadMesh.render( renderer );
  374. // temporal accumulation -> temporal target
  375. renderer.setRenderTarget( this._temporalRenderTarget );
  376. _quadMesh.material = this._temporalMaterial;
  377. _quadMesh.name = 'SVGF.temporal';
  378. _quadMesh.render( renderer );
  379. // the integrated moments become next frame's moments history
  380. renderer.copyTextureToTexture( this._temporalRenderTarget.textures[ 1 ], this._historyRenderTarget.textures[ 1 ] );
  381. // edge-avoiding à-trous, ping-pong between targets, last level into the resolve target
  382. const iterations = this.atrousIterations;
  383. let inputTarget = this._temporalRenderTarget;
  384. _quadMesh.material = this._atrousMaterial;
  385. _quadMesh.name = 'SVGF.atrous';
  386. for ( let i = 0; i < iterations; i ++ ) {
  387. const target = ( i === iterations - 1 ) ? this._resolveRenderTarget : this._atrousRenderTargets[ i % 2 ];
  388. this._stepSize.value = 1 << i; // 1, 2, 4, 8, 16, ...
  389. this._atrousInputNode.value = inputTarget.textures[ 0 ];
  390. this._atrousVarianceNode.value = inputTarget.textures[ 1 ];
  391. renderer.setRenderTarget( target );
  392. _quadMesh.render( renderer );
  393. // feed the first à-trous level back as next frame's color history
  394. if ( i === 0 ) {
  395. renderer.copyTextureToTexture( target.textures[ 0 ], this._historyRenderTarget.textures[ 0 ] );
  396. }
  397. inputTarget = target;
  398. }
  399. // store the current depth as the previous depth for next frame's disocclusion test
  400. const size = renderer.getDrawingBufferSize( _size );
  401. if ( this._historyRenderTarget.width === size.width && this._historyRenderTarget.height === size.height ) {
  402. renderer.copyTextureToTexture( this.depthNode.value, this._historyRenderTarget.depthTexture );
  403. this._previousDepthNode.value = this._historyRenderTarget.depthTexture;
  404. }
  405. renderer.setRenderTarget( null );
  406. RendererUtils.restoreRendererState( renderer, _rendererState );
  407. }
  408. /**
  409. * This method is used to setup the effect's TSL code.
  410. *
  411. * @param {NodeBuilder} builder - The current node builder.
  412. * @return {PassTextureNode}
  413. */
  414. setup( builder ) {
  415. if ( builder.renderer.reversedDepthBuffer === true ) {
  416. this._historyRenderTarget.depthTexture.type = FloatType;
  417. }
  418. const sampleDepth = ( uvNode ) => this.depthNode.sample( uvNode ).r;
  419. const sampleNormal = ( uvNode ) => ( this.normalNode !== null ) ? this.normalNode.sample( uvNode ).rgb.normalize() : getNormalFromDepth( uvNode, this.depthNode.value, this._cameraProjectionMatrixInverse );
  420. const sharedContext = builder.getSharedContext();
  421. // --- temporal accumulation pass ---
  422. // outputs the accumulated signal and the integrated luminance moments
  423. // ( μ1, μ2 ) with the variance derived from them
  424. const temporalColor = property( 'vec4' );
  425. const temporalMoments = property( 'vec4' );
  426. const temporal = Fn( () => {
  427. const uvNode = uv();
  428. const texel = ivec2( uvNode.mul( this.velocityNode.size() ) ); // texel coordinates of the velocity texture, whose resolution can differ from the input
  429. const current = this.beautyNode.sample( uvNode ).toVar();
  430. const depth = sampleDepth( uvNode ).toVar();
  431. temporalColor.assign( current );
  432. temporalMoments.assign( vec4( 0.0 ) );
  433. If( depth.lessThan( 1.0 ), () => {
  434. // reproject through the velocity buffer ( NDC -> uv )
  435. const offsetUV = this.velocityNode.load( texel ).xy.mul( vec2( 0.5, - 0.5 ) );
  436. const historyUV = uvNode.sub( offsetUV ).toVar();
  437. const inBounds = historyUV.greaterThanEqual( 0.0 ).all().and( historyUV.lessThanEqual( 1.0 ).all() );
  438. // depth-based disocclusion test ( compare linear view-space Z )
  439. const { x: near, y: far } = this._cameraNearFar;
  440. const currentZ = perspectiveDepthToViewZ( depth, near, far );
  441. const previousZ = perspectiveDepthToViewZ( this._previousDepthNode.sample( historyUV ).r, near, far );
  442. const validDepth = abs( currentZ.sub( previousZ ) ).lessThan( abs( currentZ ).mul( this.depthRejection ) );
  443. const validHistory = inBounds.and( validDepth );
  444. const history = this._historyNode.sample( historyUV ).toVar();
  445. const historyMoments = this._historyMomentsNode.sample( historyUV ).rg.toVar();
  446. // 3×3 spatial luminance moments of the incoming frame: used by the firefly clamp, as
  447. // the change estimate for the temporal gradient and as variance fallback on disocclusion
  448. const currentLuma = luminance( current.rgb ).toVar();
  449. const blurredLuma = float( currentLuma ).toVar();
  450. const blurredLuma2 = currentLuma.mul( currentLuma ).toVar();
  451. for ( let y = - 1; y <= 1; y ++ ) {
  452. for ( let x = - 1; x <= 1; x ++ ) {
  453. if ( x === 0 && y === 0 ) continue; // the center tap is already in currentLuma
  454. const tapLuma = luminance( this.beautyNode.sample( uvNode.add( vec2( x, y ).mul( this._invSize ) ) ).rgb );
  455. blurredLuma.addAssign( tapLuma );
  456. blurredLuma2.addAssign( tapLuma.mul( tapLuma ) );
  457. }
  458. }
  459. blurredLuma.mulAssign( 1 / 9 );
  460. blurredLuma2.mulAssign( 1 / 9 );
  461. // suppress fireflies: clamp the sample against its neighborhood mean so isolated
  462. // bright outliers cannot blink in and out of the accumulated result
  463. const maxLuma = blurredLuma.mul( this.fireflyFactor );
  464. current.rgb.mulAssign( currentLuma.greaterThan( maxLuma ).select( maxLuma.div( currentLuma ), float( 1.0 ) ) );
  465. // temporal gradient in units of the local standard deviation
  466. const historyVariance = max( historyMoments.y.sub( historyMoments.x.mul( historyMoments.x ) ), 0.0 );
  467. const deviation = sqrt( historyVariance.add( 1e-4 ) );
  468. const historyLuma = luminance( history.rgb );
  469. const gradient = abs( blurredLuma.sub( historyLuma ) ).div( deviation );
  470. const adaptiveAlpha = max( this.temporalAlpha, gradient.sub( _gradientDeadzone ).mul( this.antiGhosting ).clamp() );
  471. const alpha = validHistory.select( adaptiveAlpha, float( 1.0 ) );
  472. temporalColor.assign( mix( history, current, alpha ) );
  473. // the luminance moments are accumulated with the same reprojection; on disocclusion
  474. // the spatial moments take over as an immediate estimate
  475. const clampedLuma = min( currentLuma, maxLuma ); // the firefly clamp limits the luminance to maxLuma
  476. const currentMoments = vec2( clampedLuma, clampedLuma.mul( clampedLuma ) );
  477. const integratedMoments = validHistory.select( mix( historyMoments, currentMoments, _momentsAlpha ), vec2( blurredLuma, blurredLuma2 ) ).toVar();
  478. const variance = max( integratedMoments.y.sub( integratedMoments.x.mul( integratedMoments.x ) ), 0.0 );
  479. temporalMoments.assign( vec4( integratedMoments, variance, 0.0 ) );
  480. } );
  481. return vec4( 0 ); // temporary solution until TSL does not complain anymore
  482. } );
  483. this._temporalMaterial.colorNode = temporal().context( sharedContext );
  484. this._temporalMaterial.outputNode = outputStruct( temporalColor, temporalMoments );
  485. this._temporalMaterial.needsUpdate = true;
  486. // --- geometry prepare pass ( view normal + linear view Z, packed once per frame ) ---
  487. const prepare = Fn( () => {
  488. const uvNode = uv();
  489. const depth = sampleDepth( uvNode );
  490. const normal = sampleNormal( uvNode );
  491. const viewZ = perspectiveDepthToViewZ( depth, this._cameraNearFar.x, this._cameraNearFar.y );
  492. // valid view Z is negative; store a positive sentinel for background so the filter skips it
  493. return vec4( normal, depth.greaterThanEqual( 1.0 ).select( float( 1.0 ), viewZ ) );
  494. } );
  495. this._geometryMaterial.fragmentNode = prepare().context( sharedContext );
  496. this._geometryMaterial.needsUpdate = true;
  497. // --- variance-guided à-trous pass ---
  498. const atrousColor = property( 'vec4' );
  499. const atrousMoments = property( 'vec4' );
  500. const atrous = Fn( () => {
  501. const uvNode = uv();
  502. const centerColor = this._atrousInputNode.sample( uvNode ).toVar();
  503. const centerVariance = this._atrousVarianceNode.sample( uvNode ).b.toVar();
  504. const centerGeometry = this._geometryNode.sample( uvNode ).toVar();
  505. const centerZ = centerGeometry.w.toVar();
  506. atrousColor.assign( centerColor );
  507. atrousMoments.assign( vec4( 0.0, 0.0, centerVariance, 0.0 ) );
  508. If( centerZ.lessThan( 0.0 ), () => { // valid geometry only
  509. const centerNormal = centerGeometry.xyz.toVar();
  510. const centerLuma = luminance( centerColor.rgb ).toVar();
  511. const step = this._invSize.mul( this._stepSize );
  512. // gather the neighborhood once; the taps drive both the variance prefilter and the filter itself
  513. const taps = [];
  514. for ( let y = - 1; y <= 1; y ++ ) {
  515. for ( let x = - 1; x <= 1; x ++ ) {
  516. if ( x === 0 && y === 0 ) continue;
  517. const sampleUV = uvNode.add( vec2( x, y ).mul( step ) ).toVar();
  518. taps.push( {
  519. kernelWeight: _kernel[ x + 1 ] * _kernel[ y + 1 ],
  520. color: this._atrousInputNode.sample( sampleUV ).toVar(),
  521. geometry: this._geometryNode.sample( sampleUV ).toVar(),
  522. variance: this._atrousVarianceNode.sample( sampleUV ).b.toVar()
  523. } );
  524. }
  525. }
  526. // the variance estimate is itself noisy: a kernel-prefiltered variance stabilizes the
  527. // luminance edge-stop
  528. const prefilteredVariance = centerVariance.mul( _kernel[ 1 ] * _kernel[ 1 ] ).toVar();
  529. for ( const tap of taps ) {
  530. prefilteredVariance.addAssign( tap.variance.mul( tap.kernelWeight ) );
  531. }
  532. // luminance differences are weighted in units of the local deviation: noisy regions
  533. // get smoothed aggressively while converged regions keep their edges
  534. const lumaScale = sqrt( prefilteredVariance ).mul( this.lumaPhi ).add( 1e-3 ).toVar();
  535. const sum = centerColor.toVar(); // center tap has weight 1
  536. const totalWeight = float( 1.0 ).toVar();
  537. const varianceSum = centerVariance.toVar();
  538. for ( const tap of taps ) {
  539. // edge-stopping weights ( normal, linear depth, luminance )
  540. const normalWeight = pow( max( dot( centerNormal, tap.geometry.xyz ), 0.0 ), this.normalPhi );
  541. const depthWeight = max( float( 1.0 ).sub( abs( centerZ.sub( tap.geometry.w ) ).div( this.depthPhi ) ), 0.0 );
  542. const lumaWeight = max( float( 1.0 ).sub( abs( luminance( tap.color.rgb ).sub( centerLuma ) ).div( lumaScale ) ), 0.0 );
  543. const weight = float( tap.kernelWeight ).mul( normalWeight ).mul( depthWeight ).mul( lumaWeight ).toVar();
  544. sum.addAssign( tap.color.mul( weight ) );
  545. totalWeight.addAssign( weight );
  546. varianceSum.addAssign( tap.variance.mul( weight.mul( weight ) ) );
  547. }
  548. atrousColor.assign( sum.div( totalWeight ) );
  549. atrousMoments.assign( vec4( 0.0, 0.0, varianceSum.div( totalWeight.mul( totalWeight ) ), 0.0 ) );
  550. } );
  551. return vec4( 0 ); // temporary solution until TSL does not complain anymore
  552. } );
  553. this._atrousMaterial.colorNode = atrous().context( sharedContext );
  554. this._atrousMaterial.outputNode = outputStruct( atrousColor, atrousMoments );
  555. this._atrousMaterial.needsUpdate = true;
  556. return this._textureNode;
  557. }
  558. /**
  559. * Frees internal resources. This method should be called when the effect is no longer required.
  560. */
  561. dispose() {
  562. this._historyRenderTarget.dispose();
  563. this._temporalRenderTarget.dispose();
  564. this._resolveRenderTarget.dispose();
  565. this._atrousRenderTargets[ 0 ].dispose();
  566. this._atrousRenderTargets[ 1 ].dispose();
  567. this._geometryRenderTarget.dispose();
  568. this._temporalMaterial.dispose();
  569. this._atrousMaterial.dispose();
  570. this._geometryMaterial.dispose();
  571. }
  572. }
  573. export default SVGFNode;
  574. /**
  575. * TSL function for creating an SVGF denoise effect.
  576. *
  577. * @tsl
  578. * @function
  579. * @param {Node} beautyNode - The noisy node to denoise (e.g. the SSGI output).
  580. * @param {Node<float>} depthNode - A node that represents the scene's depth.
  581. * @param {?Node<vec3>} normalNode - A node that represents the scene's view-space normals.
  582. * @param {Node} velocityNode - A node that represents the scene's velocity.
  583. * @param {PerspectiveCamera} camera - The camera the scene is rendered with.
  584. * @returns {SVGFNode}
  585. */
  586. export const svgf = ( beautyNode, depthNode, normalNode, velocityNode, camera ) => {
  587. // effects that render into an internal target expose it via getTextureNode(), which
  588. // avoids re-rendering the input into an intermediate texture
  589. if ( beautyNode.isTextureNode !== true && typeof beautyNode.getTextureNode === 'function' ) {
  590. beautyNode = beautyNode.getTextureNode();
  591. }
  592. return new SVGFNode( convertToTexture( beautyNode ), depthNode, normalNode, velocityNode, camera );
  593. };
粤ICP备19079148号