BloomNode.js 17 KB

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