Home Reference Source

src/effects/SMAAEffect.js

import {
	BasicDepthPacking,
	Color,
	LinearFilter,
	LoadingManager,
	NearestFilter,
	Texture,
	Uniform,
	WebGLRenderTarget
} from "three";

import { BlendFunction } from "../enums/BlendFunction.js";
import { EdgeDetectionMode } from "../enums/EdgeDetectionMode.js";
import { EffectAttribute } from "../enums/EffectAttribute.js";
import { PredicationMode } from "../enums/PredicationMode.js";
import { SMAAPreset } from "../enums/SMAAPreset.js";
import { EdgeDetectionMaterial } from "../materials/EdgeDetectionMaterial.js";
import { SMAAWeightsMaterial } from "../materials/SMAAWeightsMaterial.js";
import { ClearPass } from "../passes/ClearPass.js";
import { ShaderPass } from "../passes/ShaderPass.js";
import { Effect } from "./Effect.js";

import searchImageDataURL from "../textures/smaa/searchImageDataURL.js";
import areaImageDataURL from "../textures/smaa/areaImageDataURL.js";

import fragmentShader from "./glsl/smaa.frag";
import vertexShader from "./glsl/smaa.vert";

/**
 * Subpixel Morphological Antialiasing (SMAA).
 *
 * https://github.com/iryoku/smaa/releases/tag/v2.8
 */

export class SMAAEffect extends Effect {

	/**
	 * Constructs a new SMAA effect.
	 *
	 * @param {Object} [options] - The options.
	 * @param {BlendFunction} [options.blendFunction=BlendFunction.SRC] - The blend function of this effect.
	 * @param {SMAAPreset} [options.preset=SMAAPreset.MEDIUM] - The quality preset.
	 * @param {EdgeDetectionMode} [options.edgeDetectionMode=EdgeDetectionMode.COLOR] - The edge detection mode.
	 * @param {PredicationMode} [options.predicationMode=PredicationMode.DISABLED] - The predication mode.
	 */

	constructor({
		blendFunction = BlendFunction.SRC,
		preset = SMAAPreset.MEDIUM,
		edgeDetectionMode = EdgeDetectionMode.COLOR,
		predicationMode = PredicationMode.DISABLED
	} = {}) {

		super("SMAAEffect", fragmentShader, {
			vertexShader,
			blendFunction,
			attributes: EffectAttribute.CONVOLUTION | EffectAttribute.DEPTH,
			uniforms: new Map([
				["weightMap", new Uniform(null)]
			])
		});

		// TODO Added for backward-compatibility.
		let searchImage, areaImage;

		if(arguments.length > 1) {

			searchImage = arguments[0];
			areaImage = arguments[1];

			if(arguments.length > 2) {

				preset = arguments[2];

			}

			if(arguments.length > 3) {

				edgeDetectionMode = arguments[3];

			}

		}

		/**
		 * A render target for the edge detection.
		 *
		 * @type {WebGLRenderTarget}
		 * @private
		 */

		this.renderTargetEdges = new WebGLRenderTarget(1, 1, { depthBuffer: false });
		this.renderTargetEdges.texture.name = "SMAA.Edges";

		/**
		 * A render target for the edge weights.
		 *
		 * @type {WebGLRenderTarget}
		 * @private
		 */

		this.renderTargetWeights = this.renderTargetEdges.clone();
		this.renderTargetWeights.texture.name = "SMAA.Weights";
		this.uniforms.get("weightMap").value = this.renderTargetWeights.texture;

		/**
		 * A clear pass for the edges buffer.
		 *
		 * @type {ClearPass}
		 * @private
		 */

		this.clearPass = new ClearPass(true, false, false);
		this.clearPass.overrideClearColor = new Color(0x000000);
		this.clearPass.overrideClearAlpha = 1;

		/**
		 * An edge detection pass.
		 *
		 * @type {ShaderPass}
		 * @private
		 */

		this.edgeDetectionPass = new ShaderPass(new EdgeDetectionMaterial());
		this.edgeDetectionMaterial.edgeDetectionMode = edgeDetectionMode;
		this.edgeDetectionMaterial.predicationMode = predicationMode;

		/**
		 * An SMAA weights pass.
		 *
		 * @type {ShaderPass}
		 * @private
		 */

		this.weightsPass = new ShaderPass(new SMAAWeightsMaterial());

		// Load the lookup textures.
		const loadingManager = new LoadingManager();
		loadingManager.onLoad = () => {

			const searchTexture = new Texture(searchImage);
			searchTexture.name = "SMAA.Search";
			searchTexture.magFilter = NearestFilter;
			searchTexture.minFilter = NearestFilter;
			searchTexture.generateMipmaps = false;
			searchTexture.needsUpdate = true;
			searchTexture.flipY = true;
			this.weightsMaterial.searchTexture = searchTexture;

			const areaTexture = new Texture(areaImage);
			areaTexture.name = "SMAA.Area";
			areaTexture.magFilter = LinearFilter;
			areaTexture.minFilter = LinearFilter;
			areaTexture.generateMipmaps = false;
			areaTexture.needsUpdate = true;
			areaTexture.flipY = false;
			this.weightsMaterial.areaTexture = areaTexture;

			this.dispatchEvent({ type: "load" });

		};

		loadingManager.itemStart("search");
		loadingManager.itemStart("area");

		if(searchImage !== undefined && areaImage !== undefined) {

			// Use the provided images.
			loadingManager.itemEnd("search");
			loadingManager.itemEnd("area");

		} else if(typeof Image !== "undefined") {

			// Load the lookup textures.
			searchImage = new Image();
			areaImage = new Image();
			searchImage.addEventListener("load", () => loadingManager.itemEnd("search"));
			areaImage.addEventListener("load", () => loadingManager.itemEnd("area"));
			searchImage.src = searchImageDataURL;
			areaImage.src = areaImageDataURL;

		}

		this.applyPreset(preset);

	}

	/**
	 * The edges texture.
	 *
	 * @type {Texture}
	 */

	get edgesTexture() {

		return this.renderTargetEdges.texture;

	}

	/**
	 * Returns the edges texture.
	 *
	 * @deprecated Use edgesTexture instead.
	 * @return {Texture} The texture.
	 */

	getEdgesTexture() {

		return this.edgesTexture;

	}

	/**
	 * The edge weights texture.
	 *
	 * @type {Texture}
	 */

	get weightsTexture() {

		return this.renderTargetWeights.texture;

	}

	/**
	 * Returns the edge weights texture.
	 *
	 * @deprecated Use weightsTexture instead.
	 * @return {Texture} The texture.
	 */

	getWeightsTexture() {

		return this.weightsTexture;

	}

	/**
	 * The edge detection material.
	 *
	 * @type {EdgeDetectionMaterial}
	 */

	get edgeDetectionMaterial() {

		return this.edgeDetectionPass.fullscreenMaterial;

	}

	/**
	 * The edge detection material.
	 *
	 * @type {EdgeDetectionMaterial}
	 * @deprecated Use edgeDetectionMaterial instead.
	 */

	get colorEdgesMaterial() {

		return this.edgeDetectionMaterial;

	}

	/**
	 * Returns the edge detection material.
	 *
	 * @deprecated Use edgeDetectionMaterial instead.
	 * @return {EdgeDetectionMaterial} The material.
	 */

	getEdgeDetectionMaterial() {

		return this.edgeDetectionMaterial;

	}

	/**
	 * The edge weights material.
	 *
	 * @type {SMAAWeightsMaterial}
	 */

	get weightsMaterial() {

		return this.weightsPass.fullscreenMaterial;

	}

	/**
	 * Returns the edge weights material.
	 *
	 * @deprecated Use weightsMaterial instead.
	 * @return {SMAAWeightsMaterial} The material.
	 */

	getWeightsMaterial() {

		return this.weightsMaterial;

	}

	/**
	 * Sets the edge detection sensitivity.
	 *
	 * See {@link EdgeDetectionMaterial#setEdgeDetectionThreshold} for more details.
	 *
	 * @deprecated Use edgeDetectionMaterial instead.
	 * @param {Number} threshold - The edge detection sensitivity. Range: [0.05, 0.5].
	 */

	setEdgeDetectionThreshold(threshold) {

		this.edgeDetectionMaterial.edgeDetectionThreshold = threshold;

	}

	/**
	 * Sets the maximum amount of horizontal/vertical search steps.
	 *
	 * See {@link SMAAWeightsMaterial#setOrthogonalSearchSteps} for more details.
	 *
	 * @deprecated Use weightsMaterial instead.
	 * @param {Number} steps - The search steps. Range: [0, 112].
	 */

	setOrthogonalSearchSteps(steps) {

		this.weightsMaterial.orthogonalSearchSteps = steps;

	}

	/**
	 * Applies the given quality preset.
	 *
	 * @param {SMAAPreset} preset - The preset.
	 */

	applyPreset(preset) {

		const edgeDetectionMaterial = this.edgeDetectionMaterial;
		const weightsMaterial = this.weightsMaterial;

		switch(preset) {

			case SMAAPreset.LOW:
				edgeDetectionMaterial.edgeDetectionThreshold = 0.15;
				weightsMaterial.orthogonalSearchSteps = 4;
				weightsMaterial.diagonalDetection = false;
				weightsMaterial.cornerDetection = false;
				break;

			case SMAAPreset.MEDIUM:
				edgeDetectionMaterial.edgeDetectionThreshold = 0.1;
				weightsMaterial.orthogonalSearchSteps = 8;
				weightsMaterial.diagonalDetection = false;
				weightsMaterial.cornerDetection = false;
				break;

			case SMAAPreset.HIGH:
				edgeDetectionMaterial.edgeDetectionThreshold = 0.1;
				weightsMaterial.orthogonalSearchSteps = 16;
				weightsMaterial.diagonalSearchSteps = 8;
				weightsMaterial.cornerRounding = 25;
				weightsMaterial.diagonalDetection = true;
				weightsMaterial.cornerDetection = true;
				break;

			case SMAAPreset.ULTRA:
				edgeDetectionMaterial.edgeDetectionThreshold = 0.05;
				weightsMaterial.orthogonalSearchSteps = 32;
				weightsMaterial.diagonalSearchSteps = 16;
				weightsMaterial.cornerRounding = 25;
				weightsMaterial.diagonalDetection = true;
				weightsMaterial.cornerDetection = true;
				break;

		}

	}

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

	setDepthTexture(depthTexture, depthPacking = BasicDepthPacking) {

		this.edgeDetectionMaterial.depthBuffer = depthTexture;
		this.edgeDetectionMaterial.depthPacking = depthPacking;

	}

	/**
	 * Updates this effect.
	 *
	 * @param {WebGLRenderer} renderer - The renderer.
	 * @param {WebGLRenderTarget} inputBuffer - A frame buffer that contains the result of the previous pass.
	 * @param {Number} [deltaTime] - The time between the last frame and the current one in seconds.
	 */

	update(renderer, inputBuffer, deltaTime) {

		this.clearPass.render(renderer, this.renderTargetEdges);
		this.edgeDetectionPass.render(renderer, inputBuffer, this.renderTargetEdges);
		this.weightsPass.render(renderer, this.renderTargetEdges, this.renderTargetWeights);

	}

	/**
	 * Updates the size of internal render targets.
	 *
	 * @param {Number} width - The width.
	 * @param {Number} height - The height.
	 */

	setSize(width, height) {

		this.edgeDetectionMaterial.setSize(width, height);
		this.weightsMaterial.setSize(width, height);
		this.renderTargetEdges.setSize(width, height);
		this.renderTargetWeights.setSize(width, height);

	}

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

	dispose() {

		const { searchTexture, areaTexture } = this.weightsMaterial;

		if(searchTexture !== null && areaTexture !== null) {

			searchTexture.dispose();
			areaTexture.dispose();

		}

		super.dispose();

	}

	/**
	 * The SMAA search image, encoded as a base64 data URL.
	 *
	 * @type {String}
	 * @deprecated
	 */

	static get searchImageDataURL() {

		return searchImageDataURL;

	}

	/**
	 * The SMAA area image, encoded as a base64 data URL.
	 *
	 * @type {String}
	 * @deprecated
	 */

	static get areaImageDataURL() {

		return areaImageDataURL;

	}

}