BloomNode.js 14 KB

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