Home Reference Source

src/effects/GlitchEffect.js

import { NearestFilter, RepeatWrapping, RGBAFormat, Uniform, Vector2 } from "three";
import { GlitchMode } from "../enums/GlitchMode.js";
import { NoiseTexture } from "../textures/NoiseTexture.js";
import { Effect } from "./Effect.js";

import fragmentShader from "./glsl/glitch.frag";

const textureTag = "Glitch.Generated";

/**
 * Returns a random float in the specified range.
 *
 * @private
 * @param {Number} low - The lowest possible value.
 * @param {Number} high - The highest possible value.
 * @return {Number} The random value.
 */

function randomFloat(low, high) {

	return low + Math.random() * (high - low);

}

/**
 * A glitch effect.
 *
 * This effect can be used in conjunction with the {@link ChromaticAberrationEffect}.
 *
 * Reference: https://github.com/staffantan/unityglitch
 */

export class GlitchEffect extends Effect {

	/**
	 * Constructs a new glitch effect.
	 *
	 * TODO Change ratio to 0.15.
	 * @param {Object} [options] - The options.
	 * @param {Vector2} [options.chromaticAberrationOffset] - A chromatic aberration offset. If provided, the glitch effect will influence this offset.
	 * @param {Vector2} [options.delay] - The minimum and maximum delay between glitch activations in seconds.
	 * @param {Vector2} [options.duration] - The minimum and maximum duration of a glitch in seconds.
	 * @param {Vector2} [options.strength] - The strength of weak and strong glitches.
	 * @param {Texture} [options.perturbationMap] - A perturbation map. If none is provided, a noise texture will be created.
	 * @param {Number} [options.dtSize=64] - The size of the generated noise map. Will be ignored if a perturbation map is provided.
	 * @param {Number} [options.columns=0.05] - The scale of the blocky glitch columns.
	 * @param {Number} [options.ratio=0.85] - The threshold for strong glitches.
	 */

	constructor({
		chromaticAberrationOffset = null,
		delay = new Vector2(1.5, 3.5),
		duration = new Vector2(0.6, 1.0),
		strength = new Vector2(0.3, 1.0),
		columns = 0.05,
		ratio = 0.85,
		perturbationMap = null,
		dtSize = 64
	} = {}) {

		super("GlitchEffect", fragmentShader, {
			uniforms: new Map([
				["perturbationMap", new Uniform(null)],
				["columns", new Uniform(columns)],
				["active", new Uniform(false)],
				["random", new Uniform(1.0)],
				["seeds", new Uniform(new Vector2())],
				["distortion", new Uniform(new Vector2())]
			])
		});

		if(perturbationMap === null) {

			const map = new NoiseTexture(dtSize, dtSize, RGBAFormat);
			map.name = textureTag;
			this.perturbationMap = map;

		} else {

			this.perturbationMap = perturbationMap;

		}

		/**
		 * A time accumulator.
		 *
		 * @type {Number}
		 * @private
		 */

		this.time = 0;

		/**
		 * A shortcut to the distortion vector.
		 *
		 * @type {Vector2}
		 * @private
		 */

		this.distortion = this.uniforms.get("distortion").value;

		/**
		 * The minimum and maximum delay between glitch activations in seconds.
		 *
		 * @type {Vector2}
		 * @deprecated Use minDelay and maxDelay instead.
		 */

		this.delay = delay;

		/**
		 * The minimum and maximum duration of a glitch in seconds.
		 *
		 * @type {Vector2}
		 * @deprecated Use minDuration and maxDuration instead.
		 */

		this.duration = duration;

		/**
		 * A random glitch break point.
		 *
		 * @type {Number}
		 * @private
		 */

		this.breakPoint = new Vector2(
			randomFloat(this.delay.x, this.delay.y),
			randomFloat(this.duration.x, this.duration.y)
		);

		/**
		 * The strength of weak and strong glitches.
		 *
		 * @type {Vector2}
		 * @deprecated Use minStrength and maxStrength instead.
		 */

		this.strength = strength;

		/**
		 * The effect mode.
		 *
		 * @type {GlitchMode}
		 */

		this.mode = GlitchMode.SPORADIC;

		/**
		 * The ratio between weak (0.0) and strong (1.0) glitches. Range is [0.0, 1.0].
		 *
		 * This value is currently being treated as a threshold for strong glitches, i.e. it's inverted.
		 *
		 * TODO Resolve inversion.
		 * @type {Number}
		 */

		this.ratio = ratio;

		/**
		 * The chromatic aberration offset.
		 *
		 * @type {Vector2}
		 */

		this.chromaticAberrationOffset = chromaticAberrationOffset;

	}

	/**
	 * Random number seeds.
	 *
	 * @type {Vector2}
	 * @private
	 */

	get seeds() {

		return this.uniforms.get("seeds").value;

	}

	/**
	 * Indicates whether the glitch effect is currently active.
	 *
	 * @type {Boolean}
	 */

	get active() {

		return this.uniforms.get("active").value;

	}

