PixelationPassNode.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. import { NearestFilter, Vector4, TempNode, NodeUpdateType, PassNode } from 'three/webgpu';
  2. import { nodeObject, Fn, float, uv, uniform, convertToTexture, vec2, vec3, clamp, floor, dot, smoothstep, If, sign, step, mrt, output, normalView, property } from 'three/tsl';
  3. /**
  4. * A inner node definition that implements the actual pixelation TSL code.
  5. *
  6. * @inner
  7. * @augments TempNode
  8. */
  9. class PixelationNode extends TempNode {
  10. static get type() {
  11. return 'PixelationNode';
  12. }
  13. /**
  14. * Constructs a new pixelation node.
  15. *
  16. * @param {TextureNode} textureNode - The texture node that represents the beauty pass.
  17. * @param {TextureNode} depthNode - The texture that represents the beauty's depth.
  18. * @param {TextureNode} normalNode - The texture that represents the beauty's normals.
  19. * @param {Node<float>} pixelSize - The pixel size.
  20. * @param {Node<float>} normalEdgeStrength - The normal edge strength.
  21. * @param {Node<float>} depthEdgeStrength - The depth edge strength.
  22. */
  23. constructor( textureNode, depthNode, normalNode, pixelSize, normalEdgeStrength, depthEdgeStrength ) {
  24. super( 'vec4' );
  25. /**
  26. * The texture node that represents the beauty pass.
  27. *
  28. * @type {TextureNode}
  29. */
  30. this.textureNode = textureNode;
  31. /**
  32. * The texture that represents the beauty's depth.
  33. *
  34. * @type {TextureNode}
  35. */
  36. this.depthNode = depthNode;
  37. /**
  38. * The texture that represents the beauty's normals.
  39. *
  40. * @type {TextureNode}
  41. */
  42. this.normalNode = normalNode;
  43. /**
  44. * The pixel size.
  45. *
  46. * @type {Node<float>}
  47. */
  48. this.pixelSize = pixelSize;
  49. /**
  50. * The pixel size.
  51. *
  52. * @type {Node<float>}
  53. */
  54. this.normalEdgeStrength = normalEdgeStrength;
  55. /**
  56. * The depth edge strength.
  57. *
  58. * @type {Node<float>}
  59. */
  60. this.depthEdgeStrength = depthEdgeStrength;
  61. /**
  62. * Uniform node that represents the resolution.
  63. *
  64. * @type {Node<vec4>}
  65. */
  66. this._resolution = uniform( new Vector4() );
  67. /**
  68. * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node updates
  69. * its internal uniforms once per frame in `updateBefore()`.
  70. *
  71. * @type {string}
  72. * @default 'frame'
  73. */
  74. this.updateBeforeType = NodeUpdateType.FRAME;
  75. }
  76. /**
  77. * This method is used to update uniforms once per frame.
  78. *
  79. * @param {NodeFrame} frame - The current node frame.
  80. */
  81. updateBefore() {
  82. const map = this.textureNode.value;
  83. const width = map.image.width;
  84. const height = map.image.height;
  85. this._resolution.value.set( width, height, 1 / width, 1 / height );
  86. }
  87. /**
  88. * This method is used to setup the effect's TSL code.
  89. *
  90. * @param {NodeBuilder} builder - The current node builder.
  91. * @return {ShaderCallNodeInternal}
  92. */
  93. setup() {
  94. const { textureNode, depthNode, normalNode } = this;
  95. const uvNodeTexture = textureNode.uvNode || uv();
  96. const uvNodeDepth = depthNode.uvNode || uv();
  97. const uvNodeNormal = normalNode.uvNode || uv();
  98. const sampleTexture = () => textureNode.sample( uvNodeTexture );
  99. const sampleDepth = ( x, y ) => depthNode.sample( uvNodeDepth.add( vec2( x, y ).mul( this._resolution.zw ) ) ).r;
  100. const sampleNormal = ( x, y ) => normalNode.sample( uvNodeNormal.add( vec2( x, y ).mul( this._resolution.zw ) ) ).rgb.normalize();
  101. const depthEdgeIndicator = ( depth ) => {
  102. const diff = property( 'float', 'diff' );
  103. diff.addAssign( clamp( sampleDepth( 1, 0 ).sub( depth ) ) );
  104. diff.addAssign( clamp( sampleDepth( - 1, 0 ).sub( depth ) ) );
  105. diff.addAssign( clamp( sampleDepth( 0, 1 ).sub( depth ) ) );
  106. diff.addAssign( clamp( sampleDepth( 0, - 1 ).sub( depth ) ) );
  107. return floor( smoothstep( 0.01, 0.02, diff ).mul( 2 ) ).div( 2 );
  108. };
  109. const neighborNormalEdgeIndicator = ( x, y, depth, normal ) => {
  110. const depthDiff = sampleDepth( x, y ).sub( depth );
  111. const neighborNormal = sampleNormal( x, y );
  112. // Edge pixels should yield to faces who's normals are closer to the bias normal.
  113. const normalEdgeBias = vec3( 1, 1, 1 ); // This should probably be a parameter.
  114. const normalDiff = dot( normal.sub( neighborNormal ), normalEdgeBias );
  115. const normalIndicator = clamp( smoothstep( - 0.01, 0.01, normalDiff ), 0.0, 1.0 );
  116. // Only the shallower pixel should detect the normal edge.
  117. const depthIndicator = clamp( sign( depthDiff.mul( .25 ).add( .0025 ) ), 0.0, 1.0 );
  118. return float( 1.0 ).sub( dot( normal, neighborNormal ) ).mul( depthIndicator ).mul( normalIndicator );
  119. };
  120. const normalEdgeIndicator = ( depth, normal ) => {
  121. const indicator = property( 'float', 'indicator' );
  122. indicator.addAssign( neighborNormalEdgeIndicator( 0, - 1, depth, normal ) );
  123. indicator.addAssign( neighborNormalEdgeIndicator( 0, 1, depth, normal ) );
  124. indicator.addAssign( neighborNormalEdgeIndicator( - 1, 0, depth, normal ) );
  125. indicator.addAssign( neighborNormalEdgeIndicator( 1, 0, depth, normal ) );
  126. return step( 0.1, indicator );
  127. };
  128. const pixelation = Fn( () => {
  129. const texel = sampleTexture();
  130. const depth = property( 'float', 'depth' );
  131. const normal = property( 'vec3', 'normal' );
  132. If( this.depthEdgeStrength.greaterThan( 0.0 ).or( this.normalEdgeStrength.greaterThan( 0.0 ) ), () => {
  133. depth.assign( sampleDepth( 0, 0 ) );
  134. normal.assign( sampleNormal( 0, 0 ) );
  135. } );
  136. const dei = property( 'float', 'dei' );
  137. If( this.depthEdgeStrength.greaterThan( 0.0 ), () => {
  138. dei.assign( depthEdgeIndicator( depth ) );
  139. } );
  140. const nei = property( 'float', 'nei' );
  141. If( this.normalEdgeStrength.greaterThan( 0.0 ), () => {
  142. nei.assign( normalEdgeIndicator( depth, normal ) );
  143. } );
  144. const strength = dei.greaterThan( 0 ).select( float( 1.0 ).sub( dei.mul( this.depthEdgeStrength ) ), nei.mul( this.normalEdgeStrength ).add( 1 ) );
  145. return texel.mul( strength );
  146. } );
  147. const outputNode = pixelation();
  148. return outputNode;
  149. }
  150. }
  151. const pixelation = ( node, depthNode, normalNode, pixelSize = 6, normalEdgeStrength = 0.3, depthEdgeStrength = 0.4 ) => nodeObject( new PixelationNode( convertToTexture( node ), convertToTexture( depthNode ), convertToTexture( normalNode ), nodeObject( pixelSize ), nodeObject( normalEdgeStrength ), nodeObject( depthEdgeStrength ) ) );
  152. /**
  153. * A special render pass node that renders the scene with a pixelation effect.
  154. *
  155. * @augments PassNode
  156. * @three_import import { pixelationPass } from 'three/addons/tsl/display/PixelationPassNode.js';
  157. */
  158. class PixelationPassNode extends PassNode {
  159. static get type() {
  160. return 'PixelationPassNode';
  161. }
  162. /**
  163. * Constructs a new pixelation pass node.
  164. *
  165. * @param {Scene} scene - The scene to render.
  166. * @param {Camera} camera - The camera to render the scene with.
  167. * @param {Node<float> | number} [pixelSize=6] - The pixel size.
  168. * @param {Node<float> | number} [normalEdgeStrength=0.3] - The normal edge strength.
  169. * @param {Node<float> | number} [depthEdgeStrength=0.4] - The depth edge strength.
  170. */
  171. constructor( scene, camera, pixelSize = 6, normalEdgeStrength = 0.3, depthEdgeStrength = 0.4 ) {
  172. super( PassNode.COLOR, scene, camera, { minFilter: NearestFilter, magFilter: NearestFilter } );
  173. /**
  174. * The pixel size.
  175. *
  176. * @type {number}
  177. * @default 6
  178. */
  179. this.pixelSize = pixelSize;
  180. /**
  181. * The normal edge strength.
  182. *
  183. * @type {number}
  184. * @default 0.3
  185. */
  186. this.normalEdgeStrength = normalEdgeStrength;
  187. /**
  188. * The depth edge strength.
  189. *
  190. * @type {number}
  191. * @default 0.4
  192. */
  193. this.depthEdgeStrength = depthEdgeStrength;
  194. /**
  195. * This flag can be used for type testing.
  196. *
  197. * @type {boolean}
  198. * @readonly
  199. * @default true
  200. */
  201. this.isPixelationPassNode = true;
  202. this._mrt = mrt( {
  203. output: output,
  204. normal: normalView
  205. } );
  206. }
  207. /**
  208. * Sets the size of the pass.
  209. *
  210. * @param {number} width - The width of the pass.
  211. * @param {number} height - The height of the pass.
  212. */
  213. setSize( width, height ) {
  214. const pixelSize = this.pixelSize.value ? this.pixelSize.value : this.pixelSize;
  215. const adjustedWidth = Math.floor( width / pixelSize );
  216. const adjustedHeight = Math.floor( height / pixelSize );
  217. super.setSize( adjustedWidth, adjustedHeight );
  218. }
  219. /**
  220. * This method is used to setup the effect's TSL code.
  221. *
  222. * @param {NodeBuilder} builder - The current node builder.
  223. * @return {PixelationNode}
  224. */
  225. setup() {
  226. const color = super.getTextureNode( 'output' );
  227. const depth = super.getTextureNode( 'depth' );
  228. const normal = super.getTextureNode( 'normal' );
  229. return pixelation( color, depth, normal, this.pixelSize, this.normalEdgeStrength, this.depthEdgeStrength );
  230. }
  231. }
  232. /**
  233. * TSL function for creating a pixelation render pass node for post processing.
  234. *
  235. * @tsl
  236. * @function
  237. * @param {Scene} scene - The scene to render.
  238. * @param {Camera} camera - The camera to render the scene with.
  239. * @param {Node<float> | number} [pixelSize=6] - The pixel size.
  240. * @param {Node<float> | number} [normalEdgeStrength=0.3] - The normal edge strength.
  241. * @param {Node<float> | number} [depthEdgeStrength=0.4] - The depth edge strength.
  242. * @returns {PixelationPassNode}
  243. */
  244. export const pixelationPass = ( scene, camera, pixelSize, normalEdgeStrength, depthEdgeStrength ) => nodeObject( new PixelationPassNode( scene, camera, pixelSize, normalEdgeStrength, depthEdgeStrength ) );
  245. export default PixelationPassNode;