From 66f7c7b094c13dfb30475334eebefe967c79d7e2 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sat, 17 Jun 2023 06:54:19 -0700 Subject: [PATCH 01/12] Remove unused Elevation Filter --- scripts/ElevationLayer.js | 72 --------------------------------------- 1 file changed, 72 deletions(-) diff --git a/scripts/ElevationLayer.js b/scripts/ElevationLayer.js index a1d0274d..65529c9c 100755 --- a/scripts/ElevationLayer.js +++ b/scripts/ElevationLayer.js @@ -1357,78 +1357,6 @@ export class ElevationLayer extends InteractionLayer { } -/** - * Filter used to display the elevation layer coloration of elevation data. - * elevationSampler is a texture that stores elevation data in the red channel. - * Elevation data currently displayed as a varying red color with varying alpha. - * Alpha is gamma corrected to ensure only darker alphas and red shades are used, to - * ensure the lower elevation values are perceivable. - */ -class ElevationFilter extends AbstractBaseFilter { - static vertexShader = ` - attribute vec2 aVertexPosition; - - uniform mat3 projectionMatrix; - uniform mat3 canvasMatrix; - uniform vec4 inputSize; - uniform vec4 outputFrame; - uniform vec2 dimensions; - - varying vec2 vTextureCoord; -// varying vec2 vCanvasCoord; - varying vec2 vCanvasCoordNorm; - - void main(void) - { - vTextureCoord = aVertexPosition * (outputFrame.zw * inputSize.zw); - vec2 position = aVertexPosition * max(outputFrame.zw, vec2(0.)) + outputFrame.xy; - vec2 canvasCoord = (canvasMatrix * vec3(position, 1.0)).xy; - vCanvasCoordNorm = canvasCoord / dimensions; - gl_Position = vec4((projectionMatrix * vec3(position, 1.0)).xy, 0.0, 1.0); - } - `; - - static fragmentShader = ` - varying vec2 vTextureCoord; - varying vec2 vCanvasCoord; - varying vec2 vCanvasCoordNorm; - - uniform sampler2D uSampler; - uniform sampler2D elevationSampler; - - void main() { - vec4 tex = texture2D(uSampler, vTextureCoord); - vec4 elevation = texture2D(elevationSampler, vCanvasCoordNorm); - - - if ( elevation.r == 0. - || vCanvasCoordNorm.x < 0. - || vCanvasCoordNorm.y < 0. - || vCanvasCoordNorm.x > 1. - || vCanvasCoordNorm.y > 1. ) { - // Outside the scene boundary or no elevation set: use the background texture. - gl_FragColor = tex; - } else { - // Adjust alpha to avoid extremely light alphas - // basically a gamma correction - float alphaAdj = pow(elevation.r, 1. / 2.2); - gl_FragColor = vec4(alphaAdj, 0., 0., alphaAdj); - } - } - `; - - /** @override */ - // Thanks to https://ptb.discord.com/channels/732325252788387980/734082399453052938/1009287977261879388 - apply(filterManager, input, output, clear, currentState) { - const { sceneX, sceneY } = canvas.dimensions; - this.uniforms.canvasMatrix ??= new PIXI.Matrix(); - this.uniforms.canvasMatrix.copyFrom(canvas.stage.worldTransform); - this.uniforms.canvasMatrix.invert(); - this.uniforms.canvasMatrix.translate(-sceneX, -sceneY); - return super.apply(filterManager, input, output, clear, currentState); - } -} - // NOTE: Testing elevation texture pixels /* api = game.modules.get("elevatedvision").api From c5025ee6cf6c12394da16529c06d03a30a4210be Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sat, 17 Jun 2023 13:05:19 -0700 Subject: [PATCH 02/12] Add min/max color settings --- languages/en.json | 7 +++++++ scripts/settings.js | 51 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/languages/en.json b/languages/en.json index fbd3bfed..247fec56 100644 --- a/languages/en.json +++ b/languages/en.json @@ -26,6 +26,13 @@ "elevatedvision.settings.add-fly-button.name": "Add Token Fly control", "elevatedvision.settings.add-fly-button.hint": "Add a control to the token toolbar that can be enabled or disabled to tell Elevated Vision when a token should be considered capable of flight. When the control is enabled, automatic token elevation will keep tokens above the ground when moved off a terrain or tile cliff greater than the token height.", + "elevatedvision.settings.color-min.name": "Elevation Minimum Color", + "elevatedvision.settings.color-min.hint": "Set the color used to display the minimum elevation (above the scene minimum) on the elevation layer.", + "elevatedvision.settings.color-max.name": "Elevation Maximum Color", + "elevatedvision.settings.color-max.hint": "Set the color used to display the maximum elevation on the elevation layer. Colors between min and max will be interpolated.", + "elevatedvision.settings.color-min.string_hint": "Set the color used to display the minimum elevation (above the scene minimum) on the elevation layer. Install the Color Picker module to get a color picker here.", + "elevatedvision.settings.color-max.string_hint": "Set the color used to display the maximum elevation on the elevation layer. Colors between min and max will be interpolated. Install the Color Picker module to get a color picker here.", + "elevatedvision.controls.fill-by-grid.name": "Fill by grid", "elevatedvision.controls.fill-by-los.name": "Fill by line-of-sight", "elevatedvision.controls.fill-space.name": "Fill space enclosed by walls", diff --git a/scripts/settings.js b/scripts/settings.js index 872e0d17..f8c8964d 100644 --- a/scripts/settings.js +++ b/scripts/settings.js @@ -23,6 +23,13 @@ export const SETTINGS = { } }, + COLOR: { + MIN: "color-min", + MAX: "color-max", + DEFAULT_MIN: "#FF0000FF", + DEFAULT_MAX: "#0000FFFF" + }, + VISION_USE_SHADER: "vision-use-shader", // Deprecated AUTO_ELEVATION: "auto-change-elevation", AUTO_AVERAGING: "auto-change-elevation.averaging", @@ -142,6 +149,50 @@ export function registerSettings() { requiresReload: true, type: Boolean }); + + if ( game.modules.get("color-picker")?.active ) { + ColorPicker.register(MODULE_ID, SETTINGS.COLOR.MIN, { + name: game.i18n.localize(`${MODULE_ID}.settings.${SETTINGS.COLOR.MIN}.name`), + hint: game.i18n.localize(`${MODULE_ID}.settings.${SETTINGS.COLOR.MIN}.hint`), + scope: "world", + config: true, + default: SETTINGS.COLOR.DEFAULT_MIN, + format: "hexa", + onChange: (value) => {}, // A callback function which triggers when the setting is changed + //insertAfter: "myModule.mySetting" // If supplied it will place the setting after the supplied setting + }); + + ColorPicker.register(MODULE_ID, SETTINGS.COLOR.MAX, { + name: game.i18n.localize(`${MODULE_ID}.settings.${SETTINGS.COLOR.MAX}.name`), + hint: game.i18n.localize(`${MODULE_ID}.settings.${SETTINGS.COLOR.MAX}.hint`), + scope: "world", + config: true, + default: SETTINGS.COLOR.DEFAULT_MAX, + format: "hexa", + onChange: (value) => {}, // A callback function which triggers when the setting is changed + }); + + } else { + game.settings.register(MODULE_ID, SETTINGS.COLOR.MIN, { + name: game.i18n.localize(`${MODULE_ID}.settings.${SETTINGS.COLOR.MIN}.name`), + hint: game.i18n.localize(`${MODULE_ID}.settings.${SETTINGS.COLOR.MIN}.string_hint`), + scope: "world", + config: true, + default: SETTINGS.COLOR.DEFAULT_MIN, + type: String, + requiresReload: false + }); + + game.settings.register(MODULE_ID, SETTINGS.COLOR.MAX, { + name: game.i18n.localize(`${MODULE_ID}.settings.${SETTINGS.COLOR.MAX}.name`), + hint: game.i18n.localize(`${MODULE_ID}.settings.${SETTINGS.COLOR.MAX}.string_hint`), + scope: "world", + config: true, + default: SETTINGS.COLOR.DEFAULT_MAX, + type: String, + requiresReload: false + }); + } } function setTokenCalculator() { From c1b2285eede4c585464be3e2f391e49c2a6fa327 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sat, 17 Jun 2023 13:06:10 -0700 Subject: [PATCH 03/12] Add a max static function for PixelCache Needed to calculate maximum elevation --- scripts/PixelCache.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/PixelCache.js b/scripts/PixelCache.js index 57fbab14..d885bd53 100644 --- a/scripts/PixelCache.js +++ b/scripts/PixelCache.js @@ -626,6 +626,7 @@ export class PixelCache extends PIXI.Rectangle { return sum / denom; } + total(shape, skip = 1) { const averageFn = PixelCache.averageFunction(); const denom = this.applyFunctionToShape(averageFn, shape, skip); @@ -652,6 +653,12 @@ export class PixelCache extends PIXI.Rectangle { return averageFn; } + static maxFunction() { + const maxFn = value => maxFn.max = Math.max(maxFn.max, value); + maxFn.max = Number.NEGATIVE_INFINITY; + return maxFn; + } + /** * Apply a function to each pixel value. * @param {function} fn Function to apply. Passed the pixel and the index. From 49be8e6692591423e9c9293b6e71a0d456ece305 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sat, 17 Jun 2023 13:07:08 -0700 Subject: [PATCH 04/12] Drop unused ready hook --- scripts/module.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scripts/module.js b/scripts/module.js index daf91139..43ca3f83 100644 --- a/scripts/module.js +++ b/scripts/module.js @@ -169,10 +169,6 @@ Hooks.once("setup", function() { registerPatches(); }); -Hooks.once("ready", function() { - log("ready"); -}); - Hooks.on("canvasInit", function(_canvas) { log("canvasInit"); registerShadowPatches(); From aaec3c510d9658aeb2dd70d65bb6fa48ae34f371 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sat, 17 Jun 2023 13:07:17 -0700 Subject: [PATCH 05/12] JSlint fixes --- scripts/ElevationTextureManager.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/ElevationTextureManager.js b/scripts/ElevationTextureManager.js index 282f76a2..0bdf9714 100644 --- a/scripts/ElevationTextureManager.js +++ b/scripts/ElevationTextureManager.js @@ -3,10 +3,8 @@ canvas, CONFIG, FilePicker, game, -ImageHelper, isNewerVersion, PIXI, -TextureExtractor, TextureLoader */ /* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */ From bd421cb7875593ab25e32bc9e193cb3c3753f626 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sat, 17 Jun 2023 13:10:41 -0700 Subject: [PATCH 06/12] elevation max getter and cached property --- scripts/ElevationLayer.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/scripts/ElevationLayer.js b/scripts/ElevationLayer.js index 65529c9c..5639e41b 100755 --- a/scripts/ElevationLayer.js +++ b/scripts/ElevationLayer.js @@ -303,6 +303,24 @@ export class ElevationLayer extends InteractionLayer { return this._scaleNormalizedElevation(this.#maximumNormalizedElevation); } + /** + * Current maximum elevation value for the scene. + * @type {number} + */ + #elevationCurrentMax; + + get elevationCurrentMax() { + return this.#elevationCurrentMax ?? (this.#elevationCurrentMax = this._calculateElevationCurrentMax()); + } + + /** + * Calculate the current maximum elevation value in the scene. + * @returns {number} + */ + _calculateElevationCurrentMax() { + return this.elevationPixelCache.applyFunctionToShape(PixelCache.maxFunction); + } + /* ------------------------ */ /** From 5b8807fea75c1db646b82614761e190d3f8c5883 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sat, 17 Jun 2023 13:30:56 -0700 Subject: [PATCH 07/12] Correct current max and improve speed For loop is by far the fastest approach in benchmarking. --- scripts/ElevationLayer.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/ElevationLayer.js b/scripts/ElevationLayer.js index 5639e41b..033b744d 100755 --- a/scripts/ElevationLayer.js +++ b/scripts/ElevationLayer.js @@ -318,7 +318,12 @@ export class ElevationLayer extends InteractionLayer { * @returns {number} */ _calculateElevationCurrentMax() { - return this.elevationPixelCache.applyFunctionToShape(PixelCache.maxFunction); + // Reduce is slow, so do this the hard way. + let max = Number.NEGATIVE_INFINITY; + const pix = this.elevationPixelCache.pixels; + const ln = pix.length; + for ( let i = 0; i < ln; i += 1 ) max = Math.max(max, pix[i]); + return max; } /* ------------------------ */ From 5b4f5382acbc4e938c1ef252a9d371b6233e1fb6 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sat, 17 Jun 2023 15:55:57 -0700 Subject: [PATCH 08/12] Update or clear current max --- scripts/ElevationLayer.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/scripts/ElevationLayer.js b/scripts/ElevationLayer.js index 033b744d..8d580c6f 100755 --- a/scripts/ElevationLayer.js +++ b/scripts/ElevationLayer.js @@ -822,6 +822,9 @@ export class ElevationLayer extends InteractionLayer { pixels[i + 1] = newPixelChannels.g; } + // Reset the elevation maximum, b/c we don't know this value anymore. + this.#elevationCurrentMax = undefined; + // This makes vertical lines: newTex = PIXI.Texture.fromBuffer(pixels, width, height) const br = new PIXI.BufferResource(pixels, {width, height}); const bt = new PIXI.BaseTexture(br); @@ -868,6 +871,10 @@ export class ElevationLayer extends InteractionLayer { } } + // Update the elevation maximum. + if ( this.#elevationCurrentMax === from ) this.#elevationCurrentMax = undefined; + else this.#elevationCurrentMax = Math.max(this.#elevationCurrentMax, to); + // Error Makes vertical lines: // newTex = PIXI.Texture.fromBuffer(pixels, width, height) const br = new PIXI.BufferResource(pixels, {width, height}); @@ -876,6 +883,7 @@ export class ElevationLayer extends InteractionLayer { // Save to the background texture (used by the background sprite, like with saved images) this.#replaceBackgroundElevationTexture(newTex); + } /** @@ -896,6 +904,9 @@ export class ElevationLayer extends InteractionLayer { const graphics = this._graphicsContainer.addChild(new PIXI.Graphics()); const color = this.elevationColor(elevation); + // Update the elevation maximum. + this.#elevationCurrentMax = Math.max(this.#elevationCurrentMax, elevation); + // Set width = 0 to avoid drawing a border line. The border line will use antialiasing // and that causes a lighter-color border to appear outside the shape. const draw = new Draw(graphics); @@ -953,8 +964,10 @@ export class ElevationLayer extends InteractionLayer { * @returns {PIXI.Graphics} The child graphics added to the _graphicsContainer */ fillLOS(origin, elevation = 0, { type = "light"} = {}) { - const los = CONFIG.Canvas.polygonBackends[type].create(origin, { type }); + // Update the elevation maximum. + this.#elevationCurrentMax = Math.max(this.#elevationCurrentMax, elevation); + const los = CONFIG.Canvas.polygonBackends[type].create(origin, { type }); const graphics = this._graphicsContainer.addChild(new PIXI.Graphics()); const draw = new Draw(graphics); const color = this.elevationColor(elevation); @@ -1011,6 +1024,9 @@ export class ElevationLayer extends InteractionLayer { return; } + // Update the elevation maximum. + this.#elevationCurrentMax = Math.max(this.#elevationCurrentMax, elevation); + // Create the graphics representing the fill! const graphics = this._graphicsContainer.addChild(new PIXI.Graphics()); drawPolygonWithHoles(polys, { graphics, fillColor: this.elevationColor(elevation) }); From 35881afc730eebf3f668c6d594e14d3996d65296 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sat, 17 Jun 2023 20:55:42 -0700 Subject: [PATCH 09/12] Working two-tone coloration, more or less Still haven't found a magical formula to make it always look great. --- scripts/ElevationLayer.js | 54 ++++-- scripts/ElevationLayerShader.js | 330 +++++++++++++++++++++++++------- scripts/module.js | 4 +- scripts/settings.js | 25 ++- 4 files changed, 314 insertions(+), 99 deletions(-) diff --git a/scripts/ElevationLayer.js b/scripts/ElevationLayer.js index 8d580c6f..55919ddd 100755 --- a/scripts/ElevationLayer.js +++ b/scripts/ElevationLayer.js @@ -1,5 +1,4 @@ /* globals -AbstractBaseFilter, canvas, CONFIG, Dialog, @@ -36,7 +35,7 @@ import { CoordinateElevationCalculator } from "./CoordinateElevationCalculator.j import { TokenPointElevationCalculator } from "./TokenPointElevationCalculator.js"; import { TokenAverageElevationCalculator } from "./TokenAverageElevationCalculator.js"; import { TravelElevationCalculator } from "./TravelElevationCalculator.js"; -import { ElevationLayerShader } from "./ElevationLayerShader.js"; +import { EVQuadMesh, ElevationLayerShader } from "./ElevationLayerShader.js"; import { ElevationTextureManager } from "./ElevationTextureManager.js"; @@ -71,7 +70,9 @@ On canvas: */ // TODO: What should replace this now that FullCanvasContainer is deprecated in v11? -class FullCanvasContainer extends FullCanvasObjectMixin(PIXI.Container) {} +class FullCanvasContainer extends FullCanvasObjectMixin(PIXI.Container) { + +} export function _onMouseMoveCanvas(wrapper, event) { wrapper(event); @@ -323,7 +324,16 @@ export class ElevationLayer extends InteractionLayer { const pix = this.elevationPixelCache.pixels; const ln = pix.length; for ( let i = 0; i < ln; i += 1 ) max = Math.max(max, pix[i]); - return max; + return this._scaleNormalizedElevation(max); + } + + /** + * Update the current elevation maximum to a specific value. + * @param {number} e Elevation value + */ + _updateElevationCurrentMax(e) { + this.#elevationCurrentMax = Math.max(this.#elevationCurrentMax, e); + this._elevationColorsMesh.shader.updateMaxCurrentElevation(); } /* ------------------------ */ @@ -566,10 +576,13 @@ export class ElevationLayer extends InteractionLayer { // Add the sprite that holds the default background elevation settings this._graphicsContainer.addChild(this._backgroundElevation); - // Add the elevation color mesh - this._elevationColorsMesh = new ElevationLayerShader(); await this.loadSceneElevationData(); + + // Add the elevation color mesh + const shader = ElevationLayerShader.create(); + this._elevationColorsMesh = new EVQuadMesh(canvas.dimensions.sceneRect, shader); + this.renderElevation(); this._initialized = true; @@ -873,7 +886,7 @@ export class ElevationLayer extends InteractionLayer { // Update the elevation maximum. if ( this.#elevationCurrentMax === from ) this.#elevationCurrentMax = undefined; - else this.#elevationCurrentMax = Math.max(this.#elevationCurrentMax, to); + else this._updateElevationCurrentMax(to); // Error Makes vertical lines: // newTex = PIXI.Texture.fromBuffer(pixels, width, height) @@ -903,9 +916,7 @@ export class ElevationLayer extends InteractionLayer { const shape = useHex ? this._hexGridShape(p) : this._squareGridShape(p); const graphics = this._graphicsContainer.addChild(new PIXI.Graphics()); const color = this.elevationColor(elevation); - - // Update the elevation maximum. - this.#elevationCurrentMax = Math.max(this.#elevationCurrentMax, elevation); + this._updateElevationCurrentMax(elevation); // Set width = 0 to avoid drawing a border line. The border line will use antialiasing // and that causes a lighter-color border to appear outside the shape. @@ -964,9 +975,7 @@ export class ElevationLayer extends InteractionLayer { * @returns {PIXI.Graphics} The child graphics added to the _graphicsContainer */ fillLOS(origin, elevation = 0, { type = "light"} = {}) { - // Update the elevation maximum. - this.#elevationCurrentMax = Math.max(this.#elevationCurrentMax, elevation); - + this._updateElevationCurrentMax(elevation); const los = CONFIG.Canvas.polygonBackends[type].create(origin, { type }); const graphics = this._graphicsContainer.addChild(new PIXI.Graphics()); const draw = new Draw(graphics); @@ -1024,8 +1033,7 @@ export class ElevationLayer extends InteractionLayer { return; } - // Update the elevation maximum. - this.#elevationCurrentMax = Math.max(this.#elevationCurrentMax, elevation); + this._updateElevationCurrentMax(elevation); // Create the graphics representing the fill! const graphics = this._graphicsContainer.addChild(new PIXI.Graphics()); @@ -1150,6 +1158,7 @@ export class ElevationLayer extends InteractionLayer { if ( !g ) return; this._graphicsContainer.removeChild(g); g.destroy(); + this.#elevationCurrentMax = undefined; this._requiresSave = true; this.renderElevation(); } @@ -1158,9 +1167,16 @@ export class ElevationLayer extends InteractionLayer { * Remove all elevation data from the scene. */ async clearElevationData() { - this.#destroy(); + this._clearElevationPixelCache(); + this._backgroundElevation.destroy(); + this._backgroundElevation = PIXI.Sprite.from(PIXI.Texture.EMPTY); + + this._graphicsContainer.destroy({children: true}); + this._graphicsContainer = new PIXI.Container(); + await canvas.scene.unsetFlag(MODULE_ID, FLAGS.ELEVATION_IMAGE); this._requiresSave = false; + this.#elevationCurrentMax = 0; this.renderElevation(); } @@ -1175,6 +1191,8 @@ export class ElevationLayer extends InteractionLayer { this._graphicsContainer.destroy({children: true}); this._graphicsContainer = new PIXI.Container(); + + this._elevationTexture?.destroy(); } /* -------------------------------------------- */ @@ -1206,6 +1224,10 @@ export class ElevationLayer extends InteractionLayer { this.container.removeChild(this._elevationColorsMesh); } + _updateMinColor() { + this._elevationColorsMesh.shader.updateMinColor(); + } + /** * Draw wall segments */ diff --git a/scripts/ElevationLayerShader.js b/scripts/ElevationLayerShader.js index 945fe44a..6378c8d6 100644 --- a/scripts/ElevationLayerShader.js +++ b/scripts/ElevationLayerShader.js @@ -1,71 +1,83 @@ /* global canvas, +Color, +foundry, +mergeObject, PIXI */ "use strict"; -export class EVQuadMesh extends PIXI.Mesh { - /** - * Vertex shader constructs a quad and - * calculates the canvas coordinate and texture coordinate varyings. - */ - static vertexShader = -` -#version 300 es -precision ${PIXI.settings.PRECISION_VERTEX} float; +import { getSetting, SETTINGS } from "./settings.js"; -in vec2 aVertexPosition; -in vec2 aTextureCoord; +class AbstractEVShader extends PIXI.Shader { + constructor(program, uniforms) { + super(program, foundry.utils.deepClone(uniforms)); -out vec2 vVertexPosition; -out vec2 vTextureCoord; + /** + * The initial default values of shader uniforms + * @type {object} + */ + this._defaults = uniforms; + } -uniform mat3 translationMatrix; -uniform mat3 projectionMatrix; - -void main() { - vVertexPosition = aVertexPosition; - vTextureCoord = aTextureCoord; - gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); -}`; + /* -------------------------------------------- */ /** - * Fragment shader intended to be overriden by subclass. + * The raw vertex shader used by this class. + * A subclass of AbstractBaseShader must implement the vertexShader static field. + * @type {string} */ - static fragmentShader = -`#version 300 es -precision ${PIXI.settings.PRECISION_FRAGMENT} float; -precision ${PIXI.settings.PRECISION_FRAGMENT} usampler2D; + static vertexShader = ""; -in vec2 vVertexPosition; -in vec2 vTextureCoord; + /** + * The raw fragment shader used by this class. + * A subclass of AbstractBaseShader must implement the fragmentShader static field. + * @type {string} + */ + static fragmentShader = ""; -out vec4 fragColor; + /** + * The default uniform values for the shader. + * A subclass of AbstractBaseShader must implement the defaultUniforms static field. + * @type {object} + */ + static defaultUniforms = {}; -void main() { - fragColor = vec4(0.0); -}`; + /* -------------------------------------------- */ - static defaultUniforms = {}; + /** + * A factory method for creating the shader using its defined default values + * @param {object} defaultUniforms + * @returns {AbstractBaseShader} + */ + static create(defaultUniforms) { + const program = PIXI.Program.from(this.vertexShader, this.fragmentShader); + const uniforms = mergeObject(this.defaultUniforms, defaultUniforms, {inplace: false, insertKeys: false}); + return new this(program, uniforms); + } - constructor(rect, { uniforms, vertexShader, fragmentShader, state, drawMode } = {}) { - // Determine default parameters for the mesh. - const geometry = EVQuadMesh.calculateQuadGeometry(rect); - uniforms ??= {}; - vertexShader ??= EVQuadMesh.vertexShader; - fragmentShader ??= EVQuadMesh.fragmentShader; - state ??= new PIXI.State(); - drawMode ??= PIXI.DRAW_MODES.TRIANGLES; + /* -------------------------------------------- */ - // Create shader - const shader = PIXI.Shader.from(vertexShader, fragmentShader, uniforms); + /** + * Reset the shader uniforms back to their provided default values + * @private + */ + reset() { + for (let [k, v] of Object.entries(this._defaults)) { + this.uniforms[k] = v; + } + } +} - // Create the mesh +/** + * Mesh that takes a rectangular frame instead of a geometry. + * @param {PIXI.Rectangle} rect + */ +export class EVQuadMesh extends PIXI.Mesh { + constructor(rect, shader, state, drawMode) { + const geometry = EVQuadMesh.calculateQuadGeometry(rect); super(geometry, shader, state, drawMode); - - // Store parameters this.rect = rect; - this.uniforms = uniforms; } /** @@ -97,7 +109,34 @@ void main() { } } -export class ElevationLayerShader extends EVQuadMesh { +/** + * Shader to represent elevation values on the elevation layer canvas. + */ +export class ElevationLayerShader extends AbstractEVShader { + /** + * Vertex shader constructs a quad and calculates the canvas coordinate and texture coordinate varyings. + * @type {string} + */ + static vertexShader = +` +#version 300 es +precision ${PIXI.settings.PRECISION_VERTEX} float; + +in vec2 aVertexPosition; +in vec2 aTextureCoord; + +out vec2 vVertexPosition; +out vec2 vTextureCoord; + +uniform mat3 translationMatrix; +uniform mat3 projectionMatrix; + +void main() { + vVertexPosition = aVertexPosition; + vTextureCoord = aTextureCoord; + gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); +}`; + static fragmentShader = `#version 300 es precision ${PIXI.settings.PRECISION_FRAGMENT} float; @@ -110,48 +149,193 @@ out vec4 fragColor; uniform sampler2D uTerrainSampler; // Elevation Texture uniform vec4 uElevationRes; +uniform vec4 uMinColor; +uniform vec4 uMaxColor; +uniform float uMaxNormalizedElevation; + +/** + * Convert a Hue-Saturation-Brightness color to RGB - useful to convert polar coordinates to RGB + * See BaseShaderMixin.HSB2RGB + * @type {string} + */ +vec3 hsb2rgb(in vec3 c) { + vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0,4.0,2.0), 6.0)-3.0)-1.0, 0.0, 1.0 ); + rgb = rgb*rgb*(3.0-2.0*rgb); + return c.z * mix(vec3(1.0), rgb, c.y); +} + +/** + * From https://stackoverflow.com/questions/15095909/from-rgb-to-hsv-in-opengl-glsl + * @param {vec3} c RGB color representation (0–1) + * @returns {vec3} HSV color representation (0–1) + */ +// All components are in the range [0…1], including hue. +vec3 rgb2hsv(in vec3 c) { + vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); + vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); + + float d = q.x - min(q.w, q.y); + float e = 1.0e-10; + return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); +} + +/** + * From https://www.shadertoy.com/view/XljGzV. + * @param {vec3} c HSV color representation (0–1) + * @returns {vec3} RGB color representation (0–1) + */ +// All components are in the range [0…1], including hue. +vec3 hsv2rgb(in vec3 c) { + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); +} + +/** + * Return the normalized elevation value for a given color representation. + * @param {vec4} pixel Color representation of elevation value on canvas + * @returns {float} The normalized elevation value, between 0 and 65,536. + */ +float decodeElevationChannels(in vec4 color) { + color = color * 255.0; + return (color.g * 256.0) + color.r; +} + +/** + * Return the scaled elevation value for a given normalized value. + * @param {float} value The normalized elevation between 0 and 65,536 + * @returns {float} Scaled elevation value based on scene settings, in grid units + */ +float scaleNormalizedElevation(in float value) { + float elevationMin = uElevationRes.r; + float elevationStep = uElevationRes.g; + return elevationMin + (round(value * elevationStep * 10.0) * 0.1); +} /** - * Calculate the canvas elevation given a pixel value. - * Currently uses only the red channel. - * Maps 0–1 red channel to elevation in canvas coordinates. - * r: elevation min; g: elevation step; b: max pixel value (likely 255); a: canvas size / distance - * uElevationRes = [elevationMin, elevationStep, maximumPixelValue, elevationMult]; + * Convert grid to pixel units. + * @param {float} value Number, in grid units + * @returns {float} The equivalent number in pixel units based on grid distance */ -float canvasElevationFromPixel(in vec4 pixel) { - float value = (pixel.g * 255.0) + pixel.r; - return (uElevationRes.r + (value * uElevationRes.b * uElevationRes.g)) * uElevationRes.a; +float gridUnitsToPixels(in float value) { + float distancePixels = uElevationRes.a; + return value * distancePixels; +} + +/** + * Convert a color pixel to a scaled elevation value, in pixel units. + */ +float colorToElevationPixelUnits(in vec4 color) { + float e = decodeElevationChannels(color); + e = scaleNormalizedElevation(e); + return gridUnitsToPixels(e); } /** * Determine the color for a given elevation value. * Currently draws increasing shades of red with a gamma correction to avoid extremely light alpha. - * Currently takes the elevation pixel, not the elevation canvas value. */ -vec4 colorForElevationPixel(vec4 elevation) { - float alphaAdj = pow(elevation.r, 1. / 2.2); - return vec4(alphaAdj, 0., 0., alphaAdj); +vec4 colorForElevation(float eNorm) { + // Linear mix of the two HSV colors and alpha. + // Skipping 0, so one less entry + float maxNorm = max(1.0, uMaxNormalizedElevation - 1.0); + vec4 color = mix(uMinColor, uMaxColor, (eNorm - 1.0) / maxNorm); + + // If using hsv + // color.rgb = hsv2rgb(color.rgb); + + // Gamma correction to avoid extremely light alpha? + // color.a = pow(color.a, 1. / 2.2); + + // Gamma correct alpha and colors? + // color = pow(color, vec4(1. / 2.2)); + + return color; } void main() { // Terrain is sized to the scene. vec4 terrainPixel = texture(uTerrainSampler, vTextureCoord); - float elevation = canvasElevationFromPixel(terrainPixel); - fragColor = colorForElevationPixel(terrainPixel); + float eNorm = decodeElevationChannels(terrainPixel); + fragColor = eNorm == 0.0 ? vec4(0.0) : colorForElevation(eNorm); }`; - constructor() { - const uniforms = { - uElevationRes: [ - canvas.elevation.elevationMin, - canvas.elevation.elevationStep, - canvas.elevation.maximumPixelValue, - canvas.dimensions.distancePixels + static defaultUniforms = { + uElevationRes: [ + 0, + 1, + 256 * 256, + 1 ], - uTerrainSampler: canvas.elevation._elevationTexture - }; + uTerrainSampler: 0, + uMinColor: [1, 0, 0, 1], + uMaxColor: [0, 0, 1, 1], + uMaxNormalizedElevation: 65536 + }; + + static create(defaultUniforms = {}) { + const ev = canvas.elevation; + defaultUniforms.uElevationRes ??= [ + ev.elevationMin, + ev.elevationStep, + ev.elevationMax, + // canvas.elevation.maximumPixelValue, + canvas.dimensions.distancePixels + ]; + defaultUniforms.uTerrainSampler = canvas.elevation._elevationTexture; + defaultUniforms.uMinColor = this.getDefaultColorArray("MIN"); + defaultUniforms.uMaxColor = this.getDefaultColorArray("MAX"); + defaultUniforms.uMaxNormalizedElevation = ev._normalizeElevation(ev.elevationCurrentMax); + + return super.create(defaultUniforms); + } - const fragmentShader = ElevationLayerShader.fragmentShader; - super(canvas.dimensions.sceneRect, { uniforms, fragmentShader }); + /** + * Update the minimum color uniform. + * @param {string} newColorHex + */ + updateMinColor(newColorHex) { + this.uniforms.uMinColor = this.constructor.getColorArray(newColorHex); + } + + /** + * Update the maximum color uniform. + * @param {string} newColorHex + */ + updateMaxColor(newColorHex) { + this.uniforms.uMaxColor = this.constructor.getColorArray(newColorHex); + } + + /** + * Update the maximum elevation value. + * @param {number} + */ + updateMaxCurrentElevation() { + this.uniforms.uMaxNormalizedElevation = canvas.elevation._normalizeElevation(canvas.elevation.elevationCurrentMax); + } + + /** + * Return the current color setting as a 4-element array. + * @param {string} type MIN or MAX + * @returns {number[4]} + */ + static getDefaultColorArray(type = "MIN") { + const hex = getSetting(SETTINGS.COLOR[type]); + return this.getColorArray(hex); + } + + /** + * Return the color array for a given hex. + * @param {string} hex Hex value for color with alpha + * @returns {number[4]} + */ + static getColorArray(hex) { + const startIdx = hex.startsWith("#") ? 1 : 0; + const hexColor = hex.substring(startIdx, startIdx + 6); + const hexAlpha = hex.substring(startIdx + 6); + const alpha = parseInt(hexAlpha, 16) / 255; + const c = Color.fromString(hexColor); + return [...c.rgb, alpha]; } } diff --git a/scripts/module.js b/scripts/module.js index 43ca3f83..0430fd14 100644 --- a/scripts/module.js +++ b/scripts/module.js @@ -22,6 +22,7 @@ import { PixelCache, TilePixelCache } from "./PixelCache.js"; import { CoordinateElevationCalculator } from "./CoordinateElevationCalculator.js"; import { TokenPointElevationCalculator } from "./TokenPointElevationCalculator.js"; import { TokenAverageElevationCalculator } from "./TokenAverageElevationCalculator.js"; +import { ElevationLayerShader } from "./ElevationLayerShader.js"; // Register methods, patches, settings import { registerAdditions, registerPatches, registerShadowPatches } from "./patching.js"; @@ -147,7 +148,8 @@ Hooks.once("init", function() { TilePixelCache, CoordinateElevationCalculator, TokenPointElevationCalculator, - TokenAverageElevationCalculator + TokenAverageElevationCalculator, + ElevationLayerShader }; // These methods need to be registered early diff --git a/scripts/settings.js b/scripts/settings.js index f8c8964d..f3a505bd 100644 --- a/scripts/settings.js +++ b/scripts/settings.js @@ -1,4 +1,8 @@ /* globals +canvas, +ColorPicker, +CONFIG, +CONST, game */ "use strict"; @@ -19,15 +23,15 @@ export const SETTINGS = { LABELS: { "shading-none": `${MODULE_ID}.shading-none`, "shading-polygons": `${MODULE_ID}.shading-polygons`, - "shading-webgl": `${MODULE_ID}.shading-webgl`, + "shading-webgl": `${MODULE_ID}.shading-webgl` } }, COLOR: { MIN: "color-min", MAX: "color-max", - DEFAULT_MIN: "#FF0000FF", - DEFAULT_MAX: "#0000FFFF" + DEFAULT_MIN: "#3C3E6431", + DEFAULT_MAX: "#C22121BB" }, VISION_USE_SHADER: "vision-use-shader", // Deprecated @@ -158,8 +162,8 @@ export function registerSettings() { config: true, default: SETTINGS.COLOR.DEFAULT_MIN, format: "hexa", - onChange: (value) => {}, // A callback function which triggers when the setting is changed - //insertAfter: "myModule.mySetting" // If supplied it will place the setting after the supplied setting + mode: 'HVS', + onChange: value => canvas.elevation._elevationColorsMesh.shader.updateMinColor(value) }); ColorPicker.register(MODULE_ID, SETTINGS.COLOR.MAX, { @@ -169,7 +173,8 @@ export function registerSettings() { config: true, default: SETTINGS.COLOR.DEFAULT_MAX, format: "hexa", - onChange: (value) => {}, // A callback function which triggers when the setting is changed + mode: 'HVS', + onChange: value => canvas.elevation._elevationColorsMesh.shader.updateMaxColor(value) }); } else { @@ -180,7 +185,8 @@ export function registerSettings() { config: true, default: SETTINGS.COLOR.DEFAULT_MIN, type: String, - requiresReload: false + requiresReload: false, + onChange: value => canvas.elevation._elevationColorsMesh.shader.updateMinColor(value) }); game.settings.register(MODULE_ID, SETTINGS.COLOR.MAX, { @@ -190,14 +196,15 @@ export function registerSettings() { config: true, default: SETTINGS.COLOR.DEFAULT_MAX, type: String, - requiresReload: false + requiresReload: false, + onChange: value => canvas.elevation._elevationColorsMesh.shader.updateMaxColor(value) }); } } function setTokenCalculator() { canvas.elevation.TokenElevationCalculator = getSetting(SETTINGS.AUTO_AVERAGING) - ? TokenAverageElevationCalculator : TokenPointElevationCalculator; + ? TokenAverageElevationCalculator : TokenPointElevationCalculator; } /** From 08642004edb73374ff1a3737ae89923df224ce3e Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sat, 17 Jun 2023 21:15:50 -0700 Subject: [PATCH 10/12] Pick settings close to original and use gamma correction --- scripts/ElevationLayerShader.js | 2 +- scripts/settings.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/ElevationLayerShader.js b/scripts/ElevationLayerShader.js index 6378c8d6..d33364f9 100644 --- a/scripts/ElevationLayerShader.js +++ b/scripts/ElevationLayerShader.js @@ -249,7 +249,7 @@ vec4 colorForElevation(float eNorm) { // color.a = pow(color.a, 1. / 2.2); // Gamma correct alpha and colors? - // color = pow(color, vec4(1. / 2.2)); + color = pow(color, vec4(1. / 2.2)); return color; } diff --git a/scripts/settings.js b/scripts/settings.js index f3a505bd..3224e2f9 100644 --- a/scripts/settings.js +++ b/scripts/settings.js @@ -30,8 +30,8 @@ export const SETTINGS = { COLOR: { MIN: "color-min", MAX: "color-max", - DEFAULT_MIN: "#3C3E6431", - DEFAULT_MAX: "#C22121BB" + DEFAULT_MIN: "#03000003", + DEFAULT_MAX: "#80000080" }, VISION_USE_SHADER: "vision-use-shader", // Deprecated From 4cbe75e32dc75709282282e3c83e119f21075ed4 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 18 Jun 2023 09:08:56 -0700 Subject: [PATCH 11/12] Update changelog --- Changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Changelog.md b/Changelog.md index 1c7bf28e..bf2f6813 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,6 @@ +# 0.5.2 +GM can define a minimum and maximum elevation color in settings. The coloration in the elevation layer will be interpolated between the two colors, based on the scene minimum and the current maximum elevation in the scene. + # 0.5.1 Elevation can now handle up to 65,536 distinct values. This is accomplished by utilizing both red and green channels of the elevation image. From c7eada1a33893239ba6f39c91d8e830631c5a1d2 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 18 Jun 2023 09:48:58 -0700 Subject: [PATCH 12/12] Update changelog dialog --- scripts/changelog.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/scripts/changelog.js b/scripts/changelog.js index 6b2b9771..342bb40f 100644 --- a/scripts/changelog.js +++ b/scripts/changelog.js @@ -72,6 +72,23 @@ Hooks.once("ready", () => { worlds/[world-id]/assets/elevatedvision/[world-id][scene-id]-elevationMap.webp.` }) + .addEntry({ + version: "0.5.2", + title: "Elevation colors", + body: `\ + GM can define a minimum and maximum elevation color in settings. The coloration in the elevation layer + will be interpolated between the two colors, based on the scene minimum and the current maximum elevation + in the scene. Alpha (transparency) values will also be interpolated. + + Install the [Color Picker](https://foundryvtt.com/packages/color-picker) module to get + a color picker for the minimum/maximum elevation settings. + + Any of you graphic experts out there, feel free to suggest improvements to how to best represent + elevation gradients on a scene! Or share your preferred min/max color choices. There is an open + comment on the Github page regarding "Multicolor height indicators." I will leave it open in case + anyone has additional suggestions.` + }) + .build() ?.render(true); });