Initial
This commit is contained in:
126
resources/app/client/pixi/layers/masks/depth.js
Normal file
126
resources/app/client/pixi/layers/masks/depth.js
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* The depth mask which contains a mapping of elevation. Needed to know if we must render objects according to depth.
|
||||
* Red channel: Lighting occlusion (top).
|
||||
* Green channel: Lighting occlusion (bottom).
|
||||
* Blue channel: Weather occlusion.
|
||||
* @category - Canvas
|
||||
*/
|
||||
class CanvasDepthMask extends CachedContainer {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.#createDepth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Container in which roofs are rendered with depth data.
|
||||
* @type {PIXI.Container}
|
||||
*/
|
||||
roofs;
|
||||
|
||||
/** @override */
|
||||
static textureConfiguration = {
|
||||
scaleMode: PIXI.SCALE_MODES.NEAREST,
|
||||
format: PIXI.FORMATS.RGB,
|
||||
multisample: PIXI.MSAA_QUALITY.NONE
|
||||
};
|
||||
|
||||
/** @override */
|
||||
clearColor = [0, 0, 0, 0];
|
||||
|
||||
/**
|
||||
* Update the elevation-to-depth mapping?
|
||||
* @type {boolean}
|
||||
* @internal
|
||||
*/
|
||||
_elevationDirty = false;
|
||||
|
||||
/**
|
||||
* The elevations of the elevation-to-depth mapping.
|
||||
* Supported are up to 255 unique elevations.
|
||||
* @type {Float64Array}
|
||||
*/
|
||||
#elevations = new Float64Array([-Infinity]);
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Map an elevation to a value in the range [0, 1] with 8-bit precision.
|
||||
* The depth-rendered object are rendered with these values into the render texture.
|
||||
* @param {number} elevation The elevation in distance units
|
||||
* @returns {number} The value for this elevation in the range [0, 1] with 8-bit precision
|
||||
*/
|
||||
mapElevation(elevation) {
|
||||
const E = this.#elevations;
|
||||
if ( elevation < E[0] ) return 0;
|
||||
let i = 0;
|
||||
let j = E.length - 1;
|
||||
while ( i < j ) {
|
||||
const k = (i + j + 1) >> 1;
|
||||
const e = E[k];
|
||||
if ( e <= elevation ) i = k;
|
||||
else j = k - 1;
|
||||
}
|
||||
return (i + 1) / 255;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update the elevation-to-depth mapping.
|
||||
* Needs to be called after the children have been sorted
|
||||
* and the canvas transform phase.
|
||||
* @internal
|
||||
*/
|
||||
_update() {
|
||||
if ( !this._elevationDirty ) return;
|
||||
this._elevationDirty = false;
|
||||
const elevations = [];
|
||||
const children = canvas.primary.children;
|
||||
for ( let i = 0, n = children.length; i < n; i++ ) {
|
||||
const child = children[i];
|
||||
if ( !child.shouldRenderDepth ) continue;
|
||||
const elevation = child.elevation;
|
||||
if ( elevation === elevations.at(-1) ) continue;
|
||||
elevations.push(elevation);
|
||||
}
|
||||
if ( !elevations.length ) elevations.push(-Infinity);
|
||||
else elevations.length = Math.min(elevations.length, 255);
|
||||
this.#elevations = new Float64Array(elevations);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Initialize the depth mask with the roofs container and token graphics.
|
||||
*/
|
||||
#createDepth() {
|
||||
this.roofs = this.addChild(this.#createRoofsContainer());
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create the roofs container.
|
||||
* @returns {PIXI.Container}
|
||||
*/
|
||||
#createRoofsContainer() {
|
||||
const c = new PIXI.Container();
|
||||
const render = renderer => {
|
||||
// Render the depth of each primary canvas object
|
||||
for ( const pco of canvas.primary.children ) {
|
||||
pco.renderDepthData?.(renderer);
|
||||
}
|
||||
};
|
||||
c.render = render.bind(c);
|
||||
return c;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Clear the depth mask.
|
||||
*/
|
||||
clear() {
|
||||
Canvas.clearContainer(this.roofs, false);
|
||||
}
|
||||
}
|
||||
204
resources/app/client/pixi/layers/masks/occlusion.js
Normal file
204
resources/app/client/pixi/layers/masks/occlusion.js
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* The occlusion mask which contains radial occlusion and vision occlusion from tokens.
|
||||
* Red channel: Fade occlusion.
|
||||
* Green channel: Radial occlusion.
|
||||
* Blue channel: Vision occlusion.
|
||||
* @category - Canvas
|
||||
*/
|
||||
class CanvasOcclusionMask extends CachedContainer {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.#createOcclusion();
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static textureConfiguration = {
|
||||
scaleMode: PIXI.SCALE_MODES.NEAREST,
|
||||
format: PIXI.FORMATS.RGB,
|
||||
multisample: PIXI.MSAA_QUALITY.NONE
|
||||
};
|
||||
|
||||
/**
|
||||
* Graphics in which token radial and vision occlusion shapes are drawn.
|
||||
* @type {PIXI.LegacyGraphics}
|
||||
*/
|
||||
tokens;
|
||||
|
||||
/**
|
||||
* The occludable tokens.
|
||||
* @type {Token[]}
|
||||
*/
|
||||
#tokens;
|
||||
|
||||
/** @override */
|
||||
clearColor = [0, 1, 1, 1];
|
||||
|
||||
/** @override */
|
||||
autoRender = false;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is vision occlusion active?
|
||||
* @type {boolean}
|
||||
*/
|
||||
get vision() {
|
||||
return this.#vision;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
#vision = false;
|
||||
|
||||
/**
|
||||
* The elevations of the elevation-to-depth mapping.
|
||||
* Supported are up to 255 unique elevations.
|
||||
* @type {Float64Array}
|
||||
*/
|
||||
#elevations = new Float64Array([-Infinity]);
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Initialize the depth mask with the roofs container and token graphics.
|
||||
*/
|
||||
#createOcclusion() {
|
||||
this.alphaMode = PIXI.ALPHA_MODES.NO_PREMULTIPLIED_ALPHA;
|
||||
this.tokens = this.addChild(new PIXI.LegacyGraphics());
|
||||
this.tokens.blendMode = PIXI.BLEND_MODES.MIN_ALL;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Clear the occlusion mask.
|
||||
*/
|
||||
clear() {
|
||||
this.tokens.clear();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Occlusion Management */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Map an elevation to a value in the range [0, 1] with 8-bit precision.
|
||||
* The radial and vision shapes are drawn with these values into the render texture.
|
||||
* @param {number} elevation The elevation in distance units
|
||||
* @returns {number} The value for this elevation in the range [0, 1] with 8-bit precision
|
||||
*/
|
||||
mapElevation(elevation) {
|
||||
const E = this.#elevations;
|
||||
let i = 0;
|
||||
let j = E.length - 1;
|
||||
if ( elevation > E[j] ) return 1;
|
||||
while ( i < j ) {
|
||||
const k = (i + j) >> 1;
|
||||
const e = E[k];
|
||||
if ( e >= elevation ) j = k;
|
||||
else i = k + 1;
|
||||
}
|
||||
return i / 255;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update the set of occludable Tokens, redraw the occlusion mask, and update the occluded state
|
||||
* of all occludable objects.
|
||||
*/
|
||||
updateOcclusion() {
|
||||
this.#tokens = canvas.tokens._getOccludableTokens();
|
||||
this._updateOcclusionMask();
|
||||
this._updateOcclusionStates();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Draw occlusion shapes to the occlusion mask.
|
||||
* Fade occlusion draws to the red channel with varying intensity from [0, 1] based on elevation.
|
||||
* Radial occlusion draws to the green channel with varying intensity from [0, 1] based on elevation.
|
||||
* Vision occlusion draws to the blue channel with varying intensity from [0, 1] based on elevation.
|
||||
* @internal
|
||||
*/
|
||||
_updateOcclusionMask() {
|
||||
this.#vision = false;
|
||||
this.tokens.clear();
|
||||
const elevations = [];
|
||||
for ( const token of this.#tokens.sort((a, b) => a.document.elevation - b.document.elevation) ) {
|
||||
const elevation = token.document.elevation;
|
||||
if ( elevation !== elevations.at(-1) ) elevations.push(elevation);
|
||||
const occlusionElevation = Math.min(elevations.length - 1, 255);
|
||||
|
||||
// Draw vision occlusion
|
||||
if ( token.vision?.active ) {
|
||||
this.#vision = true;
|
||||
this.tokens.beginFill(0xFFFF00 | occlusionElevation).drawShape(token.vision.los).endFill();
|
||||
}
|
||||
|
||||
// Draw radial occlusion (and radial into the vision channel if this token doesn't have vision)
|
||||
const origin = token.center;
|
||||
const occlusionRadius = Math.max(token.externalRadius, token.getLightRadius(token.document.occludable.radius));
|
||||
this.tokens.beginFill(0xFF0000 | (occlusionElevation << 8) | (token.vision?.active ? 0xFF : occlusionElevation))
|
||||
.drawCircle(origin.x, origin.y, occlusionRadius).endFill();
|
||||
}
|
||||
if ( !elevations.length ) elevations.push(-Infinity);
|
||||
else elevations.length = Math.min(elevations.length, 255);
|
||||
this.#elevations = new Float64Array(elevations);
|
||||
this.renderDirty = true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update the current occlusion status of all Tile objects.
|
||||
* @internal
|
||||
*/
|
||||
_updateOcclusionStates() {
|
||||
const occluded = this._identifyOccludedObjects(this.#tokens);
|
||||
for ( const pco of canvas.primary.children ) {
|
||||
const isOccludable = pco.isOccludable;
|
||||
if ( (isOccludable === undefined) || (!isOccludable && !pco.occluded) ) continue;
|
||||
pco.debounceSetOcclusion(occluded.has(pco));
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Determine the set of objects which should be currently occluded by a Token.
|
||||
* @param {Token[]} tokens The set of currently controlled Token objects
|
||||
* @returns {Set<PrimaryCanvasObjectMixin>} The PCO objects which should be currently occluded
|
||||
* @protected
|
||||
*/
|
||||
_identifyOccludedObjects(tokens) {
|
||||
const occluded = new Set();
|
||||
for ( const token of tokens ) {
|
||||
// Get the occludable primary canvas objects (PCO) according to the token bounds
|
||||
const matchingPCO = canvas.primary.quadtree.getObjects(token.bounds);
|
||||
for ( const pco of matchingPCO ) {
|
||||
// Don't bother re-testing a PCO or an object which is not occludable
|
||||
if ( !pco.isOccludable || occluded.has(pco) ) continue;
|
||||
if ( pco.testOcclusion(token, {corners: pco.restrictsLight && pco.restrictsWeather}) ) occluded.add(pco);
|
||||
}
|
||||
}
|
||||
return occluded;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Deprecation and compatibility */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since v11
|
||||
* @ignore
|
||||
*/
|
||||
_identifyOccludedTiles() {
|
||||
const msg = "CanvasOcclusionMask#_identifyOccludedTiles has been deprecated in " +
|
||||
"favor of CanvasOcclusionMask#_identifyOccludedObjects.";
|
||||
foundry.utils.logCompatibilityWarning(msg, {since: 11, until: 13});
|
||||
return this._identifyOccludedObjects();
|
||||
}
|
||||
}
|
||||
162
resources/app/client/pixi/layers/masks/vision.js
Normal file
162
resources/app/client/pixi/layers/masks/vision.js
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* @typedef {object} _CanvasVisionContainerSight
|
||||
* @property {PIXI.LegacyGraphics} preview FOV that should not be committed to fog exploration.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The sight part of {@link CanvasVisionContainer}.
|
||||
* The blend mode is MAX_COLOR.
|
||||
* @typedef {PIXI.LegacyGraphics & _CanvasVisionContainerSight} CanvasVisionContainerSight
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} _CanvasVisionContainerLight
|
||||
* @property {PIXI.LegacyGraphics} preview FOV that should not be committed to fog exploration.
|
||||
* @property {SpriteMesh} cached The sprite with the texture of FOV of cached light sources.
|
||||
* @property {PIXI.LegacyGraphics & {preview: PIXI.LegacyGraphics}} mask
|
||||
* The light perception polygons of vision sources and the FOV of vision sources that provide vision.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The light part of {@link CanvasVisionContainer}.
|
||||
* The blend mode is MAX_COLOR.
|
||||
* @typedef {PIXI.LegacyGraphics & _CanvasVisionContainerLight} CanvasVisionContainerLight
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} _CanvasVisionContainerDarkness
|
||||
* @property {PIXI.LegacyGraphics} darkness Darkness source erasing fog of war.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The sight part of {@link CanvasVisionContainer}.
|
||||
* The blend mode is ERASE.
|
||||
* @typedef {PIXI.LegacyGraphics & _CanvasVisionContainerDarkness} CanvasVisionContainerDarkness
|
||||
*/
|
||||
|
||||
/**
|
||||
* The sight part of {@link CanvasVisionContainer}.
|
||||
* The blend mode is MAX_COLOR.
|
||||
* @typedef {PIXI.LegacyGraphics & _CanvasVisionContainerSight} CanvasVisionContainerSight
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} _CanvasVisionContainer
|
||||
* @property {CanvasVisionContainerLight} light Areas visible because of light sources and light perception.
|
||||
* @property {CanvasVisionContainerSight} sight Areas visible because of FOV of vision sources.
|
||||
* @property {CanvasVisionContainerDarkness} darkness Areas erased by darkness sources.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The currently visible areas.
|
||||
* @typedef {PIXI.Container & _CanvasVisionContainer} CanvasVisionContainer
|
||||
*/
|
||||
|
||||
/**
|
||||
* The vision mask which contains the current line-of-sight texture.
|
||||
* @category - Canvas
|
||||
*/
|
||||
class CanvasVisionMask extends CachedContainer {
|
||||
|
||||
/** @override */
|
||||
static textureConfiguration = {
|
||||
scaleMode: PIXI.SCALE_MODES.NEAREST,
|
||||
format: PIXI.FORMATS.RED,
|
||||
multisample: PIXI.MSAA_QUALITY.NONE
|
||||
};
|
||||
|
||||
/** @override */
|
||||
clearColor = [0, 0, 0, 0];
|
||||
|
||||
/** @override */
|
||||
autoRender = false;
|
||||
|
||||
/**
|
||||
* The current vision Container.
|
||||
* @type {CanvasVisionContainer}
|
||||
*/
|
||||
vision;
|
||||
|
||||
/**
|
||||
* The BlurFilter which applies to the vision mask texture.
|
||||
* This filter applies a NORMAL blend mode to the container.
|
||||
* @type {AlphaBlurFilter}
|
||||
*/
|
||||
blurFilter;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create the BlurFilter for the VisionMask container.
|
||||
* @returns {AlphaBlurFilter}
|
||||
*/
|
||||
#createBlurFilter() {
|
||||
// Initialize filters properties
|
||||
this.filters ??= [];
|
||||
this.filterArea = null;
|
||||
|
||||
// Check if the canvas blur is disabled and return without doing anything if necessary
|
||||
const b = canvas.blur;
|
||||
this.filters.findSplice(f => f === this.blurFilter);
|
||||
if ( !b.enabled ) return;
|
||||
|
||||
// Create the new filter
|
||||
const f = this.blurFilter = new b.blurClass(b.strength, b.passes, PIXI.Filter.defaultResolution, b.kernels);
|
||||
f.blendMode = PIXI.BLEND_MODES.NORMAL;
|
||||
this.filterArea = canvas.app.renderer.screen;
|
||||
this.filters.push(f);
|
||||
return canvas.addBlurFilter(this.blurFilter);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
async draw() {
|
||||
this.#createBlurFilter();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Initialize the vision mask with the los and the fov graphics objects.
|
||||
* @param {PIXI.Container} vision The vision container to attach
|
||||
* @returns {CanvasVisionContainer}
|
||||
*/
|
||||
attachVision(vision) {
|
||||
return this.vision = this.addChild(vision);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Detach the vision mask from the cached container.
|
||||
* @returns {CanvasVisionContainer} The detached vision container.
|
||||
*/
|
||||
detachVision() {
|
||||
const vision = this.vision;
|
||||
this.removeChild(vision);
|
||||
this.vision = undefined;
|
||||
return vision;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Deprecations and Compatibility */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since v11
|
||||
* @ignore
|
||||
*/
|
||||
get filter() {
|
||||
foundry.utils.logCompatibilityWarning("CanvasVisionMask#filter has been renamed to blurFilter.", {since: 11, until: 13});
|
||||
return this.blurFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since v11
|
||||
* @ignore
|
||||
*/
|
||||
set filter(f) {
|
||||
foundry.utils.logCompatibilityWarning("CanvasVisionMask#filter has been renamed to blurFilter.", {since: 11, until: 13});
|
||||
this.blurFilter = f;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user