Files

307 lines
12 KiB
JavaScript
Raw Permalink Normal View History

2025-01-04 00:34:03 +01:00
import RenderedEffectSource from "./rendered-effect-source.mjs";
import {LIGHTING_LEVELS} from "../../../common/constants.mjs";
/**
* @typedef {import("./base-effect-source.mjs").BaseEffectSourceData} BaseEffectSourceData
* @typedef {import("./rendered-effect-source-source.mjs").RenderedEffectSourceData} RenderedEffectSourceData
*/
/**
* @typedef {Object} LightSourceData
* @property {number} alpha An opacity for the emitted light, if any
* @property {number} bright The allowed radius of bright vision or illumination
* @property {number} coloration The coloration technique applied in the shader
* @property {number} contrast The amount of contrast this light applies to the background texture
* @property {number} dim The allowed radius of dim vision or illumination
* @property {number} attenuation Strength of the attenuation between bright, dim, and dark
* @property {number} luminosity The luminosity applied in the shader
* @property {number} saturation The amount of color saturation this light applies to the background texture
* @property {number} shadows The depth of shadows this light applies to the background texture
* @property {boolean} vision Whether or not this source provides a source of vision
* @property {number} priority Strength of this source to beat or not negative/positive sources
*/
/**
* A specialized subclass of BaseEffectSource which deals with the rendering of light or darkness.
* @extends {RenderedEffectSource<BaseEffectSourceData & RenderedEffectSourceData & LightSourceData>}
* @abstract
*/
export default class BaseLightSource extends RenderedEffectSource {
/** @override */
static sourceType = "light";
/** @override */
static _initializeShaderKeys = ["animation.type", "walls"];
/** @override */
static _refreshUniformsKeys = ["dim", "bright", "attenuation", "alpha", "coloration", "color", "contrast",
"saturation", "shadows", "luminosity"];
/**
* The corresponding lighting levels for dim light.
* @type {number}
* @protected
*/
static _dimLightingLevel = LIGHTING_LEVELS.DIM;
/**
* The corresponding lighting levels for bright light.
* @type {string}
* @protected
*/
static _brightLightingLevel = LIGHTING_LEVELS.BRIGHT;
/**
* The corresponding animation config.
* @type {LightSourceAnimationConfig}
* @protected
*/
static get ANIMATIONS() {
return CONFIG.Canvas.lightAnimations;
}
/** @inheritDoc */
static defaultData = {
...super.defaultData,
priority: 0,
alpha: 0.5,
bright: 0,
coloration: 1,
contrast: 0,
dim: 0,
attenuation: 0.5,
luminosity: 0.5,
saturation: 0,
shadows: 0,
vision: false
}
/* -------------------------------------------- */
/* Light Source Attributes */
/* -------------------------------------------- */
/**
* A ratio of dim:bright as part of the source radius
* @type {number}
*/
ratio = 1;
/* -------------------------------------------- */
/* Light Source Initialization */
/* -------------------------------------------- */
/** @override */
_initialize(data) {
super._initialize(data);
const animationConfig = foundry.utils.deepClone(this.constructor.ANIMATIONS[this.data.animation.type] || {});
this.animation = Object.assign(this.data.animation, animationConfig);
}
/* -------------------------------------------- */
/* Shader Management */
/* -------------------------------------------- */
/** @inheritDoc */
_updateColorationUniforms() {
super._updateColorationUniforms();
const u = this.layers.coloration.shader?.uniforms;
if ( !u ) return;
// Adapting color intensity to the coloration technique
switch ( this.data.coloration ) {
case 0: // Legacy
// Default 0.25 -> Legacy technique needs quite low intensity default to avoid washing background
u.colorationAlpha = Math.pow(this.data.alpha, 2);
break;
case 4: // Color burn
case 5: // Internal burn
case 6: // External burn
case 9: // Invert absorption
// Default 0.5 -> These techniques are better at low color intensity
u.colorationAlpha = this.data.alpha;
break;
default:
// Default 1 -> The remaining techniques use adaptive lighting,
// which produces interesting results in the [0, 2] range.
u.colorationAlpha = this.data.alpha * 2;
}
u.useSampler = this.data.coloration > 0; // Not needed for legacy coloration (technique id 0)
// Flag uniforms as updated
this.layers.coloration.reset = false;
}
/* -------------------------------------------- */
/** @inheritDoc */
_updateIlluminationUniforms() {
super._updateIlluminationUniforms();
const u = this.layers.illumination.shader?.uniforms;
if ( !u ) return;
u.useSampler = false;
// Flag uniforms as updated
const i = this.layers.illumination;
i.reset = i.suppressed = false;
}
/* -------------------------------------------- */
/** @inheritDoc */
_updateBackgroundUniforms() {
super._updateBackgroundUniforms();
const u = this.layers.background.shader?.uniforms;
if ( !u ) return;
canvas.colors.background.applyRGB(u.colorBackground);
u.backgroundAlpha = this.data.alpha;
u.useSampler = true;
// Flag uniforms as updated
this.layers.background.reset = false;
}
/* -------------------------------------------- */
/** @override */
_updateCommonUniforms(shader) {
const u = shader.uniforms;
const c = canvas.colors;
// Passing common environment values
u.computeIllumination = true;
u.darknessLevel = canvas.environment.darknessLevel;
c.ambientBrightest.applyRGB(u.ambientBrightest);
c.ambientDarkness.applyRGB(u.ambientDarkness);
c.ambientDaylight.applyRGB(u.ambientDaylight);
u.weights[0] = canvas.environment.weights.dark;
u.weights[1] = canvas.environment.weights.halfdark;
u.weights[2] = canvas.environment.weights.dim;
u.weights[3] = canvas.environment.weights.bright;
u.dimLevelCorrection = this.constructor.getCorrectedLevel(this.constructor._dimLightingLevel);
u.brightLevelCorrection = this.constructor.getCorrectedLevel(this.constructor._brightLightingLevel);
// Passing advanced color correction values
u.luminosity = this.data.luminosity;
u.exposure = this.data.luminosity * 2.0 - 1.0;
u.contrast = (this.data.contrast < 0 ? this.data.contrast * 0.5 : this.data.contrast);
u.saturation = this.data.saturation;
u.shadows = this.data.shadows;
u.hasColor = this._flags.hasColor;
u.ratio = this.ratio;
u.technique = this.data.coloration;
// Graph: https://www.desmos.com/calculator/e7z0i7hrck
// mapping [0,1] attenuation user value to [0,1] attenuation shader value
if ( this.cachedAttenuation !== this.data.attenuation ) {
this.computedAttenuation = (Math.cos(Math.PI * Math.pow(this.data.attenuation, 1.5)) - 1) / -2;
this.cachedAttenuation = this.data.attenuation;
}
u.attenuation = this.computedAttenuation;
u.elevation = this.data.elevation;
u.color = this.colorRGB ?? shader.constructor.defaultUniforms.color;
// Passing screenDimensions to use screen size render textures
u.screenDimensions = canvas.screenDimensions;
if ( !u.depthTexture ) u.depthTexture = canvas.masks.depth.renderTexture;
if ( !u.primaryTexture ) u.primaryTexture = canvas.primary.renderTexture;
if ( !u.darknessLevelTexture ) u.darknessLevelTexture = canvas.effects.illumination.renderTexture;
}
/* -------------------------------------------- */
/* Animation Functions */
/* -------------------------------------------- */
/**
* An animation with flickering ratio and light intensity.
* @param {number} dt Delta time
* @param {object} [options={}] Additional options which modify the flame animation
* @param {number} [options.speed=5] The animation speed, from 0 to 10
* @param {number} [options.intensity=5] The animation intensity, from 1 to 10
* @param {boolean} [options.reverse=false] Reverse the animation direction
*/
animateTorch(dt, {speed=5, intensity=5, reverse=false} = {}) {
this.animateFlickering(dt, {speed, intensity, reverse, amplification: intensity / 5});
}
/* -------------------------------------------- */
/**
* An animation with flickering ratio and light intensity
* @param {number} dt Delta time
* @param {object} [options={}] Additional options which modify the flame animation
* @param {number} [options.speed=5] The animation speed, from 0 to 10
* @param {number} [options.intensity=5] The animation intensity, from 1 to 10
* @param {number} [options.amplification=1] Noise amplification (>1) or dampening (<1)
* @param {boolean} [options.reverse=false] Reverse the animation direction
*/
animateFlickering(dt, {speed=5, intensity=5, reverse=false, amplification=1} = {}) {
this.animateTime(dt, {speed, intensity, reverse});
// Create the noise object for the first frame
const amplitude = amplification * 0.45;
if ( !this._noise ) this._noise = new SmoothNoise({amplitude: amplitude, scale: 3, maxReferences: 2048});
// Update amplitude
if ( this._noise.amplitude !== amplitude ) this._noise.amplitude = amplitude;
// Create noise from animation time. Range [0.0, 0.45]
let n = this._noise.generate(this.animation.time);
// Update brightnessPulse and ratio with some noise in it
const co = this.layers.coloration.shader;
const il = this.layers.illumination.shader;
co.uniforms.brightnessPulse = il.uniforms.brightnessPulse = 0.55 + n; // Range [0.55, 1.0 <* amplification>]
co.uniforms.ratio = il.uniforms.ratio = (this.ratio * 0.9) + (n * 0.222);// Range [ratio * 0.9, ratio * ~1.0 <* amplification>]
}
/* -------------------------------------------- */
/**
* A basic "pulse" animation which expands and contracts.
* @param {number} dt Delta time
* @param {object} [options={}] Additional options which modify the pulse animation
* @param {number} [options.speed=5] The animation speed, from 0 to 10
* @param {number} [options.intensity=5] The animation intensity, from 1 to 10
* @param {boolean} [options.reverse=false] Reverse the animation direction
*/
animatePulse(dt, {speed=5, intensity=5, reverse=false}={}) {
// Determine the animation timing
let t = canvas.app.ticker.lastTime;
if ( reverse ) t *= -1;
this.animation.time = ((speed * t)/5000) + this.animation.seed;
// Define parameters
const i = (10 - intensity) * 0.1;
const w = 0.5 * (Math.cos(this.animation.time * 2.5) + 1);
const wave = (a, b, w) => ((a - b) * w) + b;
// Pulse coloration
const co = this.layers.coloration.shader;
co.uniforms.intensity = intensity;
co.uniforms.time = this.animation.time;
co.uniforms.pulse = wave(1.2, i, w);
// Pulse illumination
const il = this.layers.illumination.shader;
il.uniforms.intensity = intensity;
il.uniforms.time = this.animation.time;
il.uniforms.ratio = wave(this.ratio, this.ratio * i, w);
}
/* -------------------------------------------- */
/* Deprecations and Compatibility */
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
get isDarkness() {
const msg = "BaseLightSource#isDarkness is now obsolete. Use DarknessSource instead.";
foundry.utils.logCompatibilityWarning(msg, { since: 12, until: 14});
return false;
}
}