	/**
	 * Indicates whether the glitch effect is currently active.
	 *
	 * @deprecated Use active instead.
	 * @return {Boolean} Whether the glitch effect is active.
	 */

	isActive() {

		return this.active;

	}

	/**
	 * The minimum delay between glitch activations.
	 *
	 * @type {Number}
	 */

	get minDelay() {

		return this.delay.x;

	}

	set minDelay(value) {

		this.delay.x = value;

	}

	/**
	 * Returns the minimum delay between glitch activations.
	 *
	 * @deprecated Use minDelay instead.
	 * @return {Number} The minimum delay in seconds.
	 */

	getMinDelay() {

		return this.delay.x;

	}

	/**
	 * Sets the minimum delay between glitch activations.
	 *
	 * @deprecated Use minDelay instead.
	 * @param {Number} value - The minimum delay in seconds.
	 */

	setMinDelay(value) {

		this.delay.x = value;

	}

	/**
	 * The maximum delay between glitch activations.
	 *
	 * @type {Number}
	 */

	get maxDelay() {

		return this.delay.y;

	}

	set maxDelay(value) {

		this.delay.y = value;

	}

	/**
	 * Returns the maximum delay between glitch activations.
	 *
	 * @deprecated Use maxDelay instead.
	 * @return {Number} The maximum delay in seconds.
	 */

	getMaxDelay() {

		return this.delay.y;

	}

	/**
	 * Sets the maximum delay between glitch activations.
	 *
	 * @deprecated Use maxDelay instead.
	 * @param {Number} value - The maximum delay in seconds.
	 */

	setMaxDelay(value) {

		this.delay.y = value;

	}

	/**
	 * The minimum duration of sporadic glitches.
	 *
	 * @type {Number}
	 */

	get minDuration() {

		return this.duration.x;

	}

	set minDuration(value) {

		this.duration.x = value;

	}

	/**
	 * Returns the minimum duration of sporadic glitches.
	 *
	 * @deprecated Use minDuration instead.
	 * @return {Number} The minimum duration in seconds.
	 */

	getMinDuration() {

		return this.duration.x;

	}

	/**
	 * Sets the minimum duration of sporadic glitches.
	 *
	 * @deprecated Use minDuration instead.
	 * @param {Number} value - The minimum duration in seconds.
	 */

	setMinDuration(value) {

		this.duration.x = value;

	}

	/**
	 * The maximum duration of sporadic glitches.
	 *
	 * @type {Number}
	 */

	get maxDuration() {

		return this.duration.y;

	}

	set maxDuration(value) {

		this.duration.y = value;

	}

	/**
	 * Returns the maximum duration of sporadic glitches.
	 *
	 * @deprecated Use maxDuration instead.
	 * @return {Number} The maximum duration in seconds.
	 */

	getMaxDuration() {

		return this.duration.y;

	}

	/**
	 * Sets the maximum duration of sporadic glitches.
	 *
	 * @deprecated Use maxDuration instead.
	 * @param {Number} value - The maximum duration in seconds.
	 */

	setMaxDuration(value) {

		this.duration.y = value;

	}

	/**
	 * The strength of weak glitches.
	 *
	 * @type {Number}
	 */

	get minStrength() {

		return this.strength.x;

	}

	set minStrength(value) {

		this.strength.x = value;

	}

	/**
	 * Returns the strength of weak glitches.
	 *
	 * @deprecated Use minStrength instead.
	 * @return {Number} The strength.
	 */

	getMinStrength() {

		return this.strength.x;

	}

	/**
	 * Sets the strength of weak glitches.
	 *
	 * @deprecated Use minStrength instead.
	 * @param {Number} value - The strength.
	 */

	setMinStrength(value) {

		this.strength.x = value;

	}

	/**
	 * The strength of strong glitches.
	 *
	 * @type {Number}
	 */

	get maxStrength() {

		return this.strength.y;

	}

	set maxStrength(value) {

		this.strength.y = value;

	}

	/**
	 * Returns the strength of strong glitches.
	 *
	 * @deprecated Use maxStrength instead.
	 * @return {Number} The strength.
	 */

	getMaxStrength() {

		return this.strength.y;

	}

	/**
	 * Sets the strength of strong glitches.
	 *
	 * @deprecated Use maxStrength instead.
	 * @param {Number} value - The strength.
	 */

	setMaxStrength(value) {

		this.strength.y = value;

	}

	/**
	 * Returns the current glitch mode.
	 *
	 * @deprecated Use mode instead.
	 * @return {GlitchMode} The mode.
	 */

	getMode() {

		return this.mode;

	}

	/**
	 * Sets the current glitch mode.
	 *
	 * @deprecated Use mode instead.
	 * @param {GlitchMode} value - The mode.
	 */

	setMode(value) {

		this.mode = value;

	}

	/**
	 * Returns the glitch ratio.
	 *
	 * @deprecated Use ratio instead.
	 * @return {Number} The ratio.
	 */

	getGlitchRatio() {

		return (1.0 - this.ratio);

	}

