FXAANode.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. import { Vector2, TempNode } from 'three/webgpu';
  2. import { nodeObject, Fn, uniformArray, select, float, NodeUpdateType, uv, dot, clamp, uniform, convertToTexture, smoothstep, bool, vec2, vec3, If, Loop, max, min, Break, abs } from 'three/tsl';
  3. /**
  4. * Post processing node for applying FXAA. This node requires sRGB input
  5. * so tone mapping and color space conversion must happen before the anti-aliasing.
  6. *
  7. * @augments TempNode
  8. * @three_import import { fxaa } from 'three/addons/tsl/display/FXAANode.js';
  9. */
  10. class FXAANode extends TempNode {
  11. static get type() {
  12. return 'FXAANode';
  13. }
  14. /**
  15. * Constructs a new FXAA node.
  16. *
  17. * @param {TextureNode} textureNode - The texture node that represents the input of the effect.
  18. */
  19. constructor( textureNode ) {
  20. super( 'vec4' );
  21. /**
  22. * The texture node that represents the input of the effect.
  23. *
  24. * @type {TextureNode}
  25. */
  26. this.textureNode = textureNode;
  27. /**
  28. * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node updates
  29. * its internal uniforms once per frame in `updateBefore()`.
  30. *
  31. * @type {string}
  32. * @default 'frame'
  33. */
  34. this.updateBeforeType = NodeUpdateType.FRAME;
  35. /**
  36. * A uniform node holding the inverse resolution value.
  37. *
  38. * @private
  39. * @type {UniformNode<vec2>}
  40. */
  41. this._invSize = uniform( new Vector2() );
  42. }
  43. /**
  44. * This method is used to update the effect's uniforms once per frame.
  45. *
  46. * @param {NodeFrame} frame - The current node frame.
  47. */
  48. updateBefore( /* frame */ ) {
  49. const map = this.textureNode.value;
  50. this._invSize.value.set( 1 / map.image.width, 1 / map.image.height );
  51. }
  52. /**
  53. * This method is used to setup the effect's TSL code.
  54. *
  55. * @param {NodeBuilder} builder - The current node builder.
  56. * @return {ShaderCallNodeInternal}
  57. */
  58. setup( /* builder */ ) {
  59. const textureNode = this.textureNode.bias( - 100 );
  60. const uvNode = textureNode.uvNode || uv();
  61. const EDGE_STEP_COUNT = float( 6 );
  62. const EDGE_GUESS = float( 8.0 );
  63. const EDGE_STEPS = uniformArray( [ 1.0, 1.5, 2.0, 2.0, 2.0, 4.0 ] );
  64. const _ContrastThreshold = float( 0.0312 );
  65. const _RelativeThreshold = float( 0.063 );
  66. const _SubpixelBlending = float( 1.0 );
  67. const Sample = Fn( ( [ uv ] ) => {
  68. return textureNode.sample( uv );
  69. } );
  70. const SampleLuminance = Fn( ( [ uv ] ) => {
  71. return dot( Sample( uv ).rgb, vec3( 0.3, 0.59, 0.11 ) );
  72. } );
  73. const SampleLuminanceOffset = Fn( ( [ texSize, uv, uOffset, vOffset ] ) => {
  74. const shiftedUv = uv.add( texSize.mul( vec2( uOffset, vOffset ) ) );
  75. return SampleLuminance( shiftedUv );
  76. } );
  77. const ShouldSkipPixel = ( l ) => {
  78. const threshold = max( _ContrastThreshold, _RelativeThreshold.mul( l.highest ) );
  79. return l.contrast.lessThan( threshold );
  80. };
  81. const SampleLuminanceNeighborhood = ( texSize, uv ) => {
  82. const m = SampleLuminance( uv );
  83. const n = SampleLuminanceOffset( texSize, uv, 0.0, - 1.0 );
  84. const e = SampleLuminanceOffset( texSize, uv, 1.0, 0.0 );
  85. const s = SampleLuminanceOffset( texSize, uv, 0.0, 1.0 );
  86. const w = SampleLuminanceOffset( texSize, uv, - 1.0, 0.0 );
  87. const ne = SampleLuminanceOffset( texSize, uv, 1.0, - 1.0 );
  88. const nw = SampleLuminanceOffset( texSize, uv, - 1.0, - 1.0 );
  89. const se = SampleLuminanceOffset( texSize, uv, 1.0, 1.0 );
  90. const sw = SampleLuminanceOffset( texSize, uv, - 1.0, 1.0 );
  91. const highest = max( max( max( max( s, e ), n ), w ), m );
  92. const lowest = min( min( min( min( s, e ), n ), w ), m );
  93. const contrast = highest.sub( lowest );
  94. return { m, n, e, s, w, ne, nw, se, sw, highest, lowest, contrast };
  95. };
  96. const DeterminePixelBlendFactor = ( l ) => {
  97. let f = float( 2.0 ).mul( l.s.add( l.e ).add( l.n ).add( l.w ) );
  98. f = f.add( l.se.add( l.sw ).add( l.ne ).add( l.nw ) );
  99. f = f.mul( 1.0 / 12.0 );
  100. f = abs( f.sub( l.m ) );
  101. f = clamp( f.div( max( l.contrast, 0 ) ), 0.0, 1.0 );
  102. const blendFactor = smoothstep( 0.0, 1.0, f );
  103. return blendFactor.mul( blendFactor ).mul( _SubpixelBlending );
  104. };
  105. const DetermineEdge = ( texSize, l ) => {
  106. const horizontal =
  107. abs( l.s.add( l.n ).sub( l.m.mul( 2.0 ) ) ).mul( 2.0 ).add(
  108. abs( l.se.add( l.ne ).sub( l.e.mul( 2.0 ) ) ).add(
  109. abs( l.sw.add( l.nw ).sub( l.w.mul( 2.0 ) ) )
  110. )
  111. );
  112. const vertical =
  113. abs( l.e.add( l.w ).sub( l.m.mul( 2.0 ) ) ).mul( 2.0 ).add(
  114. abs( l.se.add( l.sw ).sub( l.s.mul( 2.0 ) ) ).add(
  115. abs( l.ne.add( l.nw ).sub( l.n.mul( 2.0 ) ) )
  116. )
  117. );
  118. const isHorizontal = horizontal.greaterThanEqual( vertical );
  119. const pLuminance = select( isHorizontal, l.s, l.e );
  120. const nLuminance = select( isHorizontal, l.n, l.w );
  121. const pGradient = abs( pLuminance.sub( l.m ) );
  122. const nGradient = abs( nLuminance.sub( l.m ) );
  123. const pixelStep = select( isHorizontal, texSize.y, texSize.x ).toVar();
  124. const oppositeLuminance = float().toVar();
  125. const gradient = float().toVar();
  126. If( pGradient.lessThan( nGradient ), () => {
  127. pixelStep.assign( pixelStep.negate() );
  128. oppositeLuminance.assign( nLuminance );
  129. gradient.assign( nGradient );
  130. } ).Else( () => {
  131. oppositeLuminance.assign( pLuminance );
  132. gradient.assign( pGradient );
  133. } );
  134. return { isHorizontal, pixelStep, oppositeLuminance, gradient };
  135. };
  136. const DetermineEdgeBlendFactor = ( texSize, l, e, uv ) => {
  137. const uvEdge = uv.toVar();
  138. const edgeStep = vec2().toVar();
  139. If( e.isHorizontal, () => {
  140. uvEdge.y.addAssign( e.pixelStep.mul( 0.5 ) );
  141. edgeStep.assign( vec2( texSize.x, 0.0 ) );
  142. } ).Else( () => {
  143. uvEdge.x.addAssign( e.pixelStep.mul( 0.5 ) );
  144. edgeStep.assign( vec2( 0.0, texSize.y ) );
  145. } );
  146. const edgeLuminance = l.m.add( e.oppositeLuminance ).mul( 0.5 );
  147. const gradientThreshold = e.gradient.mul( 0.25 );
  148. const puv = uvEdge.add( edgeStep.mul( EDGE_STEPS.element( 0 ) ) ).toVar();
  149. const pLuminanceDelta = SampleLuminance( puv ).sub( edgeLuminance ).toVar();
  150. const pAtEnd = abs( pLuminanceDelta ).greaterThanEqual( gradientThreshold ).toVar();
  151. Loop( { start: 1, end: EDGE_STEP_COUNT }, ( { i } ) => {
  152. If( pAtEnd, () => {
  153. Break();
  154. } );
  155. puv.addAssign( edgeStep.mul( EDGE_STEPS.element( i ) ) );
  156. pLuminanceDelta.assign( SampleLuminance( puv ).sub( edgeLuminance ) );
  157. pAtEnd.assign( abs( pLuminanceDelta ).greaterThanEqual( gradientThreshold ) );
  158. } );
  159. If( pAtEnd.not(), () => {
  160. puv.addAssign( edgeStep.mul( EDGE_GUESS ) );
  161. } );
  162. const nuv = uvEdge.sub( edgeStep.mul( EDGE_STEPS.element( 0 ) ) ).toVar();
  163. const nLuminanceDelta = SampleLuminance( nuv ).sub( edgeLuminance ).toVar();
  164. const nAtEnd = abs( nLuminanceDelta ).greaterThanEqual( gradientThreshold ).toVar();
  165. Loop( { start: 1, end: EDGE_STEP_COUNT }, ( { i } ) => {
  166. If( nAtEnd, () => {
  167. Break();
  168. } );
  169. nuv.subAssign( edgeStep.mul( EDGE_STEPS.element( i ) ) );
  170. nLuminanceDelta.assign( SampleLuminance( nuv ).sub( edgeLuminance ) );
  171. nAtEnd.assign( abs( nLuminanceDelta ).greaterThanEqual( gradientThreshold ) );
  172. } );
  173. If( nAtEnd.not(), () => {
  174. nuv.subAssign( edgeStep.mul( EDGE_GUESS ) );
  175. } );
  176. const pDistance = float().toVar();
  177. const nDistance = float().toVar();
  178. If( e.isHorizontal, () => {
  179. pDistance.assign( puv.x.sub( uv.x ) );
  180. nDistance.assign( uv.x.sub( nuv.x ) );
  181. } ).Else( () => {
  182. pDistance.assign( puv.y.sub( uv.y ) );
  183. nDistance.assign( uv.y.sub( nuv.y ) );
  184. } );
  185. const shortestDistance = float().toVar();
  186. const deltaSign = bool().toVar();
  187. If( pDistance.lessThanEqual( nDistance ), () => {
  188. shortestDistance.assign( pDistance );
  189. deltaSign.assign( pLuminanceDelta.greaterThanEqual( 0.0 ) );
  190. } ).Else( () => {
  191. shortestDistance.assign( nDistance );
  192. deltaSign.assign( nLuminanceDelta.greaterThanEqual( 0.0 ) );
  193. } );
  194. const blendFactor = float().toVar();
  195. If( deltaSign.equal( l.m.sub( edgeLuminance ).greaterThanEqual( 0.0 ) ), () => {
  196. blendFactor.assign( 0.0 );
  197. } ).Else( () => {
  198. blendFactor.assign( float( 0.5 ).sub( shortestDistance.div( pDistance.add( nDistance ) ) ) );
  199. } );
  200. return blendFactor;
  201. };
  202. const ApplyFXAA = Fn( ( [ uv, texSize ] ) => {
  203. const luminance = SampleLuminanceNeighborhood( texSize, uv );
  204. If( ShouldSkipPixel( luminance ), () => {
  205. return Sample( uv );
  206. } );
  207. const pixelBlend = DeterminePixelBlendFactor( luminance );
  208. const edge = DetermineEdge( texSize, luminance );
  209. const edgeBlend = DetermineEdgeBlendFactor( texSize, luminance, edge, uv );
  210. const finalBlend = max( pixelBlend, edgeBlend );
  211. const finalUv = uv.toVar();
  212. If( edge.isHorizontal, () => {
  213. finalUv.y.addAssign( edge.pixelStep.mul( finalBlend ) );
  214. } ).Else( () => {
  215. finalUv.x.addAssign( edge.pixelStep.mul( finalBlend ) );
  216. } );
  217. return Sample( finalUv );
  218. } ).setLayout( {
  219. name: 'FxaaPixelShader',
  220. type: 'vec4',
  221. inputs: [
  222. { name: 'uv', type: 'vec2' },
  223. { name: 'texSize', type: 'vec2' },
  224. ]
  225. } );
  226. const fxaa = Fn( () => {
  227. return ApplyFXAA( uvNode, this._invSize );
  228. } );
  229. const outputNode = fxaa();
  230. return outputNode;
  231. }
  232. }
  233. export default FXAANode;
  234. /**
  235. * TSL function for creating a FXAA node for anti-aliasing via post processing.
  236. *
  237. * @tsl
  238. * @function
  239. * @param {Node<vec4>} node - The node that represents the input of the effect.
  240. * @returns {FXAANode}
  241. */
  242. export const fxaa = ( node ) => nodeObject( new FXAANode( convertToTexture( node ) ) );