Home Reference Source

src/passes/ToneMappingPass.js

import {
	LinearFilter,
	LinearMipMapLinearFilter,
	MeshBasicMaterial,
	RGBFormat,
	WebGLRenderTarget
} from "three";

import {
	AdaptiveLuminosityMaterial,
	CopyMaterial,
	LuminosityMaterial,
	ToneMappingMaterial
} from "../materials";

import { Pass } from "./Pass.js";

/**
 * A tone mapping pass that supports adaptive luminosity.
 *
 * If adaptivity is enabled, this pass generates a texture that represents the
 * luminosity of the current scene and adjusts it over time to simulate the
 * optic nerve responding to the amount of light it is receiving.
 *
 * Reference:
 *  GDC2007 - Wolfgang Engel, Post-Processing Pipeline
 *  http://perso.univ-lyon1.fr/jean-claude.iehl/Public/educ/GAMA/2007/gdc07/Post-Processing_Pipeline.pdf
 */

export class ToneMappingPass extends Pass {

	/**
	 * Constructs a new tone mapping pass.
	 *
	 * @param {Object} [options] - The options.
	 * @param {Boolean} [options.adaptive=true] - Whether the tone mapping should use an adaptive luminance map.
	 * @param {Number} [options.resolution=256] - The render texture resolution.
	 * @param {Number} [options.distinction=1.0] - A luminance distinction factor.
	 */

	constructor(options = {}) {

		super("ToneMappingPass");

		/**
		 * The render target for the current luminosity.
		 *
		 * @type {WebGLRenderTarget}
		 * @private
		 * @todo Use RED format in WebGL 2.0.
		 */

		this.renderTargetLuminosity = new WebGLRenderTarget(1, 1, {
			minFilter: LinearMipMapLinearFilter,
			magFilter: LinearFilter,
			format: RGBFormat,
			stencilBuffer: false,
			depthBuffer: false
		});

		this.renderTargetLuminosity.texture.name = "ToneMapping.Luminosity";

		/**
		 * The render target for adapted luminosity.
		 *
		 * @type {WebGLRenderTarget}
		 * @private
		 */

		this.renderTargetAdapted = this.renderTargetLuminosity.clone();

		this.renderTargetAdapted.texture.name = "ToneMapping.AdaptedLuminosity";
		this.renderTargetAdapted.texture.generateMipmaps = false;
		this.renderTargetAdapted.texture.minFilter = LinearFilter;

		/**
		 * A render target that holds a copy of the adapted limonosity.
		 *
		 * @type {WebGLRenderTarget}
		 * @private
		 */

		this.renderTargetPrevious = this.renderTargetAdapted.clone();

		this.renderTargetPrevious.texture.name = "ToneMapping.PreviousLuminosity";

		/**
		 * Copy shader material used for saving the luminance map.
		 *
		 * @type {CopyMaterial}
		 * @private
		 */

		this.copyMaterial = new CopyMaterial();

		/**
		 * A luminosity shader material.
		 *
		 * @type {LuminosityMaterial}
		 * @private
		 */

		this.luminosityMaterial = new LuminosityMaterial();

		this.luminosityMaterial.uniforms.distinction.value = (options.distinction !== undefined) ? options.distinction : 1.0;

		/**
		 * An adaptive luminance shader material.
		 *
		 * @type {AdaptiveLuminosityMaterial}
		 * @private
		 */

		this.adaptiveLuminosityMaterial = new AdaptiveLuminosityMaterial();

		this.resolution = options.resolution;

		/**
		 * A tone mapping shader material.
		 *
		 * @type {ToneMappingMaterial}
		 * @private
		 */

		this.toneMappingMaterial = new ToneMappingMaterial();

		this.adaptive = options.adaptive;

	}

	/**
	 * The resolution of the render targets.
	 *
	 * @type {Number}
	 */

	get resolution() {

		return this.renderTargetLuminosity.width;

	}

	/**
	 * The resolution of the render targets. Must be a power of two for mipmaps.
	 *
	 * @type {Number}
	 */

	set resolution(value = 256) {

		// Round the given value to the next power of two.
		const exponent = Math.max(0, Math.ceil(Math.log2(value)));
		value = Math.pow(2, exponent);

		this.renderTargetLuminosity.setSize(value, value);
		this.renderTargetPrevious.setSize(value, value);
		this.renderTargetAdapted.setSize(value, value);

		this.adaptiveLuminosityMaterial.defines.MIP_LEVEL_1X1 = exponent.toFixed(1);
		this.adaptiveLuminosityMaterial.needsUpdate = true;

	}

	/**
	 * Whether this pass uses adaptive luminosity.
	 *
	 * @type {Boolean}
	 * @default true
	 */

	get adaptive() {

		return (this.toneMappingMaterial.defines.ADAPTED_LUMINANCE !== undefined);

	}

	/**
	 * Whether this pass should use adaptive luminosity.
	 *
	 * @type {Boolean}
	 */

	set adaptive(value = true) {

		if(value) {

			this.toneMappingMaterial.defines.ADAPTED_LUMINANCE = "1";
			this.toneMappingMaterial.uniforms.luminanceMap.value = this.renderTargetAdapted.texture;

		} else {

			delete this.toneMappingMaterial.defines.ADAPTED_LUMINANCE;
			this.toneMappingMaterial.uniforms.luminanceMap.value = null;

		}

		this.toneMappingMaterial.needsUpdate = true;

	}

	/**
	 * Indicates whether dithering is enabled.
	 *
	 * @type {Boolean}
	 */

	get dithering() {

		return this.toneMappingMaterial.dithering;

	}

	/**
	 * If enabled, the result will be dithered to remove banding artifacts.
	 *
	 * @type {Boolean}
	 */

	set dithering(value) {

		if(this.dithering !== value) {

			this.toneMappingMaterial.dithering = value;
			this.toneMappingMaterial.needsUpdate = true;

		}

	}

	/**
	 * Renders the effect.
	 *
	 * @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} [delta] - 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, delta, stencilTest) {

		const scene = this.scene;
		const camera = this.camera;

		const adaptiveLuminosityMaterial = this.adaptiveLuminosityMaterial;
		const luminosityMaterial = this.luminosityMaterial;
		const toneMappingMaterial = this.toneMappingMaterial;
		const copyMaterial = this.copyMaterial;

		const renderTargetPrevious = this.renderTargetPrevious;
		const renderTargetLuminosity = this.renderTargetLuminosity;
		const renderTargetAdapted = this.renderTargetAdapted;

		if(this.adaptive) {

			// Render the luminance of the current scene into a render target with mipmapping enabled.
			this.setFullscreenMaterial(luminosityMaterial);
			luminosityMaterial.uniforms.tDiffuse.value = inputBuffer.texture;
			renderer.render(scene, camera, renderTargetLuminosity);

			// Use the new luminance values, the previous luminance and the frame delta to adapt the luminance over time.
			this.setFullscreenMaterial(adaptiveLuminosityMaterial);
			adaptiveLuminosityMaterial.uniforms.delta.value = delta;
			adaptiveLuminosityMaterial.uniforms.tPreviousLum.value = renderTargetPrevious.texture;
			adaptiveLuminosityMaterial.uniforms.tCurrentLum.value = renderTargetLuminosity.texture;
			renderer.render(scene, camera, renderTargetAdapted);

			// Copy the new adapted luminance value so that it can be used by the next frame.
			this.setFullscreenMaterial(copyMaterial);
			copyMaterial.uniforms.tDiffuse.value = renderTargetAdapted.texture;
			renderer.render(scene, camera, renderTargetPrevious);

		}

		// Apply the tone mapping to the colours.
		this.setFullscreenMaterial(toneMappingMaterial);
		toneMappingMaterial.uniforms.tDiffuse.value = inputBuffer.texture;

		renderer.render(this.scene, this.camera, this.renderToScreen ? null : outputBuffer);

	}

	/**
	 * Performs initialization tasks.
	 *
	 * @param {WebGLRenderer} renderer - The renderer.
	 * @param {Boolean} alpha - Whether the renderer uses the alpha channel or not.
	 */

	initialize(renderer, alpha) {

		this.setFullscreenMaterial(new MeshBasicMaterial({ color: 0x7fffff }));
		renderer.render(this.scene, this.camera, this.renderTargetPrevious);
		this.getFullscreenMaterial().dispose();

	}

}