DualKawaseBloomNode.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. import { HalfFloatType, RenderTarget, Vector2, TempNode, QuadMesh, NodeMaterial, RendererUtils, NodeUpdateType } from 'three/webgpu';
  2. import { nodeObject, Fn, float, vec2, uv, passTexture, uniform, texture, luminance, smoothstep, mix, vec4 } from 'three/tsl';
  3. const _quadMesh = /*@__PURE__*/ new QuadMesh();
  4. const _size = /*@__PURE__*/ new Vector2();
  5. let _rendererState;
  6. const luminosityHighPass = Fn( ( { input, threshold, smoothWidth } ) => {
  7. const v = luminance( input.rgb );
  8. const alpha = smoothstep( threshold, threshold.add( smoothWidth ), v );
  9. return mix( vec4( 0 ), input, alpha );
  10. } );
  11. /**
  12. * Post processing node for creating a bloom effect.
  13. *
  14. * The bloom is produced with a Dual Kawase blur: the bright areas are
  15. * progressively downsampled with a 5-tap filter and then upsampled with an
  16. * 8-tap filter, accumulating the levels back together.
  17. * ```js
  18. * const renderPipeline = new THREE.RenderPipeline( renderer );
  19. *
  20. * const scenePass = pass( scene, camera );
  21. * const scenePassColor = scenePass.getTextureNode( 'output' );
  22. *
  23. * const bloomPass = dualKawaseBloom( scenePassColor );
  24. *
  25. * renderPipeline.outputNode = scenePassColor.add( bloomPass );
  26. * ```
  27. * By default, the node affects the entire image. For a selective bloom,
  28. * use the `emissive` material property to control which objects should
  29. * contribute to bloom or not. This can be achieved via MRT.
  30. * ```js
  31. * const renderPipeline = new THREE.RenderPipeline( renderer );
  32. *
  33. * const scenePass = pass( scene, camera );
  34. * scenePass.setMRT( mrt( {
  35. * output,
  36. * emissive
  37. * } ) );
  38. *
  39. * const scenePassColor = scenePass.getTextureNode( 'output' );
  40. * const emissivePass = scenePass.getTextureNode( 'emissive' );
  41. *
  42. * const bloomPass = dualKawaseBloom( emissivePass );
  43. * renderPipeline.outputNode = scenePassColor.add( bloomPass );
  44. * ```
  45. * @augments TempNode
  46. * @three_import import { dualKawaseBloom } from 'three/addons/tsl/display/DualKawaseBloomNode.js';
  47. */
  48. class DualKawaseBloomNode extends TempNode {
  49. static get type() {
  50. return 'DualKawaseBloomNode';
  51. }
  52. /**
  53. * Constructs a new bloom node.
  54. *
  55. * @param {Node<vec4>} inputNode - The node that represents the input of the effect.
  56. * @param {number} [strength=1] - The strength of the bloom.
  57. * @param {number} [radius=0] - The radius of the bloom.
  58. * @param {number} [threshold=0] - The luminance threshold limits which bright areas contribute to the bloom effect.
  59. */
  60. constructor( inputNode, strength = 1, radius = 0, threshold = 0 ) {
  61. super( 'vec4' );
  62. /**
  63. * The node that represents the input of the effect.
  64. *
  65. * @type {Node<vec4>}
  66. */
  67. this.inputNode = inputNode;
  68. /**
  69. * The strength of the bloom.
  70. *
  71. * @type {UniformNode<float>}
  72. */
  73. this.strength = strength.isNode ? strength : uniform( strength );
  74. /**
  75. * The radius of the bloom. Must be in the range `[0,1]`.
  76. *
  77. * @type {UniformNode<float>}
  78. */
  79. this.radius = radius.isNode ? radius : uniform( radius );
  80. /**
  81. * The luminance threshold limits which bright areas contribute to the bloom effect.
  82. *
  83. * @type {UniformNode<float>}
  84. */
  85. this.threshold = threshold.isNode ? threshold : uniform( threshold );
  86. /**
  87. * Can be used to tweak the extracted luminance from the scene.
  88. *
  89. * @type {UniformNode<float>}
  90. */
  91. this.smoothWidth = uniform( 0.01 );
  92. /**
  93. * Scale factor for the internal render targets.
  94. *
  95. * @private
  96. * @type {number}
  97. * @default 0.5
  98. */
  99. this._resolutionScale = 0.5;
  100. /**
  101. * Can be used to inject a custom high pass filter (e.g., for anamorphic effects).
  102. *
  103. * @type {Function}
  104. */
  105. this.highPassFn = luminosityHighPass;
  106. /**
  107. * The number of downsample / upsample levels in the Dual Kawase pyramid.
  108. *
  109. * @private
  110. * @type {number}
  111. */
  112. this._nMips = 6;
  113. /**
  114. * Sample spread of the Dual Kawase filters. Kept small so each level's kernel
  115. * stays round (a wide offset makes the diamond sampling pattern visible as a
  116. * square halo); the bloom width comes from the depth of the pyramid instead.
  117. *
  118. * @private
  119. * @type {UniformNode<float>}
  120. */
  121. this._offset = uniform( 3 );
  122. /**
  123. * Per-level mixing factors, redistributed by `radius` between a tight and a wide bloom.
  124. * A linear ramp from `1.0` to `0.2` (mean `0.6`) keeps the total bloom energy constant
  125. * as `radius` shifts weight between the fine and coarse levels.
  126. *
  127. * @private
  128. * @type {Array<number>}
  129. */
  130. this._bloomFactors = [];
  131. for ( let i = 0; i < this._nMips; i ++ ) {
  132. this._bloomFactors.push( this._nMips === 1 ? 0.6 : 1.0 - 0.8 * i / ( this._nMips - 1 ) );
  133. }
  134. /**
  135. * The render target for the luminance pass.
  136. *
  137. * @private
  138. * @type {RenderTarget}
  139. */
  140. this._renderTargetBright = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
  141. this._renderTargetBright.texture.name = 'DualKawaseBloom.bright';
  142. this._renderTargetBright.texture.generateMipmaps = false;
  143. /**
  144. * The render targets for the downsample chain.
  145. *
  146. * @private
  147. * @type {Array<RenderTarget>}
  148. */
  149. this._downsampleRTs = [];
  150. /**
  151. * The render targets for the upsample / accumulation chain.
  152. *
  153. * @private
  154. * @type {Array<RenderTarget>}
  155. */
  156. this._accumRTs = [];
  157. /**
  158. * The resolution of each pyramid level.
  159. *
  160. * @private
  161. * @type {Array<Vector2>}
  162. */
  163. this._levelSizes = [];
  164. /**
  165. * The resolution of the bright pass.
  166. *
  167. * @private
  168. * @type {Vector2}
  169. */
  170. this._brightSize = new Vector2();
  171. for ( let i = 0; i < this._nMips; i ++ ) {
  172. const downsampleRT = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
  173. downsampleRT.texture.name = 'DualKawaseBloom.down' + i;
  174. downsampleRT.texture.generateMipmaps = false;
  175. this._downsampleRTs.push( downsampleRT );
  176. const accumRT = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
  177. accumRT.texture.name = 'DualKawaseBloom.up' + i;
  178. accumRT.texture.generateMipmaps = false;
  179. this._accumRTs.push( accumRT );
  180. this._levelSizes.push( new Vector2() );
  181. }
  182. /**
  183. * The material for the luminance pass.
  184. *
  185. * @private
  186. * @type {?NodeMaterial}
  187. */
  188. this._highPassFilterMaterial = null;
  189. /**
  190. * The material for the downsample pass.
  191. *
  192. * @private
  193. * @type {?NodeMaterial}
  194. */
  195. this._downsampleMaterial = null;
  196. /**
  197. * The material for the upsample / accumulation pass.
  198. *
  199. * @private
  200. * @type {?NodeMaterial}
  201. */
  202. this._upsampleMaterial = null;
  203. /**
  204. * The result of the effect is represented as a separate texture node.
  205. * The finest accumulation target holds the composited bloom.
  206. *
  207. * @private
  208. * @type {PassTextureNode}
  209. */
  210. this._textureOutput = passTexture( this, this._accumRTs[ 0 ].texture );
  211. /**
  212. * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders
  213. * its effect once per frame in `updateBefore()`.
  214. *
  215. * @type {string}
  216. * @default 'frame'
  217. */
  218. this.updateBeforeType = NodeUpdateType.FRAME;
  219. }
  220. /**
  221. * Returns the result of the effect as a texture node.
  222. *
  223. * @return {PassTextureNode} A texture node that represents the result of the effect.
  224. */
  225. getTextureNode() {
  226. return this._textureOutput;
  227. }
  228. /**
  229. * Sets the resolution scale for the pass.
  230. * The resolution scale is a factor that is multiplied with the renderer's width and height.
  231. *
  232. * @param {number} resolutionScale - The resolution scale to set. A value of `1` means full resolution.
  233. * @return {DualKawaseBloomNode} A reference to this node.
  234. */
  235. setResolutionScale( resolutionScale ) {
  236. this._resolutionScale = resolutionScale;
  237. return this;
  238. }
  239. /**
  240. * Gets the current resolution scale of the pass.
  241. *
  242. * @return {number} The current resolution scale. A value of `1` means full resolution.
  243. */
  244. getResolutionScale() {
  245. return this._resolutionScale;
  246. }
  247. /**
  248. * Sets the size of the effect.
  249. *
  250. * @param {number} width - The width of the effect.
  251. * @param {number} height - The height of the effect.
  252. */
  253. setSize( width, height ) {
  254. const resx = Math.max( 1, Math.floor( width * this._resolutionScale ) );
  255. const resy = Math.max( 1, Math.floor( height * this._resolutionScale ) );
  256. this._renderTargetBright.setSize( resx, resy );
  257. this._brightSize.set( resx, resy );
  258. let rx = resx;
  259. let ry = resy;
  260. for ( let i = 0; i < this._nMips; i ++ ) {
  261. // Level 0 blurs in place at bright resolution; the rest halve.
  262. if ( i > 0 ) {
  263. rx = Math.max( 1, Math.floor( rx / 2 ) );
  264. ry = Math.max( 1, Math.floor( ry / 2 ) );
  265. }
  266. this._downsampleRTs[ i ].setSize( rx, ry );
  267. this._accumRTs[ i ].setSize( rx, ry );
  268. this._levelSizes[ i ].set( rx, ry );
  269. }
  270. }
  271. /**
  272. * This method is used to render the effect once per frame.
  273. *
  274. * @param {NodeFrame} frame - The current node frame.
  275. */
  276. updateBefore( frame ) {
  277. const { renderer } = frame;
  278. _rendererState = RendererUtils.resetRendererState( renderer, _rendererState );
  279. //
  280. const size = renderer.getDrawingBufferSize( _size );
  281. this.setSize( size.width, size.height );
  282. const nMips = this._nMips;
  283. const downMaterial = this._downsampleMaterial;
  284. const upMaterial = this._upsampleMaterial;
  285. // 1. Extract bright areas
  286. renderer.setRenderTarget( this._renderTargetBright );
  287. _quadMesh.material = this._highPassFilterMaterial;
  288. _quadMesh.name = 'Dual Kawase Bloom [ High Pass ]';
  289. _quadMesh.render( renderer );
  290. // 2. Downsample chain ( bright -> down0 -> down1 -> ... ). The coarsest level
  291. // pre-multiplies its `radius` weight so the upsample chain can seed from it.
  292. _quadMesh.material = downMaterial;
  293. let sourceTexture = this._renderTargetBright.texture;
  294. let sourceSize = this._brightSize;
  295. for ( let i = 0; i < nMips; i ++ ) {
  296. const isCoarsest = ( i === nMips - 1 );
  297. downMaterial.colorTexture.value = sourceTexture;
  298. downMaterial.texelSize.value.set( 1 / sourceSize.x, 1 / sourceSize.y );
  299. downMaterial.applyWeight.value = isCoarsest ? 1 : 0;
  300. downMaterial.bloomFactor.value = this._bloomFactors[ i ];
  301. renderer.setRenderTarget( this._downsampleRTs[ i ] );
  302. _quadMesh.name = `Dual Kawase Bloom [ Downsample - ${ i } ]`;
  303. _quadMesh.render( renderer );
  304. sourceTexture = this._downsampleRTs[ i ].texture;
  305. sourceSize = this._levelSizes[ i ];
  306. }
  307. // 3. Upsample chain with weighted accumulation ( ... -> up1 -> up0 ).
  308. // Each level adds its matching downsample weighted by `radius`; the
  309. // coarsest downsample seeds the chain. The finest step ( i = 0 ) lands at
  310. // bright resolution and applies `strength`, so it doubles as the composite.
  311. _quadMesh.material = upMaterial;
  312. for ( let i = nMips - 2; i >= 0; i -- ) {
  313. const isSeed = ( i === nMips - 2 );
  314. upMaterial.finalFlag.value = ( i === 0 ) ? 1 : 0;
  315. upMaterial.bloomFactor.value = this._bloomFactors[ i ];
  316. upMaterial.prevTexture.value = isSeed ? this._downsampleRTs[ i + 1 ].texture : this._accumRTs[ i + 1 ].texture;
  317. upMaterial.addTexture.value = this._downsampleRTs[ i ].texture;
  318. upMaterial.texelSize.value.set( 1 / this._levelSizes[ i + 1 ].x, 1 / this._levelSizes[ i + 1 ].y );
  319. renderer.setRenderTarget( this._accumRTs[ i ] );
  320. _quadMesh.name = ( i === 0 ) ? 'Dual Kawase Bloom [ Composite ]' : `Dual Kawase Bloom [ Upsample - ${ i } ]`;
  321. _quadMesh.render( renderer );
  322. }
  323. // restore
  324. RendererUtils.restoreRendererState( renderer, _rendererState );
  325. }
  326. /**
  327. * This method is used to setup the effect's TSL code.
  328. *
  329. * @param {NodeBuilder} builder - The current node builder.
  330. * @return {PassTextureNode}
  331. */
  332. setup( builder ) {
  333. // luminosity high pass material
  334. this._highPassFilterMaterial = this._highPassFilterMaterial || new NodeMaterial();
  335. this._highPassFilterMaterial.fragmentNode = this.highPassFn( { input: this.inputNode, threshold: this.threshold, smoothWidth: this.smoothWidth } ).context( builder.getSharedContext() );
  336. this._highPassFilterMaterial.name = 'DualKawaseBloom_highPass';
  337. this._highPassFilterMaterial.needsUpdate = true;
  338. // downsample material ( Dual Kawase, 5 taps )
  339. this._downsampleMaterial = this._downsampleMaterial || this._getDownsampleMaterial( builder );
  340. // upsample material ( Dual Kawase, 8 taps, weighted accumulation )
  341. this._upsampleMaterial = this._upsampleMaterial || this._getUpsampleMaterial( builder );
  342. //
  343. return this._textureOutput;
  344. }
  345. /**
  346. * Frees internal resources. This method should be called
  347. * when the effect is no longer required.
  348. */
  349. dispose() {
  350. this._renderTargetBright.dispose();
  351. for ( let i = 0; i < this._nMips; i ++ ) {
  352. this._downsampleRTs[ i ].dispose();
  353. this._accumRTs[ i ].dispose();
  354. }
  355. if ( this._highPassFilterMaterial !== null ) this._highPassFilterMaterial.dispose();
  356. if ( this._downsampleMaterial !== null ) this._downsampleMaterial.dispose();
  357. if ( this._upsampleMaterial !== null ) this._upsampleMaterial.dispose();
  358. }
  359. /**
  360. * Creates the Dual Kawase downsample material. Each output texel reads the
  361. * center plus four diagonal corners of the source.
  362. *
  363. * @private
  364. * @param {NodeBuilder} builder - The current node builder.
  365. * @return {NodeMaterial}
  366. */
  367. _getDownsampleMaterial( builder ) {
  368. const colorTexture = texture( null );
  369. const texelSize = uniform( new Vector2() );
  370. const offset = this._offset;
  371. const applyWeight = uniform( 0 );
  372. const bloomFactor = uniform( 1 );
  373. const uvNode = uv();
  374. const downsamplePass = Fn( () => {
  375. const o = texelSize.mul( 0.5 ).mul( offset );
  376. const color = colorTexture.sample( uvNode ).mul( 4.0 ).toVar();
  377. color.addAssign( colorTexture.sample( uvNode.add( vec2( o.x.negate(), o.y.negate() ) ) ) );
  378. color.addAssign( colorTexture.sample( uvNode.add( vec2( o.x, o.y.negate() ) ) ) );
  379. color.addAssign( colorTexture.sample( uvNode.add( vec2( o.x.negate(), o.y ) ) ) );
  380. color.addAssign( colorTexture.sample( uvNode.add( vec2( o.x, o.y ) ) ) );
  381. // The coarsest level pre-applies its `radius` weight so it can seed the upsample chain.
  382. const weight = mix( bloomFactor, float( 1.2 ).sub( bloomFactor ), this.radius );
  383. return vec4( color.rgb.div( 8.0 ).mul( mix( float( 1.0 ), weight, applyWeight ) ), 1.0 );
  384. } );
  385. const material = new NodeMaterial();
  386. material.fragmentNode = downsamplePass().context( builder.getSharedContext() );
  387. material.name = 'DualKawaseBloom_down';
  388. material.needsUpdate = true;
  389. material.colorTexture = colorTexture;
  390. material.texelSize = texelSize;
  391. material.applyWeight = applyWeight;
  392. material.bloomFactor = bloomFactor;
  393. return material;
  394. }
  395. /**
  396. * Creates the Dual Kawase upsample material. Each output texel reads four
  397. * edge centers and four diagonal corners of the source, then accumulates the
  398. * matching downsample level weighted by `radius`.
  399. *
  400. * @private
  401. * @param {NodeBuilder} builder - The current node builder.
  402. * @return {NodeMaterial}
  403. */
  404. _getUpsampleMaterial( builder ) {
  405. const prevTexture = texture( null );
  406. const addTexture = texture( null );
  407. const texelSize = uniform( new Vector2() );
  408. const offset = this._offset;
  409. const finalFlag = uniform( 0 );
  410. const bloomFactor = uniform( 1 );
  411. const uvNode = uv();
  412. const upsamplePass = Fn( () => {
  413. const o = texelSize.mul( 0.5 ).mul( offset );
  414. const sum = prevTexture.sample( uvNode.add( vec2( o.x.mul( - 2.0 ), 0.0 ) ) ).rgb.toVar();
  415. sum.addAssign( prevTexture.sample( uvNode.add( vec2( o.x.mul( 2.0 ), 0.0 ) ) ).rgb );
  416. sum.addAssign( prevTexture.sample( uvNode.add( vec2( 0.0, o.y.mul( - 2.0 ) ) ) ).rgb );
  417. sum.addAssign( prevTexture.sample( uvNode.add( vec2( 0.0, o.y.mul( 2.0 ) ) ) ).rgb );
  418. sum.addAssign( prevTexture.sample( uvNode.add( vec2( o.x.negate(), o.y ) ) ).rgb.mul( 2.0 ) );
  419. sum.addAssign( prevTexture.sample( uvNode.add( vec2( o.x, o.y ) ) ).rgb.mul( 2.0 ) );
  420. sum.addAssign( prevTexture.sample( uvNode.add( vec2( o.x.negate(), o.y.negate() ) ) ).rgb.mul( 2.0 ) );
  421. sum.addAssign( prevTexture.sample( uvNode.add( vec2( o.x, o.y.negate() ) ) ).rgb.mul( 2.0 ) );
  422. const blurred = sum.div( 12.0 );
  423. // redistribute the level's contribution between a tight and a wide bloom
  424. const weight = mix( bloomFactor, float( 1.2 ).sub( bloomFactor ), this.radius );
  425. const added = addTexture.sample( uvNode ).rgb.mul( weight );
  426. // keep the total intensity independent of the level count ( the factors sum to `_nMips * 0.6` )
  427. const norm = 3 / ( this._nMips * 0.6 );
  428. const result = blurred.add( added ).mul( mix( float( 1.0 ), this.strength.mul( norm ), finalFlag ) );
  429. return vec4( result, 1.0 );
  430. } );
  431. const material = new NodeMaterial();
  432. material.fragmentNode = upsamplePass().context( builder.getSharedContext() );
  433. material.name = 'DualKawaseBloom_up';
  434. material.needsUpdate = true;
  435. material.prevTexture = prevTexture;
  436. material.addTexture = addTexture;
  437. material.texelSize = texelSize;
  438. material.finalFlag = finalFlag;
  439. material.bloomFactor = bloomFactor;
  440. return material;
  441. }
  442. }
  443. /**
  444. * TSL function for creating a bloom effect.
  445. *
  446. * @tsl
  447. * @function
  448. * @param {Node<vec4>} node - The node that represents the input of the effect.
  449. * @param {number} [strength=1] - The strength of the bloom.
  450. * @param {number} [radius=0] - The radius of the bloom.
  451. * @param {number} [threshold=0] - The luminance threshold limits which bright areas contribute to the bloom effect.
  452. * @returns {DualKawaseBloomNode}
  453. */
  454. export const dualKawaseBloom = ( node, strength, radius, threshold ) => new DualKawaseBloomNode( nodeObject( node ), strength, radius, threshold );
  455. export default DualKawaseBloomNode;
粤ICP备19079148号