Home Reference Source

src/effects/GodRaysEffect.js

import {
	BasicDepthPacking,
	Color,
	DepthTexture,
	Matrix4,
	Scene,
	SRGBColorSpace,
	Uniform,
	Vector2,
	Vector3,
	WebGLRenderTarget
} from "three";

import { Resolution } from "../core/Resolution.js";
import { BlendFunction } from "../enums/BlendFunction.js";
import { EffectAttribute } from "../enums/EffectAttribute.js";
import { KernelSize } from "../enums/KernelSize.js";
import { DepthMaskMaterial } from "../materials/DepthMaskMaterial.js";
import { GodRaysMaterial } from "../materials/GodRaysMaterial.js";
import { KawaseBlurPass } from "../passes/KawaseBlurPass.js";
import { ClearPass } from "../passes/ClearPass.js";
import { RenderPass } from "../passes/RenderPass.js";
import { ShaderPass } from "../passes/ShaderPass.js";
import { Effect } from "./Effect.js";

import fragmentShader from "./glsl/god-rays.frag";

const v = /* @__PURE__ */ new Vector3();
const m = /* @__PURE__ */ new Matrix4();

/**
 * A god rays effect.
 */

export class GodRaysEffect extends Effect {

	/**
	 * Constructs a new god rays effect.
	 *
	 * @param {Camera} [camera] - The main camera.
	 * @param {Mesh|Points} [lightSource] - The light source. Must not write depth and has to be flagged as transparent.
	 * @param {Object} [options] - The options.
	 * @param {BlendFunction} [options.blendFunction=BlendFunction.SCREEN] - The blend function of this effect.
	 * @param {Number} [options.samples=60.0] - The number of samples per pixel.
	 * @param {Number} [options.density=0.96] - The density of the light rays.
	 * @param {Number} [options.decay=0.9] - An illumination decay factor.
	 * @param {Number} [options.weight=0.4] - A light ray weight factor.
	 * @param {Number} [options.exposure=0.6] - A constant attenuation coefficient.
	 * @param {Number} [options.clampMax=1.0] - An upper bound for the saturation of the overall effect.
	 * @param {Number} [options.resolutionScale=0.5] - 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.
	 * @param {KernelSize} [options.kernelSize=KernelSize.SMALL] - The blur kernel size. Has no effect if blur is disabled.
	 * @param {Boolean} [options.blur=true] - Whether the god rays should be blurred to reduce artifacts.
	 */

	constructor(camera, lightSource, {
		blendFunction = BlendFunction.SCREEN,
		samples = 60.0,
		density = 0.96,
		decay = 0.9,
		weight = 0.4,
		exposure = 0.6,
		clampMax = 1.0,
		blur = true,
		kernelSize = KernelSize.SMALL,
		resolutionScale = 0.5,
		width = Resolution.AUTO_SIZE,
		height = Resolution.AUTO_SIZE,
		resolutionX = width,
		resolutionY = height
	} = {}) {

		super("GodRaysEffect", fragmentShader, {
			blendFunction,
			attributes: EffectAttribute.DEPTH,
			uniforms: new Map([
				["map", new Uniform(null)]
			])
		});

		/**
		 * The main camera.
		 *
		 * @type {Camera}
		 * @private
		 */

		this.camera = camera;

		/**
		 * The light source.
		 *
		 * @type {Mesh|Points}
		 * @private
		 */

		this._lightSource = lightSource;
		this.lightSource = lightSource;

		/**
		 * A scene for the light source.
		 *
		 * @type {Scene}
		 * @private
		 */

		this.lightScene = new Scene();

		/**
		 * The light position in screen space.
		 *
		 * @type {Vector2}
		 * @private
		 */

		this.screenPosition = new Vector2();

		/**
		 * A render target.
		 *
		 * @type {WebGLRenderTarget}
		 * @private
		 */

		this.renderTargetA = new WebGLRenderTarget(1, 1, { depthBuffer: false });
		this.renderTargetA.texture.name = "GodRays.Target.A";

		/**
		 * A render target.
		 *
		 * @type {WebGLRenderTarget}
		 * @private
		 */

		this.renderTargetB = this.renderTargetA.clone();
		this.renderTargetB.texture.name = "GodRays.Target.B";
		this.uniforms.get("map").value = this.renderTargetB.texture;

		/**
		 * A render target for the light scene.
		 *
		 * @type {WebGLRenderTarget}
		 * @private
		 */

		this.renderTargetLight = new WebGLRenderTarget(1, 1);
		this.renderTargetLight.texture.name = "GodRays.Light";
		this.renderTargetLight.depthTexture = new DepthTexture();

		/**
		 * A pass that renders the light source.
		 *
		 * @type {RenderPass}
		 * @private
		 */

		this.renderPassLight = new RenderPass(this.lightScene, camera);
		this.renderPassLight.clearPass.overrideClearColor = new Color(0x000000);

		/**
		 * A clear pass.
		 *
		 * @type {ClearPass}
		 * @private
		 */

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

		/**
		 * A blur pass that reduces aliasing artifacts to make the light softer.
		 *
		 * This pass can be disabled to improve performance.
		 *
		 * @type {KawaseBlurPass}
		 * @readonly
		 */

		this.blurPass = new KawaseBlurPass({ kernelSize });
		this.blurPass.enabled = blur;

		/**
		 * A depth mask pass.
		 *
		 * @type {ShaderPass}
		 * @private
		 */

		this.depthMaskPass = new ShaderPass(new DepthMaskMaterial());
		const depthMaskMaterial = this.depthMaskMaterial;
		depthMaskMaterial.depthBuffer1 = this.renderTargetLight.depthTexture;
		depthMaskMaterial.copyCameraSettings(camera);

		/**
		 * A god rays blur pass.
		 *
		 * @type {ShaderPass}
		 * @private
		 */

		this.godRaysPass = new ShaderPass(new GodRaysMaterial(this.screenPosition));
		const godRaysMaterial = this.godRaysMaterial;
		godRaysMaterial.density = density;
		godRaysMaterial.decay = decay;
		godRaysMaterial.weight = weight;
		godRaysMaterial.exposure = exposure;
		godRaysMaterial.maxIntensity = clampMax;
		godRaysMaterial.samples = samples;

		/**
		 * The render resolution.
		 *
		 * @type {Resolution}
		 * @readonly
		 */

		const resolution = this.resolution = new Resolution(this, resolutionX, resolutionY, resolutionScale);
		resolution.addEventListener("change", (e) => this.setSize(resolution.baseWidth, resolution.baseHeight));

	}

	set mainCamera(value) {

		this.camera = value;
		this.renderPassLight.mainCamera = value;
		this.depthMaskMaterial.copyCameraSettings(value);

	}

	/**
	 * Sets the light source.
	 *
	 * @type {Mesh|Points}
	 */

	get lightSource() {

		return this._lightSource;

	}

	set lightSource(value) {

		this._lightSource = value;

		if(value !== null) {

			value.material.depthWrite = false;
			value.material.transparent = true;

		}

	}

	/**
	 * Returns the blur pass that reduces aliasing artifacts and makes the light softer.
	 *
	 * @deprecated Use blurPass instead.
	 * @return {KawaseBlurPass} The blur pass.
	 */

	getBlurPass() {

		return this.blurPass;

	}

	/**
	 * A texture that contains the intermediate result of this effect.
	 *
	 * @type {Texture}
	 */

	get texture() {

		return this.renderTargetB.texture;

	}

	/**
	 * Returns the god rays texture.
	 *
	 * @deprecated Use texture instead.
	 * @return {Texture} The texture.
	 */

	getTexture() {

		return this.texture;

	}

	/**
	 * The depth mask material.
	 *
	 * @type {DepthMaskMaterial}
	 * @private
	 */

	get depthMaskMaterial() {

		return this.depthMaskPass.fullscreenMaterial;

	}

	/**
	 * The internal god rays material.
	 *
	 * @type {GodRaysMaterial}
	 */

	get godRaysMaterial() {

		return this.godRaysPass.fullscreenMaterial;

	}

	/**
	 * Returns the god rays material.
	 *
	 * @deprecated Use godRaysMaterial instead.
	 * @return {GodRaysMaterial} The material.
	 */

	getGodRaysMaterial() {

		return this.godRaysMaterial;

	}

	/**
	 * Returns the resolution of this effect.
	 *
	 * @deprecated Use resolution instead.
	 * @return {GodRaysMaterial} The material.
	 */

	getResolution() {

		return this.resolution;

	}

	/**
	 * The current width of the internal render targets.
	 *
	 * @type {Number}
	 * @deprecated Use resolution.width instead.
	 */

	get width() {

		return this.resolution.width;

	}

	set width(value) {

		this.resolution.preferredWidth = value;

	}

	/**
	 * The current height of the internal render targets.
	 *
	 * @type {Number}
	 * @deprecated Use resolution.height instead.
	 */

	get height() {

		return this.resolution.height;

	}

	set height(value) {

		this.resolution.preferredHeight = value;

	}

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

	get dithering() {

		return this.godRaysMaterial.dithering;

	}

	set dithering(value) {

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

	}

	/**
	 * Indicates whether the god rays should be blurred to reduce artifacts.
	 *
	 * @type {Boolean}
	 * @deprecated Use blurPass.enabled instead.
	 */

	get blur() {

		return this.blurPass.enabled;

	}

	set blur(value) {

		this.blurPass.enabled = value;

	}

	/**
	 * The blur kernel size.
	 *
	 * @type {KernelSize}
	 * @deprecated Use blurPass.kernelSize instead.
	 */

	get kernelSize() {

		return this.blurPass.kernelSize;

	}

	set kernelSize(value) {

		this.blurPass.kernelSize = value;

	}

	/**
	 * Returns the current resolution scale.
	 *
	 * @return {Number} The resolution scale.
	 * @deprecated Use resolution instead.
	 */

	getResolutionScale() {

		return this.resolution.scale;

	}

	/**
	 * Sets the resolution scale.
	 *
	 * @param {Number} scale - The new resolution scale.
	 * @deprecated Use resolution instead.
	 */

	setResolutionScale(scale) {

		this.resolution.scale = scale;

	}

	/**
	 * The number of samples per pixel.
	 *
	 * @type {Number}
	 * @deprecated Use godRaysMaterial.samples instead.
	 */

	get samples() {

		return this.godRaysMaterial.samples;

	}

	/**
	 * A higher sample count improves quality at the cost of performance.
	 *
	 * @type {Number}
	 * @deprecated Use godRaysMaterial.samples instead.
	 */

	set samples(value) {

		this.godRaysMaterial.samples = value;

	}

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

	setDepthTexture(depthTexture, depthPacking = BasicDepthPacking) {

		this.depthMaskPass.fullscreenMaterial.depthBuffer0 = depthTexture;
		this.depthMaskPass.fullscreenMaterial.depthPacking0 = 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 lightSource = this.lightSource;
		const parent = lightSource.parent;
		const matrixAutoUpdate = lightSource.matrixAutoUpdate;

		const renderTargetA = this.renderTargetA;
		const renderTargetLight = this.renderTargetLight;

		// Enable depth write for the light scene render pass.
		lightSource.material.depthWrite = true;

		// Update the world matrix.
		lightSource.matrixAutoUpdate = false;
		lightSource.updateWorldMatrix(true, false);

		if(parent !== null) {

			if(!matrixAutoUpdate) {

				// Remember the local transformation to restore it later.
				m.copy(lightSource.matrix);

			}

			// Apply parent transformations.
			lightSource.matrix.copy(lightSource.matrixWorld);

		}

		// Render the light source and mask it based on depth.
		this.lightScene.add(lightSource);
		this.renderPassLight.render(renderer, renderTargetLight);
		this.clearPass.render(renderer, renderTargetA);
		this.depthMaskPass.render(renderer, renderTargetLight, renderTargetA);

		// Restore the original values.
		lightSource.material.depthWrite = false;
		lightSource.matrixAutoUpdate = matrixAutoUpdate;

		if(parent !== null) {

			if(!matrixAutoUpdate) {

				lightSource.matrix.copy(m);

			}

			parent.add(lightSource);

		}

		// Calculate the screen light position.
		v.setFromMatrixPosition(lightSource.matrixWorld).project(this.camera);

		// Translate to [0.0, 1.0] and clamp to screen with a bias of 1.0.
		this.screenPosition.set(
			Math.min(Math.max((v.x + 1.0) * 0.5, -1.0), 2.0),
			Math.min(Math.max((v.y + 1.0) * 0.5, -1.0), 2.0)
		);

		if(this.blurPass.enabled) {

			// Blur the masked scene to reduce artifacts.
			this.blurPass.render(renderer, renderTargetA, renderTargetA);

		}

		// Blur the masked scene along radial lines towards the light source.
		this.godRaysPass.render(renderer, renderTargetA, this.renderTargetB);

	}

	/**
	 * Updates the size of internal render targets.
	 *
	 * @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.renderTargetA.setSize(w, h);
		this.renderTargetB.setSize(w, h);
		this.renderTargetLight.setSize(w, h);
		this.blurPass.resolution.copy(resolution);

	}

	/**
	 * 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.blurPass.initialize(renderer, alpha, frameBufferType);
		this.renderPassLight.initialize(renderer, alpha, frameBufferType);
		this.depthMaskPass.initialize(renderer, alpha, frameBufferType);
		this.godRaysPass.initialize(renderer, alpha, frameBufferType);

		if(frameBufferType !== undefined) {

			this.renderTargetA.texture.type = frameBufferType;
			this.renderTargetB.texture.type = frameBufferType;
			this.renderTargetLight.texture.type = frameBufferType;

			if(renderer !== null && renderer.outputColorSpace === SRGBColorSpace) {

				this.renderTargetA.texture.colorSpace = SRGBColorSpace;
				this.renderTargetB.texture.colorSpace = SRGBColorSpace;
				this.renderTargetLight.texture.colorSpace = SRGBColorSpace;

			}

		}

	}

}