| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596 |
- import { HalfFloatType, RenderTarget, Vector2, TempNode, QuadMesh, NodeMaterial, RendererUtils, NodeUpdateType } from 'three/webgpu';
- import { nodeObject, Fn, float, vec2, uv, passTexture, uniform, texture, luminance, smoothstep, mix, vec4 } from 'three/tsl';
- const _quadMesh = /*@__PURE__*/ new QuadMesh();
- const _size = /*@__PURE__*/ new Vector2();
- let _rendererState;
- const luminosityHighPass = Fn( ( { input, threshold, smoothWidth } ) => {
- const v = luminance( input.rgb );
- const alpha = smoothstep( threshold, threshold.add( smoothWidth ), v );
- return mix( vec4( 0 ), input, alpha );
- } );
- /**
- * Post processing node for creating a bloom effect.
- *
- * The bloom is produced with a Dual Kawase blur: the bright areas are
- * progressively downsampled with a 5-tap filter and then upsampled with an
- * 8-tap filter, accumulating the levels back together.
- * ```js
- * const renderPipeline = new THREE.RenderPipeline( renderer );
- *
- * const scenePass = pass( scene, camera );
- * const scenePassColor = scenePass.getTextureNode( 'output' );
- *
- * const bloomPass = dualKawaseBloom( scenePassColor );
- *
- * renderPipeline.outputNode = scenePassColor.add( bloomPass );
- * ```
- * By default, the node affects the entire image. For a selective bloom,
- * use the `emissive` material property to control which objects should
- * contribute to bloom or not. This can be achieved via MRT.
- * ```js
- * const renderPipeline = new THREE.RenderPipeline( renderer );
- *
- * const scenePass = pass( scene, camera );
- * scenePass.setMRT( mrt( {
- * output,
- * emissive
- * } ) );
- *
- * const scenePassColor = scenePass.getTextureNode( 'output' );
- * const emissivePass = scenePass.getTextureNode( 'emissive' );
- *
- * const bloomPass = dualKawaseBloom( emissivePass );
- * renderPipeline.outputNode = scenePassColor.add( bloomPass );
- * ```
- * @augments TempNode
- * @three_import import { dualKawaseBloom } from 'three/addons/tsl/display/DualKawaseBloomNode.js';
- */
- class DualKawaseBloomNode extends TempNode {
- static get type() {
- return 'DualKawaseBloomNode';
- }
- /**
- * Constructs a new bloom node.
- *
- * @param {Node<vec4>} inputNode - The node that represents the input of the effect.
- * @param {number} [strength=1] - The strength of the bloom.
- * @param {number} [radius=0] - The radius of the bloom.
- * @param {number} [threshold=0] - The luminance threshold limits which bright areas contribute to the bloom effect.
- */
- constructor( inputNode, strength = 1, radius = 0, threshold = 0 ) {
- super( 'vec4' );
- /**
- * The node that represents the input of the effect.
- *
- * @type {Node<vec4>}
- */
- this.inputNode = inputNode;
- /**
- * The strength of the bloom.
- *
- * @type {UniformNode<float>}
- */
- this.strength = strength.isNode ? strength : uniform( strength );
- /**
- * The radius of the bloom. Must be in the range `[0,1]`.
- *
- * @type {UniformNode<float>}
- */
- this.radius = radius.isNode ? radius : uniform( radius );
- /**
- * The luminance threshold limits which bright areas contribute to the bloom effect.
- *
- * @type {UniformNode<float>}
- */
- this.threshold = threshold.isNode ? threshold : uniform( threshold );
- /**
- * Can be used to tweak the extracted luminance from the scene.
- *
- * @type {UniformNode<float>}
- */
- this.smoothWidth = uniform( 0.01 );
- /**
- * Scale factor for the internal render targets.
- *
- * @private
- * @type {number}
- * @default 0.5
- */
- this._resolutionScale = 0.5;
- /**
- * Can be used to inject a custom high pass filter (e.g., for anamorphic effects).
- *
- * @type {Function}
- */
- this.highPassFn = luminosityHighPass;
- /**
- * The number of downsample / upsample levels in the Dual Kawase pyramid.
- *
- * @private
- * @type {number}
- */
- this._nMips = 6;
- /**
- * Sample spread of the Dual Kawase filters. Kept small so each level's kernel
- * stays round (a wide offset makes the diamond sampling pattern visible as a
- * square halo); the bloom width comes from the depth of the pyramid instead.
- *
- * @private
- * @type {UniformNode<float>}
- */
- this._offset = uniform( 3 );
- /**
- * Per-level mixing factors, redistributed by `radius` between a tight and a wide bloom.
- * A linear ramp from `1.0` to `0.2` (mean `0.6`) keeps the total bloom energy constant
- * as `radius` shifts weight between the fine and coarse levels.
- *
- * @private
- * @type {Array<number>}
- */
- this._bloomFactors = [];
- for ( let i = 0; i < this._nMips; i ++ ) {
- this._bloomFactors.push( this._nMips === 1 ? 0.6 : 1.0 - 0.8 * i / ( this._nMips - 1 ) );
- }
- /**
- * The render target for the luminance pass.
- *
- * @private
- * @type {RenderTarget}
- */
- this._renderTargetBright = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
- this._renderTargetBright.texture.name = 'DualKawaseBloom.bright';
- this._renderTargetBright.texture.generateMipmaps = false;
- /**
- * The render targets for the downsample chain.
- *
- * @private
- * @type {Array<RenderTarget>}
- */
- this._downsampleRTs = [];
- /**
- * The render targets for the upsample / accumulation chain.
- *
- * @private
- * @type {Array<RenderTarget>}
- */
- this._accumRTs = [];
- /**
- * The resolution of each pyramid level.
- *
- * @private
- * @type {Array<Vector2>}
- */
- this._levelSizes = [];
- /**
- * The resolution of the bright pass.
- *
- * @private
- * @type {Vector2}
- */
- this._brightSize = new Vector2();
- for ( let i = 0; i < this._nMips; i ++ ) {
- const downsampleRT = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
- downsampleRT.texture.name = 'DualKawaseBloom.down' + i;
- downsampleRT.texture.generateMipmaps = false;
- this._downsampleRTs.push( downsampleRT );
- const accumRT = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
- accumRT.texture.name = 'DualKawaseBloom.up' + i;
- accumRT.texture.generateMipmaps = false;
- this._accumRTs.push( accumRT );
- this._levelSizes.push( new Vector2() );
- }
- /**
- * The material for the luminance pass.
- *
- * @private
- * @type {?NodeMaterial}
- */
- this._highPassFilterMaterial = null;
- /**
- * The material for the downsample pass.
- *
- * @private
- * @type {?NodeMaterial}
- */
- this._downsampleMaterial = null;
- /**
- * The material for the upsample / accumulation pass.
- *
- * @private
- * @type {?NodeMaterial}
- */
- this._upsampleMaterial = null;
- /**
- * The result of the effect is represented as a separate texture node.
- * The finest accumulation target holds the composited bloom.
- *
- * @private
- * @type {PassTextureNode}
- */
- this._textureOutput = passTexture( this, this._accumRTs[ 0 ].texture );
- /**
- * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders
- * its effect once per frame in `updateBefore()`.
- *
- * @type {string}
- * @default 'frame'
- */
- this.updateBeforeType = NodeUpdateType.FRAME;
- }
- /**
- * Returns the result of the effect as a texture node.
- *
- * @return {PassTextureNode} A texture node that represents the result of the effect.
- */
- getTextureNode() {
- return this._textureOutput;
- }
- /**
- * Sets the resolution scale for the pass.
- * The resolution scale is a factor that is multiplied with the renderer's width and height.
- *
- * @param {number} resolutionScale - The resolution scale to set. A value of `1` means full resolution.
- * @return {DualKawaseBloomNode} A reference to this node.
- */
- setResolutionScale( resolutionScale ) {
- this._resolutionScale = resolutionScale;
- return this;
- }
- /**
- * Gets the current resolution scale of the pass.
- *
- * @return {number} The current resolution scale. A value of `1` means full resolution.
- */
- getResolutionScale() {
- return this._resolutionScale;
- }
- /**
- * Sets the size of the effect.
- *
- * @param {number} width - The width of the effect.
- * @param {number} height - The height of the effect.
- */
- setSize( width, height ) {
- const resx = Math.max( 1, Math.floor( width * this._resolutionScale ) );
- const resy = Math.max( 1, Math.floor( height * this._resolutionScale ) );
- this._renderTargetBright.setSize( resx, resy );
- this._brightSize.set( resx, resy );
- let rx = resx;
- let ry = resy;
- for ( let i = 0; i < this._nMips; i ++ ) {
- // Level 0 blurs in place at bright resolution; the rest halve.
- if ( i > 0 ) {
- rx = Math.max( 1, Math.floor( rx / 2 ) );
- ry = Math.max( 1, Math.floor( ry / 2 ) );
- }
- this._downsampleRTs[ i ].setSize( rx, ry );
- this._accumRTs[ i ].setSize( rx, ry );
- this._levelSizes[ i ].set( rx, ry );
- }
- }
- /**
- * This method is used to render the effect once per frame.
- *
- * @param {NodeFrame} frame - The current node frame.
- */
- updateBefore( frame ) {
- const { renderer } = frame;
- _rendererState = RendererUtils.resetRendererState( renderer, _rendererState );
- //
- const size = renderer.getDrawingBufferSize( _size );
- this.setSize( size.width, size.height );
- const nMips = this._nMips;
- const downMaterial = this._downsampleMaterial;
- const upMaterial = this._upsampleMaterial;
- // 1. Extract bright areas
- renderer.setRenderTarget( this._renderTargetBright );
- _quadMesh.material = this._highPassFilterMaterial;
- _quadMesh.name = 'Dual Kawase Bloom [ High Pass ]';
- _quadMesh.render( renderer );
- // 2. Downsample chain ( bright -> down0 -> down1 -> ... ). The coarsest level
- // pre-multiplies its `radius` weight so the upsample chain can seed from it.
- _quadMesh.material = downMaterial;
- let sourceTexture = this._renderTargetBright.texture;
- let sourceSize = this._brightSize;
- for ( let i = 0; i < nMips; i ++ ) {
- const isCoarsest = ( i === nMips - 1 );
- downMaterial.colorTexture.value = sourceTexture;
- downMaterial.texelSize.value.set( 1 / sourceSize.x, 1 / sourceSize.y );
- downMaterial.applyWeight.value = isCoarsest ? 1 : 0;
- downMaterial.bloomFactor.value = this._bloomFactors[ i ];
- renderer.setRenderTarget( this._downsampleRTs[ i ] );
- _quadMesh.name = `Dual Kawase Bloom [ Downsample - ${ i } ]`;
- _quadMesh.render( renderer );
- sourceTexture = this._downsampleRTs[ i ].texture;
- sourceSize = this._levelSizes[ i ];
- }
- // 3. Upsample chain with weighted accumulation ( ... -> up1 -> up0 ).
- // Each level adds its matching downsample weighted by `radius`; the
- // coarsest downsample seeds the chain. The finest step ( i = 0 ) lands at
- // bright resolution and applies `strength`, so it doubles as the composite.
- _quadMesh.material = upMaterial;
- for ( let i = nMips - 2; i >= 0; i -- ) {
- const isSeed = ( i === nMips - 2 );
- upMaterial.finalFlag.value = ( i === 0 ) ? 1 : 0;
- upMaterial.bloomFactor.value = this._bloomFactors[ i ];
- upMaterial.prevTexture.value = isSeed ? this._downsampleRTs[ i + 1 ].texture : this._accumRTs[ i + 1 ].texture;
- upMaterial.addTexture.value = this._downsampleRTs[ i ].texture;
- upMaterial.texelSize.value.set( 1 / this._levelSizes[ i + 1 ].x, 1 / this._levelSizes[ i + 1 ].y );
- renderer.setRenderTarget( this._accumRTs[ i ] );
- _quadMesh.name = ( i === 0 ) ? 'Dual Kawase Bloom [ Composite ]' : `Dual Kawase Bloom [ Upsample - ${ i } ]`;
- _quadMesh.render( renderer );
- }
- // restore
- RendererUtils.restoreRendererState( renderer, _rendererState );
- }
- /**
- * This method is used to setup the effect's TSL code.
- *
- * @param {NodeBuilder} builder - The current node builder.
- * @return {PassTextureNode}
- */
- setup( builder ) {
- // luminosity high pass material
- this._highPassFilterMaterial = this._highPassFilterMaterial || new NodeMaterial();
- this._highPassFilterMaterial.fragmentNode = this.highPassFn( { input: this.inputNode, threshold: this.threshold, smoothWidth: this.smoothWidth } ).context( builder.getSharedContext() );
- this._highPassFilterMaterial.name = 'DualKawaseBloom_highPass';
- this._highPassFilterMaterial.needsUpdate = true;
- // downsample material ( Dual Kawase, 5 taps )
- this._downsampleMaterial = this._downsampleMaterial || this._getDownsampleMaterial( builder );
- // upsample material ( Dual Kawase, 8 taps, weighted accumulation )
- this._upsampleMaterial = this._upsampleMaterial || this._getUpsampleMaterial( builder );
- //
- return this._textureOutput;
- }
- /**
- * Frees internal resources. This method should be called
- * when the effect is no longer required.
- */
- dispose() {
- this._renderTargetBright.dispose();
- for ( let i = 0; i < this._nMips; i ++ ) {
- this._downsampleRTs[ i ].dispose();
- this._accumRTs[ i ].dispose();
- }
- if ( this._highPassFilterMaterial !== null ) this._highPassFilterMaterial.dispose();
- if ( this._downsampleMaterial !== null ) this._downsampleMaterial.dispose();
- if ( this._upsampleMaterial !== null ) this._upsampleMaterial.dispose();
- }
- /**
- * Creates the Dual Kawase downsample material. Each output texel reads the
- * center plus four diagonal corners of the source.
- *
- * @private
- * @param {NodeBuilder} builder - The current node builder.
- * @return {NodeMaterial}
- */
- _getDownsampleMaterial( builder ) {
- const colorTexture = texture( null );
- const texelSize = uniform( new Vector2() );
- const offset = this._offset;
- const applyWeight = uniform( 0 );
- const bloomFactor = uniform( 1 );
- const uvNode = uv();
- const downsamplePass = Fn( () => {
- const o = texelSize.mul( 0.5 ).mul( offset );
- const color = colorTexture.sample( uvNode ).mul( 4.0 ).toVar();
- color.addAssign( colorTexture.sample( uvNode.add( vec2( o.x.negate(), o.y.negate() ) ) ) );
- color.addAssign( colorTexture.sample( uvNode.add( vec2( o.x, o.y.negate() ) ) ) );
- color.addAssign( colorTexture.sample( uvNode.add( vec2( o.x.negate(), o.y ) ) ) );
- color.addAssign( colorTexture.sample( uvNode.add( vec2( o.x, o.y ) ) ) );
- // The coarsest level pre-applies its `radius` weight so it can seed the upsample chain.
- const weight = mix( bloomFactor, float( 1.2 ).sub( bloomFactor ), this.radius );
- return vec4( color.rgb.div( 8.0 ).mul( mix( float( 1.0 ), weight, applyWeight ) ), 1.0 );
- } );
- const material = new NodeMaterial();
- material.fragmentNode = downsamplePass().context( builder.getSharedContext() );
- material.name = 'DualKawaseBloom_down';
- material.needsUpdate = true;
- material.colorTexture = colorTexture;
- material.texelSize = texelSize;
- material.applyWeight = applyWeight;
- material.bloomFactor = bloomFactor;
- return material;
- }
- /**
- * Creates the Dual Kawase upsample material. Each output texel reads four
- * edge centers and four diagonal corners of the source, then accumulates the
- * matching downsample level weighted by `radius`.
- *
- * @private
- * @param {NodeBuilder} builder - The current node builder.
- * @return {NodeMaterial}
- */
- _getUpsampleMaterial( builder ) {
- const prevTexture = texture( null );
- const addTexture = texture( null );
- const texelSize = uniform( new Vector2() );
- const offset = this._offset;
- const finalFlag = uniform( 0 );
- const bloomFactor = uniform( 1 );
- const uvNode = uv();
- const upsamplePass = Fn( () => {
- const o = texelSize.mul( 0.5 ).mul( offset );
- const sum = prevTexture.sample( uvNode.add( vec2( o.x.mul( - 2.0 ), 0.0 ) ) ).rgb.toVar();
- sum.addAssign( prevTexture.sample( uvNode.add( vec2( o.x.mul( 2.0 ), 0.0 ) ) ).rgb );
- sum.addAssign( prevTexture.sample( uvNode.add( vec2( 0.0, o.y.mul( - 2.0 ) ) ) ).rgb );
- sum.addAssign( prevTexture.sample( uvNode.add( vec2( 0.0, o.y.mul( 2.0 ) ) ) ).rgb );
- sum.addAssign( prevTexture.sample( uvNode.add( vec2( o.x.negate(), o.y ) ) ).rgb.mul( 2.0 ) );
- sum.addAssign( prevTexture.sample( uvNode.add( vec2( o.x, o.y ) ) ).rgb.mul( 2.0 ) );
- sum.addAssign( prevTexture.sample( uvNode.add( vec2( o.x.negate(), o.y.negate() ) ) ).rgb.mul( 2.0 ) );
- sum.addAssign( prevTexture.sample( uvNode.add( vec2( o.x, o.y.negate() ) ) ).rgb.mul( 2.0 ) );
- const blurred = sum.div( 12.0 );
- // redistribute the level's contribution between a tight and a wide bloom
- const weight = mix( bloomFactor, float( 1.2 ).sub( bloomFactor ), this.radius );
- const added = addTexture.sample( uvNode ).rgb.mul( weight );
- // keep the total intensity independent of the level count ( the factors sum to `_nMips * 0.6` )
- const norm = 3 / ( this._nMips * 0.6 );
- const result = blurred.add( added ).mul( mix( float( 1.0 ), this.strength.mul( norm ), finalFlag ) );
- return vec4( result, 1.0 );
- } );
- const material = new NodeMaterial();
- material.fragmentNode = upsamplePass().context( builder.getSharedContext() );
- material.name = 'DualKawaseBloom_up';
- material.needsUpdate = true;
- material.prevTexture = prevTexture;
- material.addTexture = addTexture;
- material.texelSize = texelSize;
- material.finalFlag = finalFlag;
- material.bloomFactor = bloomFactor;
- return material;
- }
- }
- /**
- * TSL function for creating a bloom effect.
- *
- * @tsl
- * @function
- * @param {Node<vec4>} node - The node that represents the input of the effect.
- * @param {number} [strength=1] - The strength of the bloom.
- * @param {number} [radius=0] - The radius of the bloom.
- * @param {number} [threshold=0] - The luminance threshold limits which bright areas contribute to the bloom effect.
- * @returns {DualKawaseBloomNode}
- */
- export const dualKawaseBloom = ( node, strength, radius, threshold ) => new DualKawaseBloomNode( nodeObject( node ), strength, radius, threshold );
- export default DualKawaseBloomNode;
|