	/**
	 * Sets the ratio of weak (0.0) and strong (1.0) glitches.
	 *
	 * @deprecated Use ratio instead.
	 * @param {Number} value - The ratio. Range is [0.0, 1.0].
	 */

	setGlitchRatio(value) {

		this.ratio = Math.min(Math.max(1.0 - value, 0.0), 1.0);

	}

	/**
	 * The glitch column size.
	 *
	 * @type {Number}
	 */

	get columns() {

		return this.uniforms.get("columns").value;

	}

	set columns(value) {

		this.uniforms.get("columns").value = value;

	}

	/**
	 * Returns the glitch column size.
	 *
	 * @deprecated Use columns instead.
	 * @return {Number} The glitch column size.
	 */

	getGlitchColumns() {

		return this.columns;

	}

	/**
	 * Sets the glitch column size.
	 *
	 * @deprecated Use columns instead.
	 * @param {Number} value - The glitch column size.
	 */

	setGlitchColumns(value) {

		this.columns = value;

	}

	/**
	 * Returns the chromatic aberration offset.
	 *
	 * @deprecated Use chromaticAberrationOffset instead.
	 * @return {Vector2} The offset.
	 */

	getChromaticAberrationOffset() {

		return this.chromaticAberrationOffset;

	}

	/**
	 * Sets the chromatic aberration offset.
	 *
	 * @deprecated Use chromaticAberrationOffset instead.
	 * @param {Vector2} value - The offset.
	 */

	setChromaticAberrationOffset(value) {

		this.chromaticAberrationOffset = value;

	}

	/**
	 * The perturbation map.
	 *
	 * @type {Texture}
	 */

	get perturbationMap() {

		return this.uniforms.get("perturbationMap").value;

	}

	set perturbationMap(value) {

		const currentMap = this.perturbationMap;

		if(currentMap !== null && currentMap.name === textureTag) {

			currentMap.dispose();

		}

		value.minFilter = value.magFilter = NearestFilter;
		value.wrapS = value.wrapT = RepeatWrapping;
		value.generateMipmaps = false;

		this.uniforms.get("perturbationMap").value = value;

	}

	/**
	 * Returns the current perturbation map.
	 *
	 * @deprecated Use perturbationMap instead.
	 * @return {Texture} The current perturbation map.
	 */

	getPerturbationMap() {

		return this.perturbationMap;

	}

	/**
	 * Replaces the current perturbation map with the given one.
	 *
	 * The current map will be disposed if it was generated by this effect.
	 *
	 * @deprecated Use perturbationMap instead.
	 * @param {Texture} value - The new perturbation map.
	 */

	setPerturbationMap(value) {

		this.perturbationMap = value;

	}

	/**
	 * Generates a perturbation map.
	 *
	 * @deprecated Use NoiseTexture instead.
	 * @param {Number} [value=64] - The texture size.
	 * @return {DataTexture} The perturbation map.
	 */

	generatePerturbationMap(value = 64) {

		const map = new NoiseTexture(value, value, RGBAFormat);
		map.name = textureTag;
		return map;

	}

	/**
	 * 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 mode = this.mode;
		const breakPoint = this.breakPoint;
		const offset = this.chromaticAberrationOffset;
		const s = this.strength;

		let time = this.time;
		let active = false;
		let r = 0.0, a = 0.0;
		let trigger;

		if(mode !== GlitchMode.DISABLED) {

			if(mode === GlitchMode.SPORADIC) {

				time += deltaTime;
				trigger = (time > breakPoint.x);

				if(time >= (breakPoint.x + breakPoint.y)) {

					breakPoint.set(
						randomFloat(this.delay.x, this.delay.y),
						randomFloat(this.duration.x, this.duration.y)
					);

					time = 0;

				}

			}

			r = Math.random();
			this.uniforms.get("random").value = r;

			// TODO change > to <.
			if((trigger && r > this.ratio) || mode === GlitchMode.CONSTANT_WILD) {

				active = true;

				r *= s.y * 0.03;
				a = randomFloat(-Math.PI, Math.PI);

				this.seeds.set(randomFloat(-s.y, s.y), randomFloat(-s.y, s.y));
				this.distortion.set(randomFloat(0.0, 1.0), randomFloat(0.0, 1.0));

			} else if(trigger || mode === GlitchMode.CONSTANT_MILD) {

				active = true;

				r *= s.x * 0.03;
				a = randomFloat(-Math.PI, Math.PI);

				this.seeds.set(randomFloat(-s.x, s.x), randomFloat(-s.x, s.x));
				this.distortion.set(randomFloat(0.0, 1.0), randomFloat(0.0, 1.0));

			}

			this.time = time;

		}

		if(offset !== null) {

			if(active) {

				offset.set(Math.cos(a), Math.sin(a)).multiplyScalar(r);

			} else {

				offset.set(0.0, 0.0);

			}

		}

		this.uniforms.get("active").value = active;

	}

	/**
	 * Deletes generated resources.
	 */

	dispose() {

		const map = this.perturbationMap;

		if(map !== null && map.name === textureTag) {

			map.dispose();

		}

	}

}