Home Reference Source

src/materials/SSAOMaterial.js

import { BasicDepthPacking, Matrix4, NoBlending, PerspectiveCamera, ShaderMaterial, Uniform, Vector2 } from "three";
import { orthographicDepthToViewZ } from "../utils/orthographicDepthToViewZ.js";
import { viewZToOrthographicDepth } from "../utils/viewZToOrthographicDepth.js";

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

/**
 * A Screen Space Ambient Occlusion (SSAO) shader material.
 *
 * @implements {Resizable}
 */

export class SSAOMaterial extends ShaderMaterial {

	/**
	 * Constructs a new SSAO material.
	 *
	 * @param {Camera} camera - A camera.
	 */

	constructor(camera) {

		super({
			name: "SSAOMaterial",
			defines: {
				SAMPLES_INT: "0",
				INV_SAMPLES_FLOAT: "0.0",
				SPIRAL_TURNS: "0.0",
				RADIUS: "1.0",
				RADIUS_SQ: "1.0",
				DISTANCE_SCALING: "1",
				DEPTH_PACKING: "0"
			},
			uniforms: {
				depthBuffer: new Uniform(null),
				normalBuffer: new Uniform(null),
				normalDepthBuffer: new Uniform(null),
				noiseTexture: new Uniform(null),
				inverseProjectionMatrix: new Uniform(new Matrix4()),
				projectionMatrix: new Uniform(new Matrix4()),
				texelSize: new Uniform(new Vector2()),
				cameraNearFar: new Uniform(new Vector2()),
				distanceCutoff: new Uniform(new Vector2()),
				proximityCutoff: new Uniform(new Vector2()),
				noiseScale: new Uniform(new Vector2()),
				minRadiusScale: new Uniform(0.33),
				intensity: new Uniform(1.0),
				fade: new Uniform(0.01),
				bias: new Uniform(0.0)
			},
			blending: NoBlending,
			toneMapped: false,
			depthWrite: false,
			depthTest: false,
			fragmentShader,
			vertexShader
		});

		this.copyCameraSettings(camera);

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

		this.resolution = new Vector2();

		/**
		 * The relative sampling radius.
		 *
		 * @type {Number}
		 * @private
		 */

		this.r = 1.0;

	}

	/**
	 * The current near plane setting.
	 *
	 * @type {Number}
	 * @private
	 */

	get near() {

		return this.uniforms.cameraNearFar.value.x;

	}

	/**
	 * The current far plane setting.
	 *
	 * @type {Number}
	 * @private
	 */

	get far() {

		return this.uniforms.cameraNearFar.value.y;

	}

	/**
	 * A combined normal-depth buffer.
	 *
	 * @type {Texture}
	 */

	set normalDepthBuffer(value) {

		this.uniforms.normalDepthBuffer.value = value;

		if(value !== null) {

			this.defines.NORMAL_DEPTH = "1";

		} else {

			delete this.defines.NORMAL_DEPTH;

		}

		this.needsUpdate = true;

	}

	/**
	 * Sets the combined normal-depth buffer.
	 *
	 * @deprecated Use normalDepthBuffer instead.
	 * @param {Number} value - The buffer.
	 */

	setNormalDepthBuffer(value) {

		this.normalDepthBuffer = value;

	}

	/**
	 * The normal buffer.
	 *
	 * @type {Texture}
	 */

	set normalBuffer(value) {

		this.uniforms.normalBuffer.value = value;

	}

	/**
	 * Sets the normal buffer.
	 *
	 * @deprecated Use normalBuffer instead.
	 * @param {Number} value - The buffer.
	 */

	setNormalBuffer(value) {

		this.uniforms.normalBuffer.value = value;

	}

	/**
	 * The depth buffer.
	 *
	 * @type {Texture}
	 */

	set depthBuffer(value) {

		this.uniforms.depthBuffer.value = value;

	}

	/**
	 * The depth packing strategy.
	 *
	 * @type {DepthPackingStrategies}
	 */

	set depthPacking(value) {

		this.defines.DEPTH_PACKING = value.toFixed(0);
		this.needsUpdate = true;

	}

	/**
	 * Sets the depth buffer.
	 *
	 * @deprecated Use depthBuffer and depthPacking instead.
	 * @param {Texture} buffer - The depth texture.
	 * @param {DepthPackingStrategies} [depthPacking=BasicDepthPacking] - The depth packing strategy.
	 */

	setDepthBuffer(buffer, depthPacking = BasicDepthPacking) {

		this.depthBuffer = buffer;
		this.depthPacking = depthPacking;

	}

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

	set noiseTexture(value) {

		this.uniforms.noiseTexture.value = value;

	}

	/**
	 * Sets the noise texture.
	 *
	 * @deprecated Use noiseTexture instead.
	 * @param {Number} value - The texture.
	 */

	setNoiseTexture(value) {

		this.uniforms.noiseTexture.value = value;

	}

	/**
	 * The sample count.
	 *
	 * @type {Number}
	 */

	get samples() {

		return Number(this.defines.SAMPLES_INT);

	}

	set samples(value) {

		this.defines.SAMPLES_INT = value.toFixed(0);
		this.defines.INV_SAMPLES_FLOAT = (1.0 / value).toFixed(9);
		this.needsUpdate = true;

	}

	/**
	 * Returns the amount of occlusion samples per pixel.
	 *
	 * @deprecated Use samples instead.
	 * @return {Number} The sample count.
	 */

	getSamples() {

		return this.samples;

	}

	/**
	 * Sets the amount of occlusion samples per pixel.
	 *
	 * @deprecated Use samples instead.
	 * @param {Number} value - The sample count.
	 */

	setSamples(value) {

		this.samples = value;

	}

	/**
	 * The sampling spiral ring count.
	 *
	 * @type {Number}
	 */

	get rings() {

		return Number(this.defines.SPIRAL_TURNS);

	}

	set rings(value) {

		this.defines.SPIRAL_TURNS = value.toFixed(1);
		this.needsUpdate = true;

	}

	/**
	 * Returns the amount of spiral turns in the occlusion sampling pattern.
	 *
	 * @deprecated Use rings instead.
	 * @return {Number} The radius.
	 */

	getRings() {

		return this.rings;

	}

	/**
	 * Sets the amount of spiral turns in the occlusion sampling pattern.
	 *
	 * @deprecated Use rings instead.
	 * @param {Number} value - The radius.
	 */

	setRings(value) {

		this.rings = value;

	}

	/**
	 * The intensity.
	 *
	 * @type {Number}
	 * @deprecated Use SSAOEffect.intensity instead.
	 */

	get intensity() {

		return this.uniforms.intensity.value;

	}

	set intensity(value) {

		this.uniforms.intensity.value = value;

		if(this.defines.LEGACY_INTENSITY === undefined) {

			this.defines.LEGACY_INTENSITY = "1";
			this.needsUpdate = true;

		}

	}

	/**
	 * Returns the intensity.
	 *
	 * @deprecated Use SSAOEffect.intensity instead.
	 * @return {Number} The intensity.
	 */

	getIntensity() {

		return this.uniforms.intensity.value;

	}

	/**
	 * Sets the intensity.
	 *
	 * @deprecated Use SSAOEffect.intensity instead.
	 * @param {Number} value - The intensity.
	 */

	setIntensity(value) {

		this.uniforms.intensity.value = value;

	}

	/**
	 * The depth fade factor.
	 *
	 * @type {Number}
	 */

	get fade() {

		return this.uniforms.fade.value;

	}

	set fade(value) {

		this.uniforms.fade.value = value;

	}

	/**
	 * Returns the depth fade factor.
	 *
	 * @deprecated Use fade instead.
	 * @return {Number} The fade factor.
	 */

	getFade() {

		return this.uniforms.fade.value;

	}

	/**
	 * Sets the depth fade factor.
	 *
	 * @deprecated Use fade instead.
	 * @param {Number} value - The fade factor.
	 */

	setFade(value) {

		this.uniforms.fade.value = value;

	}

	/**
	 * The depth bias. Range: [0.0, 1.0].
	 *
	 * @type {Number}
	 */

	get bias() {

		return this.uniforms.bias.value;

	}

	set bias(value) {

		this.uniforms.bias.value = value;

	}

	/**
	 * Returns the depth bias.
	 *
	 * @deprecated Use bias instead.
	 * @return {Number} The bias.
	 */

	getBias() {

		return this.uniforms.bias.value;

	}

	/**
	 * Sets the depth bias.
	 *
	 * @deprecated Use bias instead.
	 * @param {Number} value - The bias.
	 */

	setBias(value) {

		this.uniforms.bias.value = value;

	}

	/**
	 * The minimum radius scale for distance scaling. Range: [0.0, 1.0].
	 *
	 * @type {Number}
	 */

	get minRadiusScale() {

		return this.uniforms.minRadiusScale.value;

	}

	set minRadiusScale(value) {

		this.uniforms.minRadiusScale.value = value;

	}

	/**
	 * Returns the minimum radius scale for distance scaling.
	 *
	 * @deprecated Use minRadiusScale instead.
	 * @return {Number} The minimum radius scale.
	 */

	getMinRadiusScale() {

		return this.uniforms.minRadiusScale.value;

	}

	/**
	 * Sets the minimum radius scale for distance scaling.
	 *
	 * @deprecated Use minRadiusScale instead.
	 * @param {Number} value - The minimum radius scale.
	 */

	setMinRadiusScale(value) {

		this.uniforms.minRadiusScale.value = value;

	}

	/**
	 * Updates the absolute radius.
	 *
	 * @private
	 */

	updateRadius() {

		const radius = this.r * this.resolution.height;
		this.defines.RADIUS = radius.toFixed(11);
		this.defines.RADIUS_SQ = (radius * radius).toFixed(11);
		this.needsUpdate = true;

	}

	/**
	 * The occlusion sampling radius. Range: [0.0, 1.0].
	 *
	 * @type {Number}
	 */

	get radius() {

		return this.r;

	}

	set radius(value) {

		this.r = Math.min(Math.max(value, 1e-6), 1.0);
		this.updateRadius();

	}

	/**
	 * Returns the occlusion sampling radius.
	 *
	 * @deprecated Use radius instead.
	 * @return {Number} The radius.
	 */

	getRadius() {

		return this.radius;

	}

	/**
	 * Sets the occlusion sampling radius.
	 *
	 * @deprecated Use radius instead.
	 * @param {Number} value - The radius. Range [1e-6, 1.0].
	 */

	setRadius(value) {

		this.radius = value;

	}

	/**
	 * Indicates whether distance-based radius scaling is enabled.
	 *
	 * @type {Boolean}
	 * @deprecated
	 */

	get distanceScaling() { return true; }
	set distanceScaling(value) {}

	/**
	 * Indicates whether distance-based radius scaling is enabled.
	 *
	 * @deprecated
	 * @return {Boolean} Whether distance scaling is enabled.
	 */

	isDistanceScalingEnabled() {

		return this.distanceScaling;

	}

	/**
	 * Enables or disables distance-based radius scaling.
	 *
	 * @deprecated
	 * @param {Boolean} value - Whether distance scaling should be enabled.
	 */

	setDistanceScalingEnabled(value) {

		this.distanceScaling = value;

	}

	/**
	 * The occlusion distance threshold. Range: [0.0, 1.0].
	 *
	 * @type {Number}
	 */

	get distanceThreshold() {

		return this.uniforms.distanceCutoff.value.x;

	}

	set distanceThreshold(value) {

		this.uniforms.distanceCutoff.value.set(
			Math.min(Math.max(value, 0.0), 1.0),
			Math.min(Math.max(value + this.distanceFalloff, 0.0), 1.0)
		);

	}

	/**
	 * The occlusion distance threshold in world units.
	 *
	 * @type {Number}
	 */

	get worldDistanceThreshold() {

		return -orthographicDepthToViewZ(this.distanceThreshold, this.near, this.far);

	}

	set worldDistanceThreshold(value) {

		this.distanceThreshold = viewZToOrthographicDepth(-value, this.near, this.far);

	}

	/**
	 * The occlusion distance falloff. Range: [0.0, 1.0].
	 *
	 * @type {Number}
	 */

	get distanceFalloff() {

		return this.uniforms.distanceCutoff.value.y - this.distanceThreshold;

	}

	set distanceFalloff(value) {

		this.uniforms.distanceCutoff.value.y = Math.min(Math.max(this.distanceThreshold + value, 0.0), 1.0);

	}

	/**
	 * The occlusion distance falloff in world units.
	 *
	 * @type {Number}
	 */

	get worldDistanceFalloff() {

		return -orthographicDepthToViewZ(this.distanceFalloff, this.near, this.far);

	}

	set worldDistanceFalloff(value) {

		this.distanceFalloff = viewZToOrthographicDepth(-value, this.near, this.far);

	}

	/**
	 * Sets the occlusion distance cutoff.
	 *
	 * @deprecated Use distanceThreshold and distanceFalloff instead.
	 * @param {Number} threshold - The distance threshold. Range [0.0, 1.0].
	 * @param {Number} falloff - The falloff. Range [0.0, 1.0].
	 */

	setDistanceCutoff(threshold, falloff) {

		this.uniforms.distanceCutoff.value.set(
			Math.min(Math.max(threshold, 0.0), 1.0),
			Math.min(Math.max(threshold + falloff, 0.0), 1.0)
		);

	}

	/**
	 * The occlusion proximity threshold. Range: [0.0, 1.0].
	 *
	 * @type {Number}
	 */

	get proximityThreshold() {

		return this.uniforms.proximityCutoff.value.x;

	}

	set proximityThreshold(value) {

		this.uniforms.proximityCutoff.value.set(
			Math.min(Math.max(value, 0.0), 1.0),
			Math.min(Math.max(value + this.proximityFalloff, 0.0), 1.0)
		);

	}

	/**
	 * The occlusion proximity threshold in world units.
	 *
	 * @type {Number}
	 */

	get worldProximityThreshold() {

		return -orthographicDepthToViewZ(this.proximityThreshold, this.near, this.far);

	}

	set worldProximityThreshold(value) {

		this.proximityThreshold = viewZToOrthographicDepth(-value, this.near, this.far);

	}

	/**
	 * The occlusion proximity falloff. Range: [0.0, 1.0].
	 *
	 * @type {Number}
	 */

	get proximityFalloff() {

		return this.uniforms.proximityCutoff.value.y - this.proximityThreshold;

	}

	set proximityFalloff(value) {

		this.uniforms.proximityCutoff.value.y = Math.min(Math.max(this.proximityThreshold + value, 0.0), 1.0);

	}

	/**
	 * The occlusion proximity falloff in world units.
	 *
	 * @type {Number}
	 */

	get worldProximityFalloff() {

		return -orthographicDepthToViewZ(this.proximityFalloff, this.near, this.far);

	}

	set worldProximityFalloff(value) {

		this.proximityFalloff = viewZToOrthographicDepth(-value, this.near, this.far);

	}

	/**
	 * Sets the occlusion proximity cutoff.
	 *
	 * @deprecated Use proximityThreshold and proximityFalloff instead.
	 * @param {Number} threshold - The range threshold. Range [0.0, 1.0].
	 * @param {Number} falloff - The falloff. Range [0.0, 1.0].
	 */

	setProximityCutoff(threshold, falloff) {

		this.uniforms.proximityCutoff.value.set(
			Math.min(Math.max(threshold, 0.0), 1.0),
			Math.min(Math.max(threshold + falloff, 0.0), 1.0)
		);

	}

	/**
	 * Sets the texel size.
	 *
	 * @deprecated Use setSize() instead.
	 * @param {Number} x - The texel width.
	 * @param {Number} y - The texel height.
	 */

	setTexelSize(x, y) {

		this.uniforms.texelSize.value.set(x, y);

	}

	/**
	 * Copies the settings of the given camera.
	 *
	 * @deprecated Use copyCameraSettings instead.
	 * @param {Camera} camera - A camera.
	 */

	adoptCameraSettings(camera) {

		this.copyCameraSettings(camera);

	}

	/**
	 * Copies the settings of the given camera.
	 *
	 * @param {Camera} camera - A camera.
	 */

	copyCameraSettings(camera) {

		if(camera) {

			this.uniforms.cameraNearFar.value.set(camera.near, camera.far);
			this.uniforms.projectionMatrix.value.copy(camera.projectionMatrix);
			this.uniforms.inverseProjectionMatrix.value.copy(camera.projectionMatrix).invert();

			if(camera instanceof PerspectiveCamera) {

				this.defines.PERSPECTIVE_CAMERA = "1";

			} else {

				delete this.defines.PERSPECTIVE_CAMERA;

			}

			this.needsUpdate = true;

		}

	}

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

	setSize(width, height) {

		const uniforms = this.uniforms;
		const noiseTexture = uniforms.noiseTexture.value;

		if(noiseTexture !== null) {

			uniforms.noiseScale.value.set(
				width / noiseTexture.image.width,
				height / noiseTexture.image.height
			);

		}

		uniforms.texelSize.value.set(1.0 / width, 1.0 / height);
		this.resolution.set(width, height);
		this.updateRadius();

	}

}