This commit is contained in:
2025-01-04 00:34:03 +01:00
parent 41829408dc
commit 0ca14bbc19
18111 changed files with 1871397 additions and 0 deletions

View File

@@ -0,0 +1,625 @@
import BaseEffectSource from "./base-effect-source.mjs";
/**
* @typedef {import("./base-effect-source.mjs").BaseEffectSourceData} BaseEffectSourceData
*/
/**
* @typedef {Object} RenderedEffectSourceData
* @property {object} animation An animation configuration for the source
* @property {number|null} color A color applied to the rendered effect
* @property {number|null} seed An integer seed to synchronize (or de-synchronize) animations
* @property {boolean} preview Is this source a temporary preview?
*/
/**
* @typedef {Object} RenderedEffectSourceAnimationConfig
* @property {string} [label] The human-readable (localized) label for the animation
* @property {Function} [animation] The animation function that runs every frame
* @property {AdaptiveIlluminationShader} [illuminationShader] A custom illumination shader used by this animation
* @property {AdaptiveColorationShader} [colorationShader] A custom coloration shader used by this animation
* @property {AdaptiveBackgroundShader} [backgroundShader] A custom background shader used by this animation
* @property {AdaptiveDarknessShader} [darknessShader] A custom darkness shader used by this animation
* @property {number} [seed] The animation seed
* @property {number} [time] The animation time
*/
/**
* @typedef {Object} RenderedEffectLayerConfig
* @property {AdaptiveLightingShader} defaultShader The default shader used by this layer
* @property {PIXI.BLEND_MODES} blendMode The blend mode used by this layer
*/
/**
* An abstract class which extends the base PointSource to provide common functionality for rendering.
* This class is extended by both the LightSource and VisionSource subclasses.
* @extends {BaseEffectSource<BaseEffectSourceData & RenderedEffectSourceData>}
* @abstract
*/
export default class RenderedEffectSource extends BaseEffectSource {
/**
* Keys of the data object which require shaders to be re-initialized.
* @type {string[]}
* @protected
*/
static _initializeShaderKeys = ["animation.type"];
/**
* Keys of the data object which require uniforms to be refreshed.
* @type {string[]}
* @protected
*/
static _refreshUniformsKeys = [];
/**
* Layers handled by this rendered source.
* @type {Record<string, RenderedEffectLayerConfig>}
* @protected
*/
static get _layers() {
return {
background: {
defaultShader: AdaptiveBackgroundShader,
blendMode: "MAX_COLOR"
},
coloration: {
defaultShader: AdaptiveColorationShader,
blendMode: "SCREEN"
},
illumination: {
defaultShader: AdaptiveIlluminationShader,
blendMode: "MAX_COLOR"
}
};
}
/**
* The offset in pixels applied to create soft edges.
* @type {number}
*/
static EDGE_OFFSET = -8;
/** @inheritDoc */
static defaultData = {
...super.defaultData,
animation: {},
seed: null,
preview: false,
color: null
}
/* -------------------------------------------- */
/* Rendered Source Attributes */
/* -------------------------------------------- */
/**
* The animation configuration applied to this source
* @type {RenderedEffectSourceAnimationConfig}
*/
animation = {};
/**
* @typedef {Object} RenderedEffectSourceLayer
* @property {boolean} active Is this layer actively rendered?
* @property {boolean} reset Do uniforms need to be reset?
* @property {boolean} suppressed Is this layer temporarily suppressed?
* @property {PointSourceMesh} mesh The rendered mesh for this layer
* @property {AdaptiveLightingShader} shader The shader instance used for the layer
*/
/**
* Track the status of rendering layers
* @type {{
* background: RenderedEffectSourceLayer,
* coloration: RenderedEffectSourceLayer,
* illumination: RenderedEffectSourceLayer
* }}
*/
layers = Object.entries(this.constructor._layers).reduce((obj, [layer, config]) => {
obj[layer] = {active: true, reset: true, suppressed: false,
mesh: undefined, shader: undefined, defaultShader: config.defaultShader,
vmUniforms: undefined, blendMode: config.blendMode};
return obj;
}, {});
/**
* Array of update uniforms functions.
* @type {Function[]}
*/
#updateUniformsFunctions = (() => {
const initializedFunctions = [];
for ( const layer in this.layers ) {
const fn = this[`_update${layer.titleCase()}Uniforms`];
if ( fn ) initializedFunctions.push(fn);
}
return initializedFunctions;
})();
/**
* The color of the source as an RGB vector.
* @type {[number, number, number]|null}
*/
colorRGB = null;
/**
* PIXI Geometry generated to draw meshes.
* @type {PIXI.Geometry|null}
* @protected
*/
_geometry = null;
/* -------------------------------------------- */
/* Source State */
/* -------------------------------------------- */
/**
* Is the rendered source animated?
* @type {boolean}
*/
get isAnimated() {
return this.active && this.data.animation?.type;
}
/**
* Has the rendered source at least one active layer?
* @type {boolean}
*/
get hasActiveLayer() {
return this.#hasActiveLayer;
}
#hasActiveLayer = false;
/**
* Is this RenderedEffectSource a temporary preview?
* @returns {boolean}
*/
get isPreview() {
return !!this.data.preview;
}
/* -------------------------------------------- */
/* Rendered Source Properties */
/* -------------------------------------------- */
/**
* A convenience accessor to the background layer mesh.
* @type {PointSourceMesh}
*/
get background() {
return this.layers.background.mesh;
}
/**
* A convenience accessor to the coloration layer mesh.
* @type {PointSourceMesh}
*/
get coloration() {
return this.layers.coloration.mesh;
}
/**
* A convenience accessor to the illumination layer mesh.
* @type {PointSourceMesh}
*/
get illumination() {
return this.layers.illumination.mesh;
}
/* -------------------------------------------- */
/* Rendered Source Initialization */
/* -------------------------------------------- */
/** @inheritDoc */
_initialize(data) {
super._initialize(data);
const color = Color.from(this.data.color ?? null);
this.data.color = color.valid ? color.valueOf() : null;
const seed = this.data.seed ?? this.animation.seed ?? Math.floor(Math.random() * 100000);
this.animation = this.data.animation = {seed, ...this.data.animation};
// Initialize the color attributes
const hasColor = this._flags.hasColor = (this.data.color !== null);
if ( hasColor ) Color.applyRGB(color, this.colorRGB ??= [0, 0, 0]);
else this.colorRGB = null;
// We need to update the hasColor uniform attribute immediately
for ( const layer of Object.values(this.layers) ) {
if ( layer.shader ) layer.shader.uniforms.hasColor = hasColor;
}
this._initializeSoftEdges();
}
/* -------------------------------------------- */
/**
* Decide whether to render soft edges with a blur.
* @protected
*/
_initializeSoftEdges() {
this._flags.renderSoftEdges = canvas.performance.lightSoftEdges && !this.isPreview;
}
/* -------------------------------------------- */
/** @override */
_configure(changes) {
// To know if we need a first time initialization of the shaders
const initializeShaders = !this._geometry;
// Initialize meshes using the computed shape
this.#initializeMeshes();
// Initialize shaders
if ( initializeShaders || this.constructor._initializeShaderKeys.some(k => k in changes) ) {
this.#initializeShaders();
}
// Refresh uniforms
else if ( this.constructor._refreshUniformsKeys.some(k => k in changes) ) {
for ( const config of Object.values(this.layers) ) {
config.reset = true;
}
}
// Update the visible state the layers
this.#updateVisibleLayers();
}
/* -------------------------------------------- */
/**
* Configure which shaders are used for each rendered layer.
* @returns {{
* background: AdaptiveLightingShader,
* coloration: AdaptiveLightingShader,
* illumination: AdaptiveLightingShader
* }}
* @private
*/
_configureShaders() {
const a = this.animation;
const shaders = {};
for ( const layer in this.layers ) {
shaders[layer] = a[`${layer.toLowerCase()}Shader`] || this.layers[layer].defaultShader;
}
return shaders;
}
/* -------------------------------------------- */
/**
* Specific configuration for a layer.
* @param {object} layer
* @param {string} layerId
* @protected
*/
_configureLayer(layer, layerId) {}
/* -------------------------------------------- */
/**
* Initialize the shaders used for this source, swapping to a different shader if the animation has changed.
*/
#initializeShaders() {
const shaders = this._configureShaders();
for ( const [layerId, layer] of Object.entries(this.layers) ) {
layer.shader = RenderedEffectSource.#createShader(shaders[layerId], layer.mesh);
this._configureLayer(layer, layerId);
}
this.#updateUniforms();
Hooks.callAll(`initialize${this.constructor.name}Shaders`, this);
}
/* -------------------------------------------- */
/**
* Create a new shader using a provider shader class
* @param {typeof AdaptiveLightingShader} cls The shader class to create
* @param {PointSourceMesh} container The container which requires a new shader
* @returns {AdaptiveLightingShader} The shader instance used
*/
static #createShader(cls, container) {
const current = container.shader;
if ( current?.constructor === cls ) return current;
const shader = cls.create({
primaryTexture: canvas.primary.renderTexture
});
shader.container = container;
container.shader = shader;
container.uniforms = shader.uniforms;
if ( current ) current.destroy();
return shader;
}
/* -------------------------------------------- */
/**
* Initialize the geometry and the meshes.
*/
#initializeMeshes() {
this._updateGeometry();
if ( !this._flags.initializedMeshes ) this.#createMeshes();
}
/* -------------------------------------------- */
/**
* Create meshes for each layer of the RenderedEffectSource that is drawn to the canvas.
*/
#createMeshes() {
if ( !this._geometry ) return;
const shaders = this._configureShaders();
for ( const [l, layer] of Object.entries(this.layers) ) {
layer.mesh = this.#createMesh(shaders[l]);
layer.mesh.blendMode = PIXI.BLEND_MODES[layer.blendMode];
layer.shader = layer.mesh.shader;
}
this._flags.initializedMeshes = true;
}
/* -------------------------------------------- */
/**
* Create a new Mesh for this source using a provided shader class
* @param {typeof AdaptiveLightingShader} shaderCls The shader class used for this mesh
* @returns {PointSourceMesh} The created Mesh
*/
#createMesh(shaderCls) {
const state = new PIXI.State();
const mesh = new PointSourceMesh(this._geometry, shaderCls.create(), state);
mesh.drawMode = PIXI.DRAW_MODES.TRIANGLES;
mesh.uniforms = mesh.shader.uniforms;
mesh.cullable = true;
return mesh;
}
/* -------------------------------------------- */
/**
* Create the geometry for the source shape that is used in shaders and compute its bounds for culling purpose.
* Triangulate the form and create buffers.
* @protected
* @abstract
*/
_updateGeometry() {}
/* -------------------------------------------- */
/* Rendered Source Canvas Rendering */
/* -------------------------------------------- */
/**
* Render the containers used to represent this light source within the LightingLayer
* @returns {{background: PIXI.Mesh, coloration: PIXI.Mesh, illumination: PIXI.Mesh}}
*/
drawMeshes() {
const meshes = {};
for ( const layerId of Object.keys(this.layers) ) {
meshes[layerId] = this._drawMesh(layerId);
}
return meshes;
}
/* -------------------------------------------- */
/**
* Create a Mesh for a certain rendered layer of this source.
* @param {string} layerId The layer key in layers to draw
* @returns {PIXI.Mesh|null} The drawn mesh for this layer, or null if no mesh is required
* @protected
*/
_drawMesh(layerId) {
const layer = this.layers[layerId];
const mesh = layer.mesh;
if ( layer.reset ) {
const fn = this[`_update${layerId.titleCase()}Uniforms`];
fn.call(this);
}
if ( !layer.active ) {
mesh.visible = false;
return null;
}
// Update the mesh
const {x, y} = this.data;
mesh.position.set(x, y);
mesh.visible = mesh.renderable = true;
return layer.mesh;
}
/* -------------------------------------------- */
/* Rendered Source Refresh */
/* -------------------------------------------- */
/** @override */
_refresh() {
this.#updateUniforms();
this.#updateVisibleLayers();
}
/* -------------------------------------------- */
/**
* Update uniforms for all rendered layers.
*/
#updateUniforms() {
for ( const updateUniformsFunction of this.#updateUniformsFunctions ) updateUniformsFunction.call(this);
}
/* -------------------------------------------- */
/**
* Update the visible state of the component channels of this RenderedEffectSource.
* @returns {boolean} Is there an active layer?
*/
#updateVisibleLayers() {
const active = this.active;
let hasActiveLayer = false;
for ( const layer of Object.values(this.layers) ) {
layer.active = active && (layer.shader?.isRequired !== false);
if ( layer.active ) hasActiveLayer = true;
}
this.#hasActiveLayer = hasActiveLayer;
}
/* -------------------------------------------- */
/**
* Update shader uniforms used by every rendered layer.
* @param {AbstractBaseShader} shader
* @protected
*/
_updateCommonUniforms(shader) {}
/* -------------------------------------------- */
/**
* Update shader uniforms used for the background layer.
* @protected
*/
_updateBackgroundUniforms() {
const shader = this.layers.background.shader;
if ( !shader ) return;
this._updateCommonUniforms(shader);
}
/* -------------------------------------------- */
/**
* Update shader uniforms used for the coloration layer.
* @protected
*/
_updateColorationUniforms() {
const shader = this.layers.coloration.shader;
if ( !shader ) return;
this._updateCommonUniforms(shader);
}
/* -------------------------------------------- */
/**
* Update shader uniforms used for the illumination layer.
* @protected
*/
_updateIlluminationUniforms() {
const shader = this.layers.illumination.shader;
if ( !shader ) return;
this._updateCommonUniforms(shader);
}
/* -------------------------------------------- */
/* Rendered Source Destruction */
/* -------------------------------------------- */
/** @override */
_destroy() {
for ( const layer of Object.values(this.layers) ) layer.mesh?.destroy();
this._geometry?.destroy();
}
/* -------------------------------------------- */
/* Animation Functions */
/* -------------------------------------------- */
/**
* Animate the PointSource, if an animation is enabled and if it currently has rendered containers.
* @param {number} dt Delta time.
*/
animate(dt) {
if ( !this.isAnimated ) return;
const {animation, ...options} = this.animation;
return animation?.call(this, dt, options);
}
/* -------------------------------------------- */
/**
* Generic time-based animation used for Rendered Point Sources.
* @param {number} dt Delta time.
* @param {object} [options] Options which affect the time 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
*/
animateTime(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;
// Update uniforms
for ( const layer of Object.values(this.layers) ) {
const u = layer.mesh.uniforms;
u.time = this.animation.time;
u.intensity = intensity;
}
}
/* -------------------------------------------- */
/* Static Helper Methods */
/* -------------------------------------------- */
/**
* Get corrected level according to level and active vision mode data.
* @param {VisionMode.LIGHTING_LEVELS} level
* @returns {number} The corrected level.
*/
static getCorrectedLevel(level) {
// Retrieving the lighting mode and the corrected level, if any
const lightingOptions = canvas.visibility.visionModeData?.activeLightingOptions;
return (lightingOptions?.levels?.[level]) ?? level;
}
/* -------------------------------------------- */
/**
* Get corrected color according to level, dim color, bright color and background color.
* @param {VisionMode.LIGHTING_LEVELS} level
* @param {Color} colorDim
* @param {Color} colorBright
* @param {Color} [colorBackground]
* @returns {Color}
*/
static getCorrectedColor(level, colorDim, colorBright, colorBackground) {
colorBackground ??= canvas.colors.background;
// Returning the corrected color according to the lighting options
const levels = VisionMode.LIGHTING_LEVELS;
switch ( this.getCorrectedLevel(level) ) {
case levels.HALFDARK:
case levels.DIM: return colorDim;
case levels.BRIGHT:
case levels.DARKNESS: return colorBright;
case levels.BRIGHTEST: return canvas.colors.ambientBrightest;
case levels.UNLIT: return colorBackground;
default: return colorDim;
}
}
/* -------------------------------------------- */
/* Deprecations and Compatibility */
/* -------------------------------------------- */
/**
* @deprecated since v11
* @ignore
*/
get preview() {
const msg = "The RenderedEffectSource#preview is deprecated. Use RenderedEffectSource#isPreview instead.";
foundry.utils.logCompatibilityWarning(msg, {since: 11, until: 13});
return this.isPreview;
}
/**
* @deprecated since v11
* @ignore
*/
set preview(preview) {
const msg = "The RenderedEffectSource#preview is deprecated. "
+ "Set RenderedEffectSource#preview as part of RenderedEffectSource#initialize instead.";
foundry.utils.logCompatibilityWarning(msg, {since: 11, until: 13});
this.data.preview = preview;
}
}