src/effects/SSAOEffect.js
import { BasicDepthPacking, Color, RepeatWrapping, RGBAFormat, Uniform, WebGLRenderTarget } from "three";
import { Resolution } from "../core/Resolution.js";
import { BlendFunction } from "../enums/BlendFunction.js";
import { EffectAttribute } from "../enums/EffectAttribute.js";
import { NoiseTexture } from "../textures/NoiseTexture.js";
import { SSAOMaterial } from "../materials/SSAOMaterial.js";
import { DepthDownsamplingPass } from "../passes/DepthDownsamplingPass.js";
import { ShaderPass } from "../passes/ShaderPass.js";
import { Effect } from "./Effect.js";
import fragmentShader from "./glsl/ssao.frag";
const NOISE_TEXTURE_SIZE = 64;
/**
* A Screen Space Ambient Occlusion effect.
*
* Based on "Scalable Ambient Obscurance" by Morgan McGuire et al.
* and "Depth-aware upsampling experiments" by Eleni Maria Stea:
* https://research.nvidia.com/publication/scalable-ambient-obscurance
* https://eleni.mutantstargoat.com/hikiko/on-depth-aware-upsampling
*
* The view position calculation is based on a shader by Norbert Nopper:
* https://github.com/McNopper/OpenGL/blob/master/Example28/shader/ssao.frag.glsl
*/
export class SSAOEffect extends Effect {
/**
* Constructs a new SSAO effect.
*
* @todo Move normalBuffer to options.
* @param {Camera} [camera] - The main camera.
* @param {Texture} [normalBuffer] - A texture that contains the scene normals.
* @param {Object} [options] - The options.
* @param {BlendFunction} [options.blendFunction=BlendFunction.MULTIPLY] - The blend function of this effect.
* @param {Boolean} [options.distanceScaling=true] - Deprecated.
* @param {Boolean} [options.depthAwareUpsampling=true] - Enables or disables depth-aware upsampling. Has no effect if WebGL 2 is not supported.
* @param {Texture} [options.normalDepthBuffer=null] - Deprecated.
* @param {Number} [options.samples=9] - The amount of samples per pixel. Should not be a multiple of the ring count.
* @param {Number} [options.rings=7] - The amount of spiral turns in the occlusion sampling pattern. Should be a prime number.
* @param {Number} [options.worldDistanceThreshold] - The world distance threshold at which the occlusion effect starts to fade out.
* @param {Number} [options.worldDistanceFalloff] - The world distance falloff. Influences the smoothness of the occlusion cutoff.
* @param {Number} [options.worldProximityThreshold] - The world proximity threshold at which the occlusion starts to fade out.
* @param {Number} [options.worldProximityFalloff] - The world proximity falloff. Influences the smoothness of the proximity cutoff.
* @param {Number} [options.distanceThreshold=0.97] - Deprecated.
* @param {Number} [options.distanceFalloff=0.03] - Deprecated.
* @param {Number} [options.rangeThreshold=0.0005] - Deprecated.
* @param {Number} [options.rangeFalloff=0.001] - Deprecated.
* @param {Number} [options.minRadiusScale=0.1] - The minimum radius scale.
* @param {Number} [options.luminanceInfluence=0.7] - Determines how much the luminance of the scene influences the ambient occlusion.
* @param {Number} [options.radius=0.1825] - The occlusion sampling radius, expressed as a scale relative to the resolution. Range [1e-6, 1.0].
* @param {Number} [options.intensity=1.0] - The intensity of the ambient occlusion.
* @param {Number} [options.bias=0.025] - An occlusion bias. Eliminates artifacts caused by depth discontinuities.
* @param {Number} [options.fade=0.01] - Influences the smoothness of the shadows. A lower value results in higher contrast.
* @param {Color} [options.color=null] - The color of the ambient occlusion.
* @param {Number} [options.resolutionScale=1.0] - The resolution scale.
* @param {Number} [options.resolutionX=Resolution.AUTO_SIZE] - The horizontal resolution.
* @param {Number} [options.resolutionY=Resolution.AUTO_SIZE] - The vertical resolution.
* @param {Number} [options.width=Resolution.AUTO_SIZE] - Deprecated. Use resolutionX instead.
* @param {Number} [options.height=Resolution.AUTO_SIZE] - Deprecated. Use resolutionY instead.
*/
constructor(camera, normalBuffer, {
blendFunction = BlendFunction.MULTIPLY,
samples = 9,
rings = 7,
normalDepthBuffer = null,
depthAwareUpsampling = true,
worldDistanceThreshold,
worldDistanceFalloff,
worldProximityThreshold,
worldProximityFalloff,
distanceThreshold = 0.97,
distanceFalloff = 0.03,
rangeThreshold = 0.0005,
rangeFalloff = 0.001,
minRadiusScale = 0.1,
luminanceInfluence = 0.7,
radius = 0.1825,
intensity = 1.0,
bias = 0.025,
fade = 0.01,
color = null,
resolutionScale = 1.0,
width = Resolution.AUTO_SIZE,
height = Resolution.AUTO_SIZE,
resolutionX = width,
resolutionY = height
} = {}) {
super("SSAOEffect", fragmentShader, {
blendFunction,
attributes: EffectAttribute.DEPTH,
defines: new Map([
["THRESHOLD", "0.997"]
]),
uniforms: new Map([
["aoBuffer", new Uniform(null)],
["normalDepthBuffer", new Uniform(normalDepthBuffer)],
["luminanceInfluence", new Uniform(luminanceInfluence)],
["color", new Uniform(null)],
["intensity", new Uniform(intensity)],
["scale", new Uniform(0.0)] // Unused.
])
});
/**
* A render target.
*
* @type {WebGLRenderTarget}
* @private
*/
this.renderTarget = new WebGLRenderTarget(1, 1, { depthBuffer: false });
this.renderTarget.texture.name = "AO.Target";
this.uniforms.get("aoBuffer").value = this.renderTarget.texture;
/**
* The resolution.
*
* @type {Resolution}
*/
const resolution = this.resolution = new Resolution(this, resolutionX, resolutionY, resolutionScale);
resolution.addEventListener("change", (e) => this.setSize(resolution.baseWidth, resolution.baseHeight));
/**
* The main camera.
*
* @type {Camera}
* @private
*/
this.camera = camera;
/**
* A depth downsampling pass.
*
* @type {DepthDownsamplingPass}
* @private
*/
this.depthDownsamplingPass = new DepthDownsamplingPass({ normalBuffer, resolutionScale });
this.depthDownsamplingPass.enabled = (normalDepthBuffer === null);
/**
* An SSAO pass.
*
* @type {ShaderPass}
* @private
*/
this.ssaoPass = new ShaderPass(new SSAOMaterial(camera));
const noiseTexture = new NoiseTexture(NOISE_TEXTURE_SIZE, NOISE_TEXTURE_SIZE, RGBAFormat);
noiseTexture.wrapS = noiseTexture.wrapT = RepeatWrapping;
const ssaoMaterial = this.ssaoMaterial;
ssaoMaterial.normalBuffer = normalBuffer;
ssaoMaterial.noiseTexture = noiseTexture;
ssaoMaterial.minRadiusScale = minRadiusScale;
ssaoMaterial.samples = samples;
ssaoMaterial.radius = radius;
ssaoMaterial.rings = rings;
ssaoMaterial.fade = fade;
ssaoMaterial.bias = bias;
ssaoMaterial.distanceThreshold = distanceThreshold;
ssaoMaterial.distanceFalloff = distanceFalloff;
ssaoMaterial.proximityThreshold = rangeThreshold;
ssaoMaterial.proximityFalloff = rangeFalloff;
if(worldDistanceThreshold !== undefined) {
ssaoMaterial.worldDistanceThreshold = worldDistanceThreshold;
}
if(worldDistanceFalloff !== undefined) {
ssaoMaterial.worldDistanceFalloff = worldDistanceFalloff;
}
if(worldProximityThreshold !== undefined) {
ssaoMaterial.worldProximityThreshold = worldProximityThreshold;
}
if(worldProximityFalloff !== undefined) {
ssaoMaterial.worldProximityFalloff = worldProximityFalloff;
}
if(normalDepthBuffer !== null) {
this.ssaoMaterial.normalDepthBuffer = normalDepthBuffer;
this.defines.set("NORMAL_DEPTH", "1");
}
this.depthAwareUpsampling = depthAwareUpsampling;
this.color = color;
}
set mainCamera(value) {
this.camera = value;
this.ssaoMaterial.copyCameraSettings(value);
}
/**
* Sets the normal buffer.
*
* @type {Texture}
*/
get normalBuffer() {
return this.ssaoMaterial.normalBuffer;
}
set normalBuffer(value) {
this.ssaoMaterial.normalBuffer = value;
this.depthDownsamplingPass.fullscreenMaterial.normalBuffer = value;
}
/**
* Returns the resolution settings.
*
* @deprecated Use resolution instead.
* @return {Resolution} The resolution.
*/
getResolution() {
return this.resolution;
}
/**
* The SSAO material.
*
* @type {SSAOMaterial}
*/
get ssaoMaterial() {
return this.ssaoPass.fullscreenMaterial;
}
/**
* Returns the SSAO material.
*
* @deprecated Use ssaoMaterial instead.
* @return {SSAOMaterial} The material.
*/
getSSAOMaterial() {
return this.ssaoMaterial;
}
/**
* The amount of occlusion samples per pixel.
*
* @type {Number}
* @deprecated Use ssaoMaterial.samples instead.
*/
get samples() {
return this.ssaoMaterial.samples;
}
set samples(value) {
this.ssaoMaterial.samples = value;
}
/**
* The amount of spiral turns in the occlusion sampling pattern.
*
* @type {Number}
* @deprecated Use ssaoMaterial.rings instead.
*/
get rings() {
return this.ssaoMaterial.rings;
}
set rings(value) {
this.ssaoMaterial.rings = value;
}
/**
* The occlusion sampling radius.
*
* @type {Number}
* @deprecated Use ssaoMaterial.radius instead.
*/
get radius() {
return this.ssaoMaterial.radius;
}
set radius(value) {
this.ssaoMaterial.radius = value;
}
/**
* Indicates whether depth-aware upsampling is enabled.
*
* @type {Boolean}
*/
get depthAwareUpsampling() {
return this.defines.has("DEPTH_AWARE_UPSAMPLING");
}
set depthAwareUpsampling(value) {
if(this.depthAwareUpsampling !== value) {
if(value) {
this.defines.set("DEPTH_AWARE_UPSAMPLING", "1");
} else {
this.defines.delete("DEPTH_AWARE_UPSAMPLING");
}
this.setChanged();
}
}
/**
* Indicates whether depth-aware upsampling is enabled.
*
* @deprecated Use depthAwareUpsampling instead.
* @return {Boolean} Whether depth-aware upsampling is enabled.
*/
isDepthAwareUpsamplingEnabled() {
return this.depthAwareUpsampling;
}
/**
* Enables or disables depth-aware upsampling.
*
* @deprecated Use depthAwareUpsampling instead.
* @param {Boolean} value - Whether depth-aware upsampling should be enabled.
*/
setDepthAwareUpsamplingEnabled(value) {
this.depthAwareUpsampling = value;
}
/**
* Indicates whether distance-based radius scaling is enabled.
*
* @type {Boolean}
* @deprecated
*/
get distanceScaling() { return true; }
set distanceScaling(value) {}
/**
* The color of the ambient occlusion. Set to `null` to disable.
*
* @type {Color}
*/
get color() {
return this.uniforms.get("color").value;
}
set color(value) {
const uniforms = this.uniforms;
const defines = this.defines;
if(value !== null) {
if(defines.has("COLORIZE")) {
uniforms.get("color").value.set(value);
} else {
defines.set("COLORIZE", "1");
uniforms.get("color").value = new Color(value);
this.setChanged();
}
} else if(defines.has("COLORIZE")) {
defines.delete("COLORIZE");
uniforms.get("color").value = null;
this.setChanged();
}
}
/**
* The luminance influence factor. Range: [0.0, 1.0].
*
* @type {Boolean}
*/
get luminanceInfluence() {
return this.uniforms.get("luminanceInfluence").value;
}
set luminanceInfluence(value) {
this.uniforms.get("luminanceInfluence").value = value;
}
/**
* The intensity.
*
* @type {Number}
*/
get intensity() {
return this.uniforms.get("intensity").value;
}
set intensity(value) {
this.uniforms.get("intensity").value = value;
}
/**
* Returns the color of the ambient occlusion.
*
* @deprecated Use color instead.
* @return {Color} The color.
*/
getColor() {
return this.color;
}
/**
* Sets the color of the ambient occlusion. Set to `null` to disable colorization.
*
* @deprecated Use color instead.
* @param {Color} value - The color.
*/
setColor(value) {
this.color = value;
}
/**
* Sets the occlusion distance cutoff.
*
* @deprecated Use ssaoMaterial 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.ssaoMaterial.distanceThreshold = threshold;
this.ssaoMaterial.distanceFalloff = falloff;
}
/**
* Sets the occlusion proximity cutoff.
*
* @deprecated Use ssaoMaterial instead.
* @param {Number} threshold - The proximity threshold. Range [0.0, 1.0].
* @param {Number} falloff - The falloff. Range [0.0, 1.0].
*/
setProximityCutoff(threshold, falloff) {
this.ssaoMaterial.proximityThreshold = threshold;
this.ssaoMaterial.proximityFalloff = falloff;
}
/**
* Sets the depth texture.
*
* @param {Texture} depthTexture - A depth texture.
* @param {DepthPackingStrategies} [depthPacking=BasicDepthPacking] - The depth packing.
*/
setDepthTexture(depthTexture, depthPacking = BasicDepthPacking) {
this.depthDownsamplingPass.setDepthTexture(depthTexture, depthPacking);
this.ssaoMaterial.depthBuffer = depthTexture;
this.ssaoMaterial.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) {
const renderTarget = this.renderTarget;
if(this.depthDownsamplingPass.enabled) {
this.depthDownsamplingPass.render(renderer);
}
this.ssaoPass.render(renderer, null, renderTarget);
}
/**
* Sets the size.
*
* @param {Number} width - The width.
* @param {Number} height - The height.
*/
setSize(width, height) {
const resolution = this.resolution;
resolution.setBaseSize(width, height);
const w = resolution.width, h = resolution.height;
this.ssaoMaterial.copyCameraSettings(this.camera);
this.ssaoMaterial.setSize(w, h);
this.renderTarget.setSize(w, h);
this.depthDownsamplingPass.resolution.scale = resolution.scale;
this.depthDownsamplingPass.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) {
try {
let normalDepthBuffer = this.uniforms.get("normalDepthBuffer").value;
if(normalDepthBuffer === null) {
this.depthDownsamplingPass.initialize(renderer, alpha, frameBufferType);
normalDepthBuffer = this.depthDownsamplingPass.texture;
this.uniforms.get("normalDepthBuffer").value = normalDepthBuffer;
this.ssaoMaterial.normalDepthBuffer = normalDepthBuffer;
this.defines.set("NORMAL_DEPTH", "1");
}
} catch(e) {
// Not supported.
this.depthDownsamplingPass.enabled = false;
}
}
}