BloomNode.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. import { HalfFloatType, RenderTarget, Vector2, Vector3, TempNode, QuadMesh, NodeMaterial, RendererUtils, NodeUpdateType } from 'three/webgpu';
  2. import { nodeObject, Fn, float, uv, passTexture, uniform, Loop, texture, luminance, smoothstep, mix, vec4, uniformArray, add, int } from 'three/tsl';
  3. const _quadMesh = /*@__PURE__*/ new QuadMesh();
  4. const _size = /*@__PURE__*/ new Vector2();
  5. const _BlurDirectionX = /*@__PURE__*/ new Vector2( 1.0, 0.0 );
  6. const _BlurDirectionY = /*@__PURE__*/ new Vector2( 0.0, 1.0 );
  7. let _rendererState;
  8. /**
  9. * Post processing node for creating a bloom effect.
  10. * ```js
  11. * const postProcessing = new THREE.PostProcessing( renderer );
  12. *
  13. * const scenePass = pass( scene, camera );
  14. * const scenePassColor = scenePass.getTextureNode( 'output' );
  15. *
  16. * const bloomPass = bloom( scenePassColor );
  17. *
  18. * postProcessing.outputNode = scenePassColor.add( bloomPass );
  19. * ```
  20. * By default, the node affects the entire image. For a selective bloom,
  21. * use the `emissive` material property to control which objects should
  22. * contribute to bloom or not. This can be achieved via MRT.
  23. * ```js
  24. * const postProcessing = new THREE.PostProcessing( renderer );
  25. *
  26. * const scenePass = pass( scene, camera );
  27. * scenePass.setMRT( mrt( {
  28. * output,
  29. * emissive
  30. * } ) );
  31. *
  32. * const scenePassColor = scenePass.getTextureNode( 'output' );
  33. * const emissivePass = scenePass.getTextureNode( 'emissive' );
  34. *
  35. * const bloomPass = bloom( emissivePass );
  36. * postProcessing.outputNode = scenePassColor.add( bloomPass );
  37. * ```
  38. * @augments TempNode
  39. * @three_import import { bloom } from 'three/addons/tsl/display/BloomNode.js';
  40. */
  41. class BloomNode extends TempNode {
  42. static get type() {
  43. return 'BloomNode';
  44. }
  45. /**
  46. * Constructs a new bloom node.
  47. *
  48. * @param {Node<vec4>} inputNode - The node that represents the input of the effect.
  49. * @param {number} [strength=1] - The strength of the bloom.
  50. * @param {number} [radius=0] - The radius of the bloom.
  51. * @param {number} [threshold=0] - The luminance threshold limits which bright areas contribute to the bloom effect.
  52. */
  53. constructor( inputNode, strength = 1, radius = 0, threshold = 0 ) {
  54. super( 'vec4' );
  55. /**
  56. * The node that represents the input of the effect.
  57. *
  58. * @type {Node<vec4>}
  59. */
  60. this.inputNode = inputNode;
  61. /**
  62. * The strength of the bloom.
  63. *
  64. * @type {UniformNode<float>}
  65. */
  66. this.strength = uniform( strength );
  67. /**
  68. * The radius of the bloom.
  69. *
  70. * @type {UniformNode<float>}
  71. */
  72. this.radius = uniform( radius );
  73. /**
  74. * The luminance threshold limits which bright areas contribute to the bloom effect.
  75. *
  76. * @type {UniformNode<float>}
  77. */
  78. this.threshold = uniform( threshold );
  79. /**
  80. * Can be used to tweak the extracted luminance from the scene.
  81. *
  82. * @type {UniformNode<float>}
  83. */
  84. this.smoothWidth = uniform( 0.01 );
  85. /**
  86. * An array that holds the render targets for the horizontal blur passes.
  87. *
  88. * @private
  89. * @type {Array<RenderTarget>}
  90. */
  91. this._renderTargetsHorizontal = [];
  92. /**
  93. * An array that holds the render targets for the vertical blur passes.
  94. *
  95. * @private
  96. * @type {Array<RenderTarget>}
  97. */
  98. this._renderTargetsVertical = [];
  99. /**
  100. * The number if blur mips.
  101. *
  102. * @private
  103. * @type {number}
  104. */
  105. this._nMips = 5;
  106. /**
  107. * The render target for the luminance pass.
  108. *
  109. * @private
  110. * @type {RenderTarget}
  111. */
  112. this._renderTargetBright = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
  113. this._renderTargetBright.texture.name = 'UnrealBloomPass.bright';
  114. this._renderTargetBright.texture.generateMipmaps = false;
  115. //
  116. for ( let i = 0; i < this._nMips; i ++ ) {
  117. const renderTargetHorizontal = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
  118. renderTargetHorizontal.texture.name = 'UnrealBloomPass.h' + i;
  119. renderTargetHorizontal.texture.generateMipmaps = false;
  120. this._renderTargetsHorizontal.push( renderTargetHorizontal );
  121. const renderTargetVertical = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
  122. renderTargetVertical.texture.name = 'UnrealBloomPass.v' + i;
  123. renderTargetVertical.texture.generateMipmaps = false;
  124. this._renderTargetsVertical.push( renderTargetVertical );
  125. }
  126. /**
  127. * The material for the composite pass.
  128. *
  129. * @private
  130. * @type {?NodeMaterial}
  131. */
  132. this._compositeMaterial = null;
  133. /**
  134. * The material for the luminance pass.
  135. *
  136. * @private
  137. * @type {?NodeMaterial}
  138. */
  139. this._highPassFilterMaterial = null;
  140. /**
  141. * The materials for the blur pass.
  142. *
  143. * @private
  144. * @type {Array<NodeMaterial>}
  145. */
  146. this._separableBlurMaterials = [];
  147. /**
  148. * The result of the luminance pass as a texture node for further processing.
  149. *
  150. * @private
  151. * @type {TextureNode}
  152. */
  153. this._textureNodeBright = texture( this._renderTargetBright.texture );
  154. /**
  155. * The result of the first blur pass as a texture node for further processing.
  156. *
  157. * @private
  158. * @type {TextureNode}
  159. */
  160. this._textureNodeBlur0 = texture( this._renderTargetsVertical[ 0 ].texture );
  161. /**
  162. * The result of the second blur pass as a texture node for further processing.
  163. *
  164. * @private
  165. * @type {TextureNode}
  166. */
  167. this._textureNodeBlur1 = texture( this._renderTargetsVertical[ 1 ].texture );
  168. /**
  169. * The result of the third blur pass as a texture node for further processing.
  170. *
  171. * @private
  172. * @type {TextureNode}
  173. */
  174. this._textureNodeBlur2 = texture( this._renderTargetsVertical[ 2 ].texture );
  175. /**
  176. * The result of the fourth blur pass as a texture node for further processing.
  177. *
  178. * @private
  179. * @type {TextureNode}
  180. */
  181. this._textureNodeBlur3 = texture( this._renderTargetsVertical[ 3 ].texture );
  182. /**
  183. * The result of the fifth blur pass as a texture node for further processing.
  184. *
  185. * @private
  186. * @type {TextureNode}
  187. */
  188. this._textureNodeBlur4 = texture( this._renderTargetsVertical[ 4 ].texture );
  189. /**
  190. * The result of the effect is represented as a separate texture node.
  191. *
  192. * @private
  193. * @type {PassTextureNode}
  194. */
  195. this._textureOutput = passTexture( this, this._renderTargetsHorizontal[ 0 ].texture );
  196. /**
  197. * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders
  198. * its effect once per frame in `updateBefore()`.
  199. *
  200. * @type {string}
  201. * @default 'frame'
  202. */
  203. this.updateBeforeType = NodeUpdateType.FRAME;
  204. }
  205. /**
  206. * Returns the result of the effect as a texture node.
  207. *
  208. * @return {PassTextureNode} A texture node that represents the result of the effect.
  209. */
  210. getTextureNode() {
  211. return this._textureOutput;
  212. }
  213. /**
  214. * Sets the size of the effect.
  215. *
  216. * @param {number} width - The width of the effect.
  217. * @param {number} height - The height of the effect.
  218. */
  219. setSize( width, height ) {
  220. let resx = Math.round( width / 2 );
  221. let resy = Math.round( height / 2 );
  222. this._renderTargetBright.setSize( resx, resy );
  223. for ( let i = 0; i < this._nMips; i ++ ) {
  224. this._renderTargetsHorizontal[ i ].setSize( resx, resy );
  225. this._renderTargetsVertical[ i ].setSize( resx, resy );
  226. this._separableBlurMaterials[ i ].invSize.value.set( 1 / resx, 1 / resy );
  227. resx = Math.round( resx / 2 );
  228. resy = Math.round( resy / 2 );
  229. }
  230. }
  231. /**
  232. * This method is used to render the effect once per frame.
  233. *
  234. * @param {NodeFrame} frame - The current node frame.
  235. */
  236. updateBefore( frame ) {
  237. const { renderer } = frame;
  238. _rendererState = RendererUtils.resetRendererState( renderer, _rendererState );
  239. //
  240. const size = renderer.getDrawingBufferSize( _size );
  241. this.setSize( size.width, size.height );
  242. // 1. Extract bright areas
  243. renderer.setRenderTarget( this._renderTargetBright );
  244. _quadMesh.material = this._highPassFilterMaterial;
  245. _quadMesh.render( renderer );
  246. // 2. Blur all the mips progressively
  247. let inputRenderTarget = this._renderTargetBright;
  248. for ( let i = 0; i < this._nMips; i ++ ) {
  249. _quadMesh.material = this._separableBlurMaterials[ i ];
  250. this._separableBlurMaterials[ i ].colorTexture.value = inputRenderTarget.texture;
  251. this._separableBlurMaterials[ i ].direction.value = _BlurDirectionX;
  252. renderer.setRenderTarget( this._renderTargetsHorizontal[ i ] );
  253. _quadMesh.render( renderer );
  254. this._separableBlurMaterials[ i ].colorTexture.value = this._renderTargetsHorizontal[ i ].texture;
  255. this._separableBlurMaterials[ i ].direction.value = _BlurDirectionY;
  256. renderer.setRenderTarget( this._renderTargetsVertical[ i ] );
  257. _quadMesh.render( renderer );
  258. inputRenderTarget = this._renderTargetsVertical[ i ];
  259. }
  260. // 3. Composite all the mips
  261. renderer.setRenderTarget( this._renderTargetsHorizontal[ 0 ] );
  262. _quadMesh.material = this._compositeMaterial;
  263. _quadMesh.render( renderer );
  264. // restore
  265. RendererUtils.restoreRendererState( renderer, _rendererState );
  266. }
  267. /**
  268. * This method is used to setup the effect's TSL code.
  269. *
  270. * @param {NodeBuilder} builder - The current node builder.
  271. * @return {PassTextureNode}
  272. */
  273. setup( builder ) {
  274. // luminosity high pass material
  275. const luminosityHighPass = Fn( () => {
  276. const texel = this.inputNode;
  277. const v = luminance( texel.rgb );
  278. const alpha = smoothstep( this.threshold, this.threshold.add( this.smoothWidth ), v );
  279. return mix( vec4( 0 ), texel, alpha );
  280. } );
  281. this._highPassFilterMaterial = this._highPassFilterMaterial || new NodeMaterial();
  282. this._highPassFilterMaterial.fragmentNode = luminosityHighPass().context( builder.getSharedContext() );
  283. this._highPassFilterMaterial.name = 'Bloom_highPass';
  284. this._highPassFilterMaterial.needsUpdate = true;
  285. // gaussian blur materials
  286. const kernelSizeArray = [ 3, 5, 7, 9, 11 ];
  287. for ( let i = 0; i < this._nMips; i ++ ) {
  288. this._separableBlurMaterials.push( this._getSeparableBlurMaterial( builder, kernelSizeArray[ i ] ) );
  289. }
  290. // composite material
  291. const bloomFactors = uniformArray( [ 1.0, 0.8, 0.6, 0.4, 0.2 ] );
  292. const bloomTintColors = uniformArray( [ new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ) ] );
  293. const lerpBloomFactor = Fn( ( [ factor, radius ] ) => {
  294. const mirrorFactor = float( 1.2 ).sub( factor );
  295. return mix( factor, mirrorFactor, radius );
  296. } ).setLayout( {
  297. name: 'lerpBloomFactor',
  298. type: 'float',
  299. inputs: [
  300. { name: 'factor', type: 'float' },
  301. { name: 'radius', type: 'float' },
  302. ]
  303. } );
  304. const compositePass = Fn( () => {
  305. const color0 = lerpBloomFactor( bloomFactors.element( 0 ), this.radius ).mul( vec4( bloomTintColors.element( 0 ), 1.0 ) ).mul( this._textureNodeBlur0 );
  306. const color1 = lerpBloomFactor( bloomFactors.element( 1 ), this.radius ).mul( vec4( bloomTintColors.element( 1 ), 1.0 ) ).mul( this._textureNodeBlur1 );
  307. const color2 = lerpBloomFactor( bloomFactors.element( 2 ), this.radius ).mul( vec4( bloomTintColors.element( 2 ), 1.0 ) ).mul( this._textureNodeBlur2 );
  308. const color3 = lerpBloomFactor( bloomFactors.element( 3 ), this.radius ).mul( vec4( bloomTintColors.element( 3 ), 1.0 ) ).mul( this._textureNodeBlur3 );
  309. const color4 = lerpBloomFactor( bloomFactors.element( 4 ), this.radius ).mul( vec4( bloomTintColors.element( 4 ), 1.0 ) ).mul( this._textureNodeBlur4 );
  310. const sum = color0.add( color1 ).add( color2 ).add( color3 ).add( color4 );
  311. return sum.mul( this.strength );
  312. } );
  313. this._compositeMaterial = this._compositeMaterial || new NodeMaterial();
  314. this._compositeMaterial.fragmentNode = compositePass().context( builder.getSharedContext() );
  315. this._compositeMaterial.name = 'Bloom_comp';
  316. this._compositeMaterial.needsUpdate = true;
  317. //
  318. return this._textureOutput;
  319. }
  320. /**
  321. * Frees internal resources. This method should be called
  322. * when the effect is no longer required.
  323. */
  324. dispose() {
  325. for ( let i = 0; i < this._renderTargetsHorizontal.length; i ++ ) {
  326. this._renderTargetsHorizontal[ i ].dispose();
  327. }
  328. for ( let i = 0; i < this._renderTargetsVertical.length; i ++ ) {
  329. this._renderTargetsVertical[ i ].dispose();
  330. }
  331. this._renderTargetBright.dispose();
  332. }
  333. /**
  334. * Create a separable blur material for the given kernel radius.
  335. *
  336. * @param {NodeBuilder} builder - The current node builder.
  337. * @param {number} kernelRadius - The kernel radius.
  338. * @return {NodeMaterial}
  339. */
  340. _getSeparableBlurMaterial( builder, kernelRadius ) {
  341. const coefficients = [];
  342. for ( let i = 0; i < kernelRadius; i ++ ) {
  343. coefficients.push( 0.39894 * Math.exp( - 0.5 * i * i / ( kernelRadius * kernelRadius ) ) / kernelRadius );
  344. }
  345. //
  346. const colorTexture = texture( null );
  347. const gaussianCoefficients = uniformArray( coefficients );
  348. const invSize = uniform( new Vector2() );
  349. const direction = uniform( new Vector2( 0.5, 0.5 ) );
  350. const uvNode = uv();
  351. const sampleTexel = ( uv ) => colorTexture.sample( uv );
  352. const separableBlurPass = Fn( () => {
  353. const weightSum = gaussianCoefficients.element( 0 ).toVar();
  354. const diffuseSum = sampleTexel( uvNode ).rgb.mul( weightSum ).toVar();
  355. Loop( { start: int( 1 ), end: int( kernelRadius ), type: 'int', condition: '<' }, ( { i } ) => {
  356. const x = float( i );
  357. const w = gaussianCoefficients.element( i );
  358. const uvOffset = direction.mul( invSize ).mul( x );
  359. const sample1 = sampleTexel( uvNode.add( uvOffset ) ).rgb;
  360. const sample2 = sampleTexel( uvNode.sub( uvOffset ) ).rgb;
  361. diffuseSum.addAssign( add( sample1, sample2 ).mul( w ) );
  362. weightSum.addAssign( float( 2.0 ).mul( w ) );
  363. } );
  364. return vec4( diffuseSum.div( weightSum ), 1.0 );
  365. } );
  366. const separableBlurMaterial = new NodeMaterial();
  367. separableBlurMaterial.fragmentNode = separableBlurPass().context( builder.getSharedContext() );
  368. separableBlurMaterial.name = 'Bloom_separable';
  369. separableBlurMaterial.needsUpdate = true;
  370. // uniforms
  371. separableBlurMaterial.colorTexture = colorTexture;
  372. separableBlurMaterial.direction = direction;
  373. separableBlurMaterial.invSize = invSize;
  374. return separableBlurMaterial;
  375. }
  376. }
  377. /**
  378. * TSL function for creating a bloom effect.
  379. *
  380. * @tsl
  381. * @function
  382. * @param {Node<vec4>} node - The node that represents the input of the effect.
  383. * @param {number} [strength=1] - The strength of the bloom.
  384. * @param {number} [radius=0] - The radius of the bloom.
  385. * @param {number} [threshold=0] - The luminance threshold limits which bright areas contribute to the bloom effect.
  386. * @returns {BloomNode}
  387. */
  388. export const bloom = ( node, strength, radius, threshold ) => nodeObject( new BloomNode( nodeObject( node ), strength, radius, threshold ) );
  389. export default BloomNode;