Home Reference Source

src/passes/MipmapBlurPass.js

import { SRGBColorSpace, UnsignedByteType, Vector2, WebGLRenderTarget } from "three";
import { DownsamplingMaterial } from "../materials/DownsamplingMaterial.js";
import { UpsamplingMaterial } from "../materials/UpsamplingMaterial.js";
import { Pass } from "./Pass.js";

/**
 * A blur pass that produces a wide blur by downsampling and upsampling the input over multiple MIP levels.
 *
 * Based on an article by Fabrice Piquet:
 * https://www.froyok.fr/blog/2021-12-ue4-custom-bloom/
 */

export class MipmapBlurPass extends Pass {

	/**
	 * Constructs a new mipmap blur pass.
	 *
	 * @param {Object} [options] - The options.
	 */

	constructor() {

		super("MipmapBlurPass");

		this.needsSwap = false;

		/**
		 * The output render target.
		 *
		 * @type {WebGLRenderTarget}
		 * @private
		 */

		this.renderTarget = new WebGLRenderTarget(1, 1, { depthBuffer: false });
		this.renderTarget.texture.name = "Upsampling.Mipmap0";

		/**
		 * The mipmaps used for downsampling.
		 *
		 * @type {WebGLRenderTarget[]}
		 * @readonly
		 */

		this.downsamplingMipmaps = [];

		/**
		 * The mipmaps used for upsampling.
		 *
		 * @type {WebGLRenderTarget[]}
		 * @readonly
		 */

		this.upsamplingMipmaps = [];

		/**
		 * A downsampling material.
		 *
		 * @type {DownsamplingMaterial}
		 * @private
		 */

		this.downsamplingMaterial = new DownsamplingMaterial();

		/**
		 * An upsampling material.
		 *
		 * @type {UpsamplingMaterial}
		 * @private
		 */

		this.upsamplingMaterial = new UpsamplingMaterial();

		/**
		 * The current resolution.
		 *
		 * @type {Vector2}
		 * @private
		 */

		this.resolution = new Vector2();

	}

	/**
	 * A texture that contains the blurred result.
	 *
	 * @type {Texture}
	 */

	get texture() {

		return this.renderTarget.texture;

	}

	/**
	 * The MIP levels. Default is 8.
	 *
	 * @type {Number}
	 */

	get levels() {

		return this.downsamplingMipmaps.length;

	}

	set levels(value) {

		if(this.levels !== value) {

			const renderTarget = this.renderTarget;

			this.dispose();
			this.downsamplingMipmaps = [];
			this.upsamplingMipmaps = [];

			for(let i = 0; i < value; ++i) {

				const mipmap = renderTarget.clone();
				mipmap.texture.name = "Downsampling.Mipmap" + i;
				this.downsamplingMipmaps.push(mipmap);

			}

			this.upsamplingMipmaps.push(renderTarget);

			for(let i = 1, l = value - 1; i < l; ++i) {

				const mipmap = renderTarget.clone();
				mipmap.texture.name = "Upsampling.Mipmap" + i;
				this.upsamplingMipmaps.push(mipmap);

			}

			this.setSize(this.resolution.x, this.resolution.y);

		}

	}

	/**
	 * The blur radius.
	 *
	 * @type {Number}
	 */

	get radius() {

		return this.upsamplingMaterial.radius;

	}

	set radius(value) {

		this.upsamplingMaterial.radius = value;

	}

	/**
	 * Renders the blur.
	 *
	 * @param {WebGLRenderer} renderer - The renderer.
	 * @param {WebGLRenderTarget} inputBuffer - A frame buffer that contains the result of the previous pass.
	 * @param {WebGLRenderTarget} outputBuffer - A frame buffer that serves as the output render target unless this pass renders to screen.
	 * @param {Number} [deltaTime] - The time between the last frame and the current one in seconds.
	 * @param {Boolean} [stencilTest] - Indicates whether a stencil mask is active.
	 */

	render(renderer, inputBuffer, outputBuffer, deltaTime, stencilTest) {

		const { scene, camera } = this;
		const { downsamplingMaterial, upsamplingMaterial } = this;
		const { downsamplingMipmaps, upsamplingMipmaps } = this;

		let previousBuffer = inputBuffer;

		// Downsample the input to the highest MIP level (smallest mipmap).
		this.fullscreenMaterial = downsamplingMaterial;

		for(let i = 0, l = downsamplingMipmaps.length; i < l; ++i) {

			const mipmap = downsamplingMipmaps[i];
			downsamplingMaterial.setSize(previousBuffer.width, previousBuffer.height);
			downsamplingMaterial.inputBuffer = previousBuffer.texture;
			renderer.setRenderTarget(mipmap);
			renderer.render(scene, camera);
			previousBuffer = mipmap;

		}

		// Upsample the result back to the lowest MIP level (largest mipmap, half resolution).
		this.fullscreenMaterial = upsamplingMaterial;

		// A + B = C, then D + C = F, etc.
		for(let i = upsamplingMipmaps.length - 1; i >= 0; --i) {

			const mipmap = upsamplingMipmaps[i];
			upsamplingMaterial.setSize(previousBuffer.width, previousBuffer.height);
			upsamplingMaterial.inputBuffer = previousBuffer.texture;
			upsamplingMaterial.supportBuffer = downsamplingMipmaps[i].texture;
			renderer.setRenderTarget(mipmap);
			renderer.render(scene, camera);
			previousBuffer = mipmap;

		}

	}

	/**
	 * Updates the size of this pass.
	 *
	 * @param {Number} width - The width.
	 * @param {Number} height - The height.
	 */

	setSize(width, height) {

		const resolution = this.resolution;
		resolution.set(width, height);

		let w = resolution.width, h = resolution.height;

		for(let i = 0, l = this.downsamplingMipmaps.length; i < l; ++i) {

			w = Math.round(w * 0.5);
			h = Math.round(h * 0.5);

			this.downsamplingMipmaps[i].setSize(w, h);

			if(i < this.upsamplingMipmaps.length) {

				this.upsamplingMipmaps[i].setSize(w, h);

			}

		}

	}

	/**
	 * Performs initialization tasks.
	 *
	 * @param {WebGLRenderer} renderer - The renderer.
	 * @param {Boolean} alpha - Whether the renderer uses the alpha channel or not.
	 * @param {Number} frameBufferType - The type of the main frame buffers.
	 */

	initialize(renderer, alpha, frameBufferType) {

		if(frameBufferType !== undefined) {

			const mipmaps = this.downsamplingMipmaps.concat(this.upsamplingMipmaps);

			for(const mipmap of mipmaps) {

				mipmap.texture.type = frameBufferType;

			}

			if(frameBufferType !== UnsignedByteType) {

				this.downsamplingMaterial.defines.FRAMEBUFFER_PRECISION_HIGH = "1";
				this.upsamplingMaterial.defines.FRAMEBUFFER_PRECISION_HIGH = "1";

			} else if(renderer !== null && renderer.outputColorSpace === SRGBColorSpace) {

				for(const mipmap of mipmaps) {

					mipmap.texture.colorSpace = SRGBColorSpace;

				}

			}

		}

	}

	/**
	 * Deletes internal render targets and textures.
	 */

	dispose() {

		super.dispose();

		for(const mipmap of this.downsamplingMipmaps.concat(this.upsamplingMipmaps)) {

			mipmap.dispose();

		}

	}

}