Home Reference Source

src/passes/EffectPass.js

import { BasicDepthPacking, NoColorSpace, SRGBColorSpace, UnsignedByteType } from "three";
import { EffectShaderData } from "../core/EffectShaderData.js";
import { BlendFunction } from "../enums/BlendFunction.js";
import { EffectAttribute } from "../enums/EffectAttribute.js";
import { EffectShaderSection as Section } from "../enums/EffectShaderSection.js";

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

/**
 * Prefixes substrings within the given strings.
 *
 * @private
 * @param {String} prefix - A prefix.
 * @param {Iterable<String>} substrings - The substrings.
 * @param {Map<String, String>} strings - A collection of named strings.
 */

function prefixSubstrings(prefix, substrings, strings) {

	for(const substring of substrings) {

		// Prefix the substring and build a RegExp that searches for the unprefixed version.
		const prefixed = "$1" + prefix + substring.charAt(0).toUpperCase() + substring.slice(1);
		const regExp = new RegExp("([^\\.])(\\b" + substring + "\\b)", "g");

		for(const entry of strings.entries()) {

			if(entry[1] !== null) {

				// Replace all occurances of the substring with the prefixed version.
				strings.set(entry[0], entry[1].replace(regExp, prefixed));

			}

		}

	}

}

/**
 * Integrates the given effect.
 *
 * @private
 * @param {String} prefix - A prefix.
 * @param {Effect} effect - The effect.
 * @param {EffectShaderData} data - Cumulative shader data.
 */

function integrateEffect(prefix, effect, data) {

	let fragmentShader = effect.getFragmentShader();
	let vertexShader = effect.getVertexShader();

	const mainImageExists = (fragmentShader !== undefined && /mainImage/.test(fragmentShader));
	const mainUvExists = (fragmentShader !== undefined && /mainUv/.test(fragmentShader));

	data.attributes |= effect.getAttributes();

	if(fragmentShader === undefined) {

		throw new Error(`Missing fragment shader (${effect.name})`);

	} else if(mainUvExists && (data.attributes & EffectAttribute.CONVOLUTION) !== 0) {

		throw new Error(`Effects that transform UVs are incompatible with convolution effects (${effect.name})`);

	} else if(!mainImageExists && !mainUvExists) {

		throw new Error(`Could not find mainImage or mainUv function (${effect.name})`);

	} else {

		const functionRegExp = /\w+\s+(\w+)\([\w\s,]*\)\s*{/g;

		const shaderParts = data.shaderParts;
		let fragmentHead = shaderParts.get(Section.FRAGMENT_HEAD) || "";
		let fragmentMainUv = shaderParts.get(Section.FRAGMENT_MAIN_UV) || "";
		let fragmentMainImage = shaderParts.get(Section.FRAGMENT_MAIN_IMAGE) || "";
		let vertexHead = shaderParts.get(Section.VERTEX_HEAD) || "";
		let vertexMainSupport = shaderParts.get(Section.VERTEX_MAIN_SUPPORT) || "";

		const varyings = new Set();
		const names = new Set();

		if(mainUvExists) {

			fragmentMainUv += `\t${prefix}MainUv(UV);\n`;
			data.uvTransformation = true;

		}

		if(vertexShader !== null && /mainSupport/.test(vertexShader)) {

			// Build the mainSupport call (with optional uv parameter).
			const needsUv = /mainSupport *\([\w\s]*?uv\s*?\)/.test(vertexShader);
			vertexMainSupport += `\t${prefix}MainSupport(`;
			vertexMainSupport += needsUv ? "vUv);\n" : ");\n";

			// Collect names of varyings and functions.
			for(const m of vertexShader.matchAll(/(?:varying\s+\w+\s+([\S\s]*?);)/g)) {

				// Handle unusual formatting and commas.
				for(const n of m[1].split(/\s*,\s*/)) {

					data.varyings.add(n);
					varyings.add(n);
					names.add(n);

				}

			}

			for(const m of vertexShader.matchAll(functionRegExp)) {

				names.add(m[1]);

			}

		}

		for(const m of fragmentShader.matchAll(functionRegExp)) {

			names.add(m[1]);

		}

		for(const d of effect.defines.keys()) {

			// Ignore parameters of function-like macros.
			names.add(d.replace(/\([\w\s,]*\)/g, ""));

		}

		for(const u of effect.uniforms.keys()) {

			names.add(u);

		}

		// Remove potential false positives.
		names.delete("while");
		names.delete("for");
		names.delete("if");

		// Store prefixed uniforms and macros.
		effect.uniforms.forEach((val, key) => data.uniforms.set(prefix + key.charAt(0).toUpperCase() + key.slice(1), val));
		effect.defines.forEach((val, key) => data.defines.set(prefix + key.charAt(0).toUpperCase() + key.slice(1), val));

		// Prefix varyings, functions, uniforms and macro values.
		const shaders = new Map([["fragment", fragmentShader], ["vertex", vertexShader]]);
		prefixSubstrings(prefix, names, data.defines);
		prefixSubstrings(prefix, names, shaders);
		fragmentShader = shaders.get("fragment");
		vertexShader = shaders.get("vertex");

		// Collect unique blend modes.
		const blendMode = effect.blendMode;
		data.blendModes.set(blendMode.blendFunction, blendMode);

		if(mainImageExists) {

			if(effect.inputColorSpace !== null && effect.inputColorSpace !== data.colorSpace) {

				fragmentMainImage += (effect.inputColorSpace === SRGBColorSpace) ?
					"color0 = LinearTosRGB(color0);\n\t" :
					"color0 = sRGBToLinear(color0);\n\t";

			}

			if(effect.outputColorSpace !== NoColorSpace) {

				data.colorSpace = effect.outputColorSpace;

			} else if(effect.inputColorSpace !== null) {

				data.colorSpace = effect.inputColorSpace;

			}

			const depthParamRegExp = /MainImage *\([\w\s,]*?depth[\w\s,]*?\)/;
			fragmentMainImage += `${prefix}MainImage(color0, UV, `;

			// Check if the effect reads depth in the fragment shader.
			if((data.attributes & EffectAttribute.DEPTH) !== 0 && depthParamRegExp.test(fragmentShader)) {

				fragmentMainImage += "depth, ";
				data.readDepth = true;

			}

			fragmentMainImage += "color1);\n\t";

			// Include the blend opacity uniform of this effect.
			const blendOpacity = prefix + "BlendOpacity";
			data.uniforms.set(blendOpacity, blendMode.opacity);

			// Blend the result of this effect with the input color (color0 = dst, color1 = src).
			fragmentMainImage += `color0 = blend${blendMode.blendFunction}(color0, color1, ${blendOpacity});\n\n\t`;
			fragmentHead += `uniform float ${blendOpacity};\n\n`;

		}

		// Include the modified code in the final shader.
		fragmentHead += fragmentShader + "\n";

		if(vertexShader !== null) {

			vertexHead += vertexShader + "\n";

		}

		shaderParts.set(Section.FRAGMENT_HEAD, fragmentHead);
		shaderParts.set(Section.FRAGMENT_MAIN_UV, fragmentMainUv);
		shaderParts.set(Section.FRAGMENT_MAIN_IMAGE, fragmentMainImage);
		shaderParts.set(Section.VERTEX_HEAD, vertexHead);
		shaderParts.set(Section.VERTEX_MAIN_SUPPORT, vertexMainSupport);

		if(effect.extensions !== null) {

			// Collect required WebGL extensions.
			for(const extension of effect.extensions) {

				data.extensions.add(extension);

			}

		}

	}

}

/**
 * An effect pass.
 *
 * Use this pass to combine {@link Effect} instances.
 *
 * @implements {EventListener}
 */

export class EffectPass extends Pass {

	/**
	 * Constructs a new effect pass.
	 *
	 * @param {Camera} camera - The main camera.
	 * @param {...Effect} effects - The effects that will be rendered by this pass.
	 */

	constructor(camera, ...effects) {

		super("EffectPass");

		this.fullscreenMaterial = new EffectMaterial(null, null, null, camera);

		/**
		 * An event listener that forwards events to {@link handleEvent}.
		 *
		 * @type {EventListener}
		 * @private
		 */

		this.listener = (event) => this.handleEvent(event);

		/**
		 * The effects.
		 *
		 * Use `updateMaterial` or `recompile` after changing the effects and consider calling `dispose` to free resources
		 * of unused effects.
		 *
		 * @type {Effect[]}
		 * @private
		 */

		this.effects = [];
		this.setEffects(effects);

		/**
		 * Indicates whether this pass should skip rendering.
		 *
		 * Effects will still be updated, even if this flag is true.
		 *
		 * @type {Boolean}
		 * @private
		 */

		this.skipRendering = false;

		/**
		 * A time offset.
		 *
		 * Elapsed time will start at this value.
		 *
		 * @type {Number}
		 * @deprecated
		 */

		this.minTime = 1.0;

		/**
		 * The maximum time.
		 *
		 * If the elapsed time exceeds this value, it will be reset.
		 *
		 * @type {Number}
		 * @deprecated
		 */

		this.maxTime = Number.POSITIVE_INFINITY;

		/**
		 * The animation time scale.
		 *
		 * @type {Number}
		 */

		this.timeScale = 1.0;

	}

	set mainScene(value) {

		for(const effect of this.effects) {

			effect.mainScene = value;

		}

	}

	set mainCamera(value) {

		this.fullscreenMaterial.copyCameraSettings(value);

		for(const effect of this.effects) {

			effect.mainCamera = value;

		}

	}

	/**
	 * Indicates whether this pass encodes its output when rendering to screen.
	 *
	 * @type {Boolean}
	 * @deprecated Use fullscreenMaterial.encodeOutput instead.
	 */

	get encodeOutput() {

		return this.fullscreenMaterial.encodeOutput;

	}

	set encodeOutput(value) {

		this.fullscreenMaterial.encodeOutput = value;

	}

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

	get dithering() {

		return this.fullscreenMaterial.dithering;

	}

	set dithering(value) {

		const material = this.fullscreenMaterial;
		material.dithering = value;
		material.needsUpdate = true;

	}

	/**
	 * Sets the effects.
	 *
	 * @param {Effect[]} effects - The effects.
	 * @protected
	 */

	setEffects(effects) {

		for(const effect of this.effects) {

			effect.removeEventListener("change", this.listener);

		}

		this.effects = effects.sort((a, b) => (b.attributes - a.attributes));

		for(const effect of this.effects) {

			effect.addEventListener("change", this.listener);

		}

	}

	/**
	 * Updates the compound shader material.
	 *
	 * @protected
	 */

	updateMaterial() {

		const data = new EffectShaderData();
		let id = 0;

		for(const effect of this.effects) {

			if(effect.blendMode.blendFunction === BlendFunction.DST) {

				// Check if this effect relies on depth and continue.
				data.attributes |= (effect.getAttributes() & EffectAttribute.DEPTH);

			} else if((data.attributes & effect.getAttributes() & EffectAttribute.CONVOLUTION) !== 0) {

				throw new Error(`Convolution effects cannot be merged (${effect.name})`);

			} else {

				integrateEffect("e" + id++, effect, data);

			}

		}

		let fragmentHead = data.shaderParts.get(Section.FRAGMENT_HEAD);
		let fragmentMainImage = data.shaderParts.get(Section.FRAGMENT_MAIN_IMAGE);
		let fragmentMainUv = data.shaderParts.get(Section.FRAGMENT_MAIN_UV);

		// Integrate the relevant blend functions.
		const blendRegExp = /\bblend\b/g;

		for(const blendMode of data.blendModes.values()) {

			fragmentHead += blendMode.getShaderCode().replace(blendRegExp, `blend${blendMode.blendFunction}`) + "\n";

		}

		// Check if any effect relies on depth.
		if((data.attributes & EffectAttribute.DEPTH) !== 0) {

			// Check if depth should be read.
			if(data.readDepth) {

				fragmentMainImage = "float depth = readDepth(UV);\n\n\t" + fragmentMainImage;

			}

			// Only request a depth texture if none has been provided yet.
			this.needsDepthTexture = (this.getDepthTexture() === null);

		} else {

			this.needsDepthTexture = false;

		}

		if(data.colorSpace === SRGBColorSpace) {

			// Convert back to linear.
			fragmentMainImage += "color0 = sRGBToLinear(color0);\n\t";

		}

		// Check if any effect transforms UVs in the fragment shader.
		if(data.uvTransformation) {

			fragmentMainUv = "vec2 transformedUv = vUv;\n" + fragmentMainUv;
			data.defines.set("UV", "transformedUv");

		} else {

			data.defines.set("UV", "vUv");

		}

		data.shaderParts.set(Section.FRAGMENT_HEAD, fragmentHead);
		data.shaderParts.set(Section.FRAGMENT_MAIN_IMAGE, fragmentMainImage);
		data.shaderParts.set(Section.FRAGMENT_MAIN_UV, fragmentMainUv);

		// Ensure that leading preprocessor directives start on a new line.
		for(const [key, value] of data.shaderParts) {

			if(value !== null) {

				data.shaderParts.set(key, value.trim().replace(/^#/, "\n#"));

			}

		}

		this.skipRendering = (id === 0);
		this.needsSwap = !this.skipRendering;
		this.fullscreenMaterial.setShaderData(data);

	}

	/**
	 * Rebuilds the shader material.
	 */

	recompile() {

		this.updateMaterial();

	}

	/**
	 * Returns the current depth texture.
	 *
	 * @return {Texture} The current depth texture, or null if there is none.
	 */

	getDepthTexture() {

		return this.fullscreenMaterial.depthBuffer;

	}

	/**
	 * Sets the depth texture.
	 *
	 * @param {Texture} depthTexture - A depth texture.
	 * @param {DepthPackingStrategies} [depthPacking=BasicDepthPacking] - The depth packing.
	 */

	setDepthTexture(depthTexture, depthPacking = BasicDepthPacking) {

		this.fullscreenMaterial.depthBuffer = depthTexture;
		this.fullscreenMaterial.depthPacking = depthPacking;

		for(const effect of this.effects) {

			effect.setDepthTexture(depthTexture, depthPacking);

		}

	}

	/**
	 * 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} [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) {

		for(const effect of this.effects) {

			effect.update(renderer, inputBuffer, deltaTime);

		}

		if(!this.skipRendering || this.renderToScreen) {

			const material = this.fullscreenMaterial;
			material.inputBuffer = inputBuffer.texture;
			material.time += deltaTime * this.timeScale;

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

		}

	}

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

	setSize(width, height) {

		this.fullscreenMaterial.setSize(width, height);

		for(const effect of this.effects) {

			effect.setSize(width, height);

		}

	}

	/**
	 * 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) {

		this.renderer = renderer;

		// Initialize effects before building the shader.
		for(const effect of this.effects) {

			effect.initialize(renderer, alpha, frameBufferType);

		}

		// Initialize the fullscreen material.
		this.updateMaterial();

		if(frameBufferType !== undefined && frameBufferType !== UnsignedByteType) {

			this.fullscreenMaterial.defines.FRAMEBUFFER_PRECISION_HIGH = "1";

		}

	}

	/**
	 * Deletes disposable objects.
	 */

	dispose() {

		super.dispose();

		for(const effect of this.effects) {

			effect.removeEventListener("change", this.listener);
			effect.dispose();

		}

	}

	/**
	 * Handles events.
	 *
	 * @param {Event} event - An event.
	 */

	handleEvent(event) {

		switch(event.type) {

			case "change":
				this.recompile();
				break;

		}

	}

}