BloomNode.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  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. 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. * Scale factor for the internal render targets.
  92. *
  93. * @private
  94. * @type {number}
  95. * @default 0.5
  96. */
  97. this._resolutionScale = 0.5;
  98. /**
  99. * Can be used to inject a custom high pass filter (e.g., for anamorphic effects).
  100. *
  101. * @type {Function}
  102. */
  103. this.highPassFn = luminosityHighPass;
  104. /**
  105. * An array that holds the render targets for the horizontal blur passes.
  106. *
  107. * @private
  108. * @type {Array<RenderTarget>}
  109. */
  110. this._renderTargetsHorizontal = [];
  111. /**
  112. * An array that holds the render targets for the vertical blur passes.
  113. *
  114. * @private
  115. * @type {Array<RenderTarget>}
  116. */
  117. this._renderTargetsVertical = [];
  118. /**
  119. * The number if blur mips.
  120. *
  121. * @private
  122. * @type {number}
  123. */
  124. this._nMips = 5;
  125. /**
  126. * The render target for the luminance pass.
  127. *
  128. * @private
  129. * @type {RenderTarget}
  130. */
  131. this._renderTargetBright = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
  132. this._renderTargetBright.texture.name = 'UnrealBloomPass.bright';
  133. this._renderTargetBright.texture.generateMipmaps = false;
  134. //
  135. for ( let i = 0; i < this._nMips; i ++ ) {
  136. const renderTargetHorizontal = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
  137. renderTargetHorizontal.texture.name = 'UnrealBloomPass.h' + i;
  138. renderTargetHorizontal.texture.generateMipmaps = false;
  139. this._renderTargetsHorizontal.push( renderTargetHorizontal );
  140. const renderTargetVertical = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
  141. renderTargetVertical.texture.name = 'UnrealBloomPass.v' + i;
  142. renderTargetVertical.texture.generateMipmaps = false;
  143. this._renderTargetsVertical.push( renderTargetVertical );
  144. }
  145. /**
  146. * The material for the composite pass.
  147. *
  148. * @private
  149. * @type {?NodeMaterial}
  150. */
  151. this._compositeMaterial = null;
  152. /**
  153. * The material for the luminance pass.
  154. *
  155. * @private
  156. * @type {?NodeMaterial}
  157. */
  158. this._highPassFilterMaterial = null;
  159. /**
  160. * The materials for the blur pass.
  161. *
  162. * @private
  163. * @type {Array<NodeMaterial>}
  164. */
  165. this._separableBlurMaterials = [];
  166. /**
  167. * The result of the luminance pass as a texture node for further processing.
  168. *
  169. * @private
  170. * @type {TextureNode}
  171. */
  172. this._textureNodeBright = texture( this._renderTargetBright.texture );
  173. /**
  174. * The result of the first blur pass as a texture node for further processing.
  175. *
  176. * @private
  177. * @type {TextureNode}
  178. */
  179. this._textureNodeBlur0 = texture( this._renderTargetsVertical[ 0 ].texture );
  180. /**
  181. * The result of the second blur pass as a texture node for further processing.
  182. *
  183. * @private
  184. * @type {TextureNode}
  185. */
  186. this._textureNodeBlur1 = texture( this._renderTargetsVertical[ 1 ].texture );
  187. /**
  188. * The result of the third blur pass as a texture node for further processing.
  189. *
  190. * @private
  191. * @type {TextureNode}
  192. */
  193. this._textureNodeBlur2 = texture( this._renderTargetsVertical[ 2 ].texture );
  194. /**
  195. * The result of the fourth blur pass as a texture node for further processing.
  196. *
  197. * @private
  198. * @type {TextureNode}
  199. */
  200. this._textureNodeBlur3 = texture( this._renderTargetsVertical[ 3 ].texture );
  201. /**
  202. * The result of the fifth blur pass as a texture node for further processing.
  203. *
  204. * @private
  205. * @type {TextureNode}
  206. */
  207. this._textureNodeBlur4 = texture( this._renderTargetsVertical[ 4 ].texture );
  208. /**
  209. * The result of the effect is represented as a separate texture node.
  210. *
  211. * @private
  212. * @type {PassTextureNode}
  213. */
  214. this._textureOutput = passTexture( this, this._renderTargetsHorizontal[ 0 ].texture );
  215. /**
  216. * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders
  217. * its effect once per frame in `updateBefore()`.
  218. *
  219. * @type {string}
  220. * @default 'frame'
  221. */
  222. this.updateBeforeType = NodeUpdateType.FRAME;
  223. }
  224. /**
  225. * Returns the result of the effect as a texture node.
  226. *
  227. * @return {PassTextureNode} A texture node that represents the result of the effect.
  228. */
  229. getTextureNode() {
  230. return this._textureOutput;
  231. }
  232. /**
  233. * Sets the resolution scale for the pass.
  234. * The resolution scale is a factor that is multiplied with the renderer's width and height.
  235. *
  236. * @param {number} resolutionScale - The resolution scale to set. A value of `1` means full resolution.
  237. * @return {BloomNode} A reference to this node.
  238. */
  239. setResolutionScale( resolutionScale ) {
  240. this._resolutionScale = resolutionScale;
  241. return this;
  242. }
  243. /**
  244. * Gets the current resolution scale of the pass.
  245. *
  246. * @return {number} The current resolution scale. A value of `1` means full resolution.
  247. */
  248. getResolutionScale() {
  249. return this._resolutionScale;
  250. }
  251. /**
  252. * Sets the size of the effect.
  253. *
  254. * @param {number} width - The width of the effect.
  255. * @param {number} height - The height of the effect.
  256. */
  257. setSize( width, height ) {
  258. let resx = Math.floor( width * this._resolutionScale );
  259. let resy = Math.floor( height * this._resolutionScale );
  260. this._renderTargetBright.setSize( resx, resy );
  261. for ( let i = 0; i < this._nMips; i ++ ) {
  262. this._renderTargetsHorizontal[ i ].setSize( resx, resy );
  263. this._renderTargetsVertical[ i ].setSize( resx, resy );
  264. this._separableBlurMaterials[ i ].invSize.value.set( 1 / resx, 1 / resy );
  265. resx = Math.floor( resx / 2 );
  266. resy = Math.floor( resy / 2 );
  267. }
  268. }
  269. /**
  270. * This method is used to render the effect once per frame.
  271. *
  272. * @param {NodeFrame} frame - The current node frame.
  273. */
  274. updateBefore( frame ) {
  275. const { renderer } = frame;
  276. _rendererState = RendererUtils.resetRendererState( renderer, _rendererState );
  277. //
  278. const size = renderer.getDrawingBufferSize( _size );
  279. this.setSize( size.width, size.height );
  280. // 1. Extract bright areas
  281. renderer.setRenderTarget( this._renderTargetBright );
  282. _quadMesh.material = this._highPassFilterMaterial;
  283. _quadMesh.name = 'Bloom [ High Pass ]';
  284. _quadMesh.render( renderer );
  285. // 2. Blur all the mips progressively
  286. let inputRenderTarget = this._renderTargetBright;
  287. for ( let i = 0; i < this._nMips; i ++ ) {
  288. _quadMesh.material = this._separableBlurMaterials[ i ];
  289. this._separableBlurMaterials[ i ].colorTexture.value = inputRenderTarget.texture;
  290. this._separableBlurMaterials[ i ].direction.value = _BlurDirectionX;
  291. renderer.setRenderTarget( this._renderTargetsHorizontal[ i ] );
  292. _quadMesh.name = `Bloom [ Blur Horizontal - ${ i } ]`;
  293. _quadMesh.render( renderer );
  294. this._separableBlurMaterials[ i ].colorTexture.value = this._renderTargetsHorizontal[ i ].texture;
  295. this._separableBlurMaterials[ i ].direction.value = _BlurDirectionY;
  296. renderer.setRenderTarget( this._renderTargetsVertical[ i ] );
  297. _quadMesh.name = `Bloom [ Blur Vertical - ${ i } ]`;
  298. _quadMesh.render( renderer );
  299. inputRenderTarget = this._renderTargetsVertical[ i ];
  300. }
  301. // 3. Composite all the mips
  302. renderer.setRenderTarget( this._renderTargetsHorizontal[ 0 ] );
  303. _quadMesh.material = this._compositeMaterial;
  304. _quadMesh.name = 'Bloom [ Composite ]';
  305. _quadMesh.render( renderer );
  306. // restore
  307. RendererUtils.restoreRendererState( renderer, _rendererState );
  308. }
  309. /**
  310. * This method is used to setup the effect's TSL code.
  311. *
  312. * @param {NodeBuilder} builder - The current node builder.
  313. * @return {PassTextureNode}
  314. */
  315. setup( builder ) {
  316. // luminosity high pass material
  317. this._highPassFilterMaterial = this._highPassFilterMaterial || new NodeMaterial();
  318. this._highPassFilterMaterial.fragmentNode = this.highPassFn( { input: this.inputNode, threshold: this.threshold, smoothWidth: this.smoothWidth } ).context( builder.getSharedContext() );
  319. this._highPassFilterMaterial.name = 'Bloom_highPass';
  320. this._highPassFilterMaterial.needsUpdate = true;
  321. // gaussian blur materials
  322. // These sizes have been changed to account for the altered coefficients-calculation to avoid blockiness,
  323. // while retaining the same blur-strength. For details see https://github.com/mrdoob/three.js/pull/31528
  324. const kernelSizeArray = [ 6, 10, 14, 18, 22 ];
  325. for ( let i = 0; i < this._nMips; i ++ ) {
  326. this._separableBlurMaterials.push( this._getSeparableBlurMaterial( builder, kernelSizeArray[ i ] ) );
  327. }
  328. // composite material
  329. const bloomFactors = uniformArray( [ 1.0, 0.8, 0.6, 0.4, 0.2 ] );
  330. 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 ) ] );
  331. const lerpBloomFactor = Fn( ( [ factor, radius ] ) => {
  332. const mirrorFactor = float( 1.2 ).sub( factor );
  333. return mix( factor, mirrorFactor, radius );
  334. } ).setLayout( {
  335. name: 'lerpBloomFactor',
  336. type: 'float',
  337. inputs: [
  338. { name: 'factor', type: 'float' },
  339. { name: 'radius', type: 'float' },
  340. ]
  341. } );
  342. const compositePass = Fn( () => {
  343. const color0 = lerpBloomFactor( bloomFactors.element( 0 ), this.radius ).mul( vec4( bloomTintColors.element( 0 ), 1.0 ) ).mul( this._textureNodeBlur0 );
  344. const color1 = lerpBloomFactor( bloomFactors.element( 1 ), this.radius ).mul( vec4( bloomTintColors.element( 1 ), 1.0 ) ).mul( this._textureNodeBlur1 );
  345. const color2 = lerpBloomFactor( bloomFactors.element( 2 ), this.radius ).mul( vec4( bloomTintColors.element( 2 ), 1.0 ) ).mul( this._textureNodeBlur2 );
  346. const color3 = lerpBloomFactor( bloomFactors.element( 3 ), this.radius ).mul( vec4( bloomTintColors.element( 3 ), 1.0 ) ).mul( this._textureNodeBlur3 );
  347. const color4 = lerpBloomFactor( bloomFactors.element( 4 ), this.radius ).mul( vec4( bloomTintColors.element( 4 ), 1.0 ) ).mul( this._textureNodeBlur4 );
  348. const sum = color0.add( color1 ).add( color2 ).add( color3 ).add( color4 );
  349. return sum.mul( this.strength );
  350. } );
  351. this._compositeMaterial = this._compositeMaterial || new NodeMaterial();
  352. this._compositeMaterial.fragmentNode = compositePass().context( builder.getSharedContext() );
  353. this._compositeMaterial.name = 'Bloom_comp';
  354. this._compositeMaterial.needsUpdate = true;
  355. //
  356. return this._textureOutput;
  357. }
  358. /**
  359. * Frees internal resources. This method should be called
  360. * when the effect is no longer required.
  361. */
  362. dispose() {
  363. for ( let i = 0; i < this._renderTargetsHorizontal.length; i ++ ) {
  364. this._renderTargetsHorizontal[ i ].dispose();
  365. }
  366. for ( let i = 0; i < this._renderTargetsVertical.length; i ++ ) {
  367. this._renderTargetsVertical[ i ].dispose();
  368. }
  369. this._renderTargetBright.dispose();
  370. if ( this._highPassFilterMaterial !== null ) this._highPassFilterMaterial.dispose();
  371. if ( this._compositeMaterial !== null ) this._compositeMaterial.dispose();
  372. for ( let i = 0; i < this._separableBlurMaterials.length; i ++ ) {
  373. this._separableBlurMaterials[ i ].dispose();
  374. }
  375. }
  376. /**
  377. * Create a separable blur material for the given kernel radius.
  378. *
  379. * @private
  380. * @param {NodeBuilder} builder - The current node builder.
  381. * @param {number} kernelRadius - The kernel radius.
  382. * @return {NodeMaterial}
  383. */
  384. _getSeparableBlurMaterial( builder, kernelRadius ) {
  385. const coefficients = [];
  386. const sigma = kernelRadius / 3;
  387. for ( let i = 0; i < kernelRadius; i ++ ) {
  388. coefficients.push( 0.39894 * Math.exp( - 0.5 * i * i / ( sigma * sigma ) ) / sigma );
  389. }
  390. //
  391. const colorTexture = texture( null );
  392. const gaussianCoefficients = uniformArray( coefficients );
  393. const invSize = uniform( new Vector2() );
  394. const direction = uniform( new Vector2( 0.5, 0.5 ) );
  395. const uvNode = uv();
  396. const sampleTexel = ( uv ) => colorTexture.sample( uv );
  397. const separableBlurPass = Fn( () => {
  398. const diffuseSum = sampleTexel( uvNode ).rgb.mul( gaussianCoefficients.element( 0 ) ).toVar();
  399. Loop( { start: int( 1 ), end: int( kernelRadius ), type: 'int', condition: '<' }, ( { i } ) => {
  400. const x = float( i );
  401. const w = gaussianCoefficients.element( i );
  402. const uvOffset = direction.mul( invSize ).mul( x );
  403. const sample1 = sampleTexel( uvNode.add( uvOffset ) ).rgb;
  404. const sample2 = sampleTexel( uvNode.sub( uvOffset ) ).rgb;
  405. diffuseSum.addAssign( add( sample1, sample2 ).mul( w ) );
  406. } );
  407. return vec4( diffuseSum, 1.0 );
  408. } );
  409. const separableBlurMaterial = new NodeMaterial();
  410. separableBlurMaterial.fragmentNode = separableBlurPass().context( builder.getSharedContext() );
  411. separableBlurMaterial.name = 'Bloom_separable';
  412. separableBlurMaterial.needsUpdate = true;
  413. // uniforms
  414. separableBlurMaterial.colorTexture = colorTexture;
  415. separableBlurMaterial.direction = direction;
  416. separableBlurMaterial.invSize = invSize;
  417. return separableBlurMaterial;
  418. }
  419. }
  420. /**
  421. * TSL function for creating a bloom effect.
  422. *
  423. * @tsl
  424. * @function
  425. * @param {Node<vec4>} node - The node that represents the input of the effect.
  426. * @param {number} [strength=1] - The strength of the bloom.
  427. * @param {number} [radius=0] - The radius of the bloom.
  428. * @param {number} [threshold=0] - The luminance threshold limits which bright areas contribute to the bloom effect.
  429. * @returns {BloomNode}
  430. */
  431. export const bloom = ( node, strength, radius, threshold ) => new BloomNode( nodeObject( node ), strength, radius, threshold );
  432. export default BloomNode;
粤ICP备19079148号