From 336eea514808dcab290247310c9da767ead2a9a7 Mon Sep 17 00:00:00 2001 From: Johannes Loher Date: Tue, 26 Jul 2022 10:10:34 +0200 Subject: [PATCH] feat: add fog filter (experimental, might change at any time, use at your own risk) Closes #67 --- package-lock.json | 51 +++++++ package.json | 1 + rollup.config.js | 4 + src/filter-effects/filter-effects-db.js | 4 +- src/filter-effects/filters/fog.js | 134 +++++++++++++++--- .../filters/mixins/fading-filter.js | 17 ++- src/filter-effects/filters/mixins/filter.js | 10 +- .../filters/shaders/custom-vertex-2d.vert | 22 +++ .../filters/shaders/customvertex2D.js | 37 ----- src/filter-effects/filters/shaders/fog.frag | 83 +++++++++++ src/filter-effects/filters/shaders/fog.js | 97 ------------- 11 files changed, 302 insertions(+), 158 deletions(-) create mode 100644 src/filter-effects/filters/shaders/custom-vertex-2d.vert delete mode 100644 src/filter-effects/filters/shaders/customvertex2D.js create mode 100644 src/filter-effects/filters/shaders/fog.frag delete mode 100644 src/filter-effects/filters/shaders/fog.js diff --git a/package-lock.json b/package-lock.json index e404fcd1..9dada2b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "rollup": "2.78.1", "rollup-plugin-livereload": "2.0.5", "rollup-plugin-sourcemaps": "0.6.3", + "rollup-plugin-string": "3.0.0", "rollup-plugin-styles": "4.0.0", "rollup-plugin-terser": "7.0.2", "standard-version": "9.5.0", @@ -5861,6 +5862,15 @@ } } }, + "node_modules/rollup-plugin-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-string/-/rollup-plugin-string-3.0.0.tgz", + "integrity": "sha512-vqyzgn9QefAgeKi+Y4A7jETeIAU1zQmS6VotH6bzm/zmUQEnYkpIGRaOBPY41oiWYV4JyBoGAaBjYMYuv+6wVw==", + "dev": true, + "dependencies": { + "rollup-pluginutils": "^2.4.1" + } + }, "node_modules/rollup-plugin-styles": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/rollup-plugin-styles/-/rollup-plugin-styles-4.0.0.tgz", @@ -5927,6 +5937,21 @@ "rollup": "^2.0.0" } }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/rollup-pluginutils/node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -11347,6 +11372,15 @@ "source-map-resolve": "^0.6.0" } }, + "rollup-plugin-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-string/-/rollup-plugin-string-3.0.0.tgz", + "integrity": "sha512-vqyzgn9QefAgeKi+Y4A7jETeIAU1zQmS6VotH6bzm/zmUQEnYkpIGRaOBPY41oiWYV4JyBoGAaBjYMYuv+6wVw==", + "dev": true, + "requires": { + "rollup-pluginutils": "^2.4.1" + } + }, "rollup-plugin-styles": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/rollup-plugin-styles/-/rollup-plugin-styles-4.0.0.tgz", @@ -11403,6 +11437,23 @@ "terser": "^5.0.0" } }, + "rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1" + }, + "dependencies": { + "estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + } + } + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", diff --git a/package.json b/package.json index 03c0a3db..f22aedd9 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "rollup": "2.78.1", "rollup-plugin-livereload": "2.0.5", "rollup-plugin-sourcemaps": "0.6.3", + "rollup-plugin-string": "3.0.0", "rollup-plugin-styles": "4.0.0", "rollup-plugin-terser": "7.0.2", "standard-version": "9.5.0", diff --git a/rollup.config.js b/rollup.config.js index 7558ef40..68f61c41 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -2,6 +2,7 @@ import copy from "@guanghechen/rollup-plugin-copy"; import livereload from "rollup-plugin-livereload"; import sourcemaps from "rollup-plugin-sourcemaps"; import styles from "rollup-plugin-styles"; +import { string } from "rollup-plugin-string"; import { terser } from "rollup-plugin-terser"; import { distDirectory, name, sourceDirectory } from "./tools/const.mjs"; @@ -40,6 +41,9 @@ const config = { sourceMap: true, minimize: isProduction, }), + string({ + include: [`${sourceDirectory}/**/*.frag`, `${sourceDirectory}/**/*.vert`], + }), copy({ verbose: true, targets: [{ src: staticFiles, dest: distDirectory }], diff --git a/src/filter-effects/filter-effects-db.js b/src/filter-effects/filter-effects-db.js index b793055d..da3f02b2 100644 --- a/src/filter-effects/filter-effects-db.js +++ b/src/filter-effects/filter-effects-db.js @@ -1,6 +1,6 @@ import { BloomFilter } from "./filters/bloom.js"; import { ColorFilter } from "./filters/color.js"; -// import { FogFilter } from "./filters/fog.js"; +import { FogFilter } from "./filters/fog.js"; import { LightningFilter } from "./filters/lightning.js"; import { OldFilmFilter } from "./filters/old-film.js"; import { PredatorFilter } from "./filters/predator.js"; @@ -12,7 +12,7 @@ import { UnderwaterFilter } from "./filters/underwater.js"; export const filterEffects = { bloom: BloomFilter, color: ColorFilter, - // fog: FXFogFilter + fog: FogFilter, lightning: LightningFilter, oldfilm: OldFilmFilter, predator: PredatorFilter, diff --git a/src/filter-effects/filters/fog.js b/src/filter-effects/filters/fog.js index 06afd471..c4f7fdc1 100644 --- a/src/filter-effects/filters/fog.js +++ b/src/filter-effects/filters/fog.js @@ -1,45 +1,139 @@ -import { fog } from "./shaders/fog.js"; -import { customVertex2D } from "./shaders/customvertex2D.js"; -import { FXMasterFilterEffectMixin } from "./mixins/filter.js"; +import fog from "./shaders/fog.frag"; +import customVertex2D from "./shaders/custom-vertex-2d.vert"; +import { FadingFilterMixin } from "./mixins/fading-filter.js"; -export class FogFilter extends FXMasterFilterEffectMixin(PIXI.Filter) { +export class FogFilter extends FadingFilterMixin(PIXI.Filter) { constructor(options, id) { super(options, id, customVertex2D, fog); - this.color = new Float32Array([1.0, 0.4, 0.1, 0.55]); - this.dimensions = new Float32Array([1.0, 1.0]); - this.time = 0.0; - this.density = 0.65; + this.uniforms.time = 0.0; + this.uniforms.dimensions = new Float32Array([0.0, 0.0]); + this.uniforms.color = new Float32Array([0.0, 0.0, 0.0, 1.0]); } + /** @type {number} */ + lastTick; + /** @override */ static label = "FXMASTER.FilterEffectFog"; /** @override */ static icon = "fas fa-cloud"; - apply(filterManager, input, output, clear) { - this.uniforms.color = this.color; - this.uniforms.dimensions = this.dimensions; - this.uniforms.time = this.time; - this.uniforms.density = this.density; - this.uniforms.dimensions = this.dimensions; + /** @override */ + static get parameters() { + return { + dimensions: { + label: "FXMASTER.Scale", + type: "range", + max: 5, + min: 0, + step: 0.1, + value: 1, + skipInitialAnimation: true, + }, + speed: { + label: "FXMASTER.Speed", + type: "range", + max: 5, + min: 0, + step: 0.1, + value: 1, + skipInitialAnimation: true, + }, + density: { + label: "FXMASTER.Density", + type: "range", + max: 1, + min: 0, + step: 0.05, + value: 0.65, + }, + color: { + label: "FXMASTER.Tint", + type: "color", + value: { + value: "#000000", + apply: false, + }, + skipInitialAnimation: true, + }, + }; + } - filterManager.applyFilter(this, input, output, clear); + /** @override */ + static get neutral() { + return { + density: 0, + }; + } + + /** @type {number} */ + get r() { + return this.uniforms.color[0]; + } + set r(value) { + this.uniforms.color[0] = value; + } + + /** @type {number} */ + get g() { + return this.uniforms.color[1]; + } + set g(value) { + this.uniforms.color[1] = value; + } + + /** @type {number} */ + get b() { + return this.uniforms.color[2]; + } + set b(value) { + this.uniforms.color[2] = value; + } + + /** @type {number} */ + get density() { + return this.uniforms.density; + } + set density(value) { + this.uniforms.density = value; + } + + /** @type {number} */ + get dimensions() { + return this.uniforms.dimensions[0]; + } + set dimensions(value) { + this.uniforms.dimensions[0] = this.uniforms.dimensions[1] = (value * 100) / (canvas?.dimensions?.size ?? 100); } /** @override */ - static get parameters() { - return {}; + configure(options) { + if (!options) { + return; + } + const { color, ...otherOptions } = options; + const { r, g, b } = foundry.utils.Color.from(color.apply ? color.value : 0x000000); + super.configure({ ...otherOptions, r, g, b }); } /** @override */ - static get neutral() { - return {}; + play(options) { + this.lastTick = canvas.app.ticker.lastTime; + super.play(options); } /** @override */ async step() { - this.time = canvas.app.ticker.lastTime; + const delta = canvas.app.ticker.lastTime - this.lastTick; + this.lastTick = canvas.app.ticker.lastTime; + this.uniforms.time += delta * this.speed * 0.1; await super.step(); } + + apply(filterManager, input, output, clear, currentState) { + this.uniforms.filterMatrix ??= new PIXI.Matrix(); + this.uniforms.filterMatrix.copyFrom(currentState.target.worldTransform).invert(); + return super.apply(filterManager, input, output, clear, currentState); + } } diff --git a/src/filter-effects/filters/mixins/fading-filter.js b/src/filter-effects/filters/mixins/fading-filter.js index 98e23f1c..c01d1183 100644 --- a/src/filter-effects/filters/mixins/fading-filter.js +++ b/src/filter-effects/filters/mixins/fading-filter.js @@ -19,6 +19,9 @@ export function FadingFilterMixin(Base) { */ currentAnimation; + /** Has this filter already been initialized? */ + initialized = false; + /** * Apply options to this filter effect as an animation. * @param {object} [options] The options to animate @@ -31,7 +34,18 @@ export function FadingFilterMixin(Base) { await this.currentAnimation; } const data = { name, duration }; - const anim = Object.entries(options).map(([key, value]) => ({ parent: this, attribute: key, to: value })); + + const [toAnimate, toSet] = Object.entries(options) + .partition(([key]) => !!this.constructor.parameters[key]?.skipInitialAnimation && !this.initialized) + .map(Object.fromEntries); + + this.applyOptions(toSet); + + const anim = Object.entries(toAnimate).map(([key, value]) => ({ + parent: this.optionContext, + attribute: key, + to: value, + })); this.currentAnimation = CanvasAnimation.animate(anim, data).finally(() => (this.currentAnimation = undefined)); return this.currentAnimation; } @@ -44,6 +58,7 @@ export function FadingFilterMixin(Base) { this.enabled = true; this.animateOptions(); } + this.initialized = true; } /** @override */ diff --git a/src/filter-effects/filters/mixins/filter.js b/src/filter-effects/filters/mixins/filter.js index 7031e72e..9193e37a 100644 --- a/src/filter-effects/filters/mixins/filter.js +++ b/src/filter-effects/filters/mixins/filter.js @@ -81,6 +81,14 @@ export function FXMasterFilterEffectMixin(Base) { this.options = { ...this.constructor.default, ...options }; } + /** + * The context on which options should be applied. + * @type {object} + */ + get optionContext() { + return this; + } + /** * Apply options to this filter effect, setting the corresponding properties on this effect itself. * @param {object} options The options to apply @@ -88,7 +96,7 @@ export function FXMasterFilterEffectMixin(Base) { applyOptions(options = this.options) { const keys = Object.keys(options); for (const key of keys) { - this[key] = options[key]; + this.optionContext[key] = options[key]; } } diff --git a/src/filter-effects/filters/shaders/custom-vertex-2d.vert b/src/filter-effects/filters/shaders/custom-vertex-2d.vert new file mode 100644 index 00000000..9e18c9b0 --- /dev/null +++ b/src/filter-effects/filters/shaders/custom-vertex-2d.vert @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2022 Johannes Loher +// +// SPDX-License-Identifier: BSD-3-Clause + +precision mediump float; + +attribute vec2 aVertexPosition; + +uniform mat3 projectionMatrix; +uniform mat3 filterMatrix; +uniform vec4 inputSize; +uniform vec4 outputFrame; + +varying vec2 vTextureCoord; +varying vec2 vFilterCoord; + +void main(void) { + vTextureCoord = aVertexPosition * (outputFrame.zw * inputSize.zw); + vec2 position = aVertexPosition * max(outputFrame.zw, vec2(0.)) + outputFrame.xy; + vFilterCoord = (filterMatrix * vec3(position, 1.0)).xy; + gl_Position = vec4((projectionMatrix * vec3(position, 1.0)).xy, 0.0, 1.0); +} diff --git a/src/filter-effects/filters/shaders/customvertex2D.js b/src/filter-effects/filters/shaders/customvertex2D.js deleted file mode 100644 index c87b612c..00000000 --- a/src/filter-effects/filters/shaders/customvertex2D.js +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright © SecretFire 2021 -// This file is under BSD-3-Clause License - -// Custom vertex shader with filterLocalCoord() - -export const customVertex2D = ` -precision mediump float; - -attribute vec2 aVertexPosition; - -uniform mat3 projectionMatrix; -uniform mat3 filterMatrix; -uniform vec4 inputSize; -uniform vec4 outputFrame; - -varying vec2 vTextureCoord; -varying vec2 vFilterCoord; - -vec4 filterVertexPosition( void ) -{ - vec2 position = aVertexPosition * max(outputFrame.zw, vec2(0.)) + outputFrame.xy; - - return vec4((projectionMatrix * vec3(position, 1.0)).xy, 0., 1.); -} - -vec2 filterTextureCoord( void ) -{ - return aVertexPosition * (outputFrame.zw * inputSize.zw); -} - -void main(void) -{ - gl_Position = filterVertexPosition(); - vTextureCoord = filterTextureCoord(); - vFilterCoord = (filterMatrix * vec3(vTextureCoord, 1.0)).xy; -} -`; diff --git a/src/filter-effects/filters/shaders/fog.frag b/src/filter-effects/filters/shaders/fog.frag new file mode 100644 index 00000000..bfb3844d --- /dev/null +++ b/src/filter-effects/filters/shaders/fog.frag @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2021 SecretFire +// SPDX-FileCopyrightText: 2022 Johannes Loher +// +// SPDX-License-Identifier: BSD-3-Clause + +precision mediump float; + +uniform float time; +uniform vec3 color; +uniform float density; +uniform vec2 dimensions; +uniform sampler2D uSampler; + +varying vec2 vTextureCoord; +varying vec2 vFilterCoord; + +// generates pseudo-random based on screen position +float random(vec2 pos) { + return fract(sin(dot(pos.xy, vec2(12.9898, 78.233))) * 43758.5453123); +} + +// perlin noise +float noise(vec2 pos) { + vec2 i = floor(pos); + vec2 f = fract(pos); + float a = random(i + vec2(0.0, 0.0)); + float b = random(i + vec2(1.0, 0.0)); + float c = random(i + vec2(0.0, 1.0)); + float d = random(i + vec2(1.0, 1.0)); + vec2 u = f * f * (3.0 - 2.0 * f); + return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; +} + +// fractional brownian motion +float fbm(vec2 pos) { + float v = 0.0; + float a = 0.5; + vec2 shift = vec2(100.); + mat2 rot = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5)); + for(int i = 0; i < 16; i++) { + v = (sin(v * 1.07)) + (a * noise(pos)); + pos = rot * pos * 1.9 + shift; + a *= 0.5; + } + return v; +} + +mat4 contrastMatrix(float contrast) { + float t = (1.0 - contrast) * 0.5; + + return mat4(contrast, 0, 0, 0, 0, contrast, 0, 0, 0, 0, contrast, 0, t, t, t, 1); +} + +vec4 fog() { + vec2 p = (vFilterCoord.xy * 8. - vFilterCoord.xy) * dimensions * 0.00025; + + float time2 = time * 0.0025; + + vec2 q = vec2(0.0); + q.x = fbm(p); + q.y = fbm(p); + vec2 r = vec2(-1.0); + r.x = fbm(p * q + vec2(1.7, 9.2) + .15 * time2); + r.y = fbm(p * q + vec2(9.3, 2.8) + .35 * time2); + float f = fbm(p * .2 + r * 3.102); + + vec4 fogPixel = mix(vec4(color, 1.0), vec4(1.5, 1.5, 1.5, 1.5), clamp(length(r.x), 0.4, 1.)); + + return (f * f * f + 0.6 * f * f + 0.5 * f) * fogPixel; +} + +void main(void) { + vec4 pixel = texture2D(uSampler, vTextureCoord); + + // to avoid computation on an invisible pixel. + if(pixel.a == 0.) { + gl_FragColor = pixel; + return; + } + + vec4 fogPixel = contrastMatrix(3.0) * fog(); + gl_FragColor = mix(pixel, fogPixel, 1. * density) * pixel.a; +} diff --git a/src/filter-effects/filters/shaders/fog.js b/src/filter-effects/filters/shaders/fog.js deleted file mode 100644 index 4a2248ce..00000000 --- a/src/filter-effects/filters/shaders/fog.js +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright © SecretFire 2021 -// This file is under BSD-3-Clause License - -export const fog = ` -precision mediump float; - -uniform float time; -uniform vec3 color; -uniform float density; -uniform vec2 dimensions; -uniform sampler2D uSampler; - -varying vec2 vTextureCoord; -varying vec2 vFilterCoord; - -// generates pseudo-random based on screen position -float random(vec2 pos) -{ - return fract(sin(dot(pos.xy, vec2(12.9898, 78.233))) * 43758.5453123); -} - -// perlin noise -float noise(vec2 pos) -{ - vec2 i = floor(pos); - vec2 f = fract(pos); - float a = random(i + vec2(0.0, 0.0)); - float b = random(i + vec2(1.0, 0.0)); - float c = random(i + vec2(0.0, 1.0)); - float d = random(i + vec2(1.0, 1.0)); - vec2 u = f * f * (3.0 - 2.0 * f); - return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; -} - -// fractional brownian motion -float fbm(vec2 pos) -{ - float v = 0.0; - float a = 0.5; - vec2 shift = vec2(100.); - mat2 rot = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5)); - for (int i=0; i<16; i++) - { - v = (sin(v*1.07)) + ( a * noise(pos) ); - pos = rot * pos * 1.9 + shift; - a *= 0.5; - } - return v; -} - -mat4 contrastMatrix(float contrast) -{ - float t = ( 1.0 - contrast ) * 0.5; - - return mat4( contrast, 0, 0, 0, - 0, contrast, 0, 0, - 0, 0, contrast, 0, - t, t, t, 1 ); -} - -vec4 fog() -{ - vec2 p = (vFilterCoord.xy * 8. - vFilterCoord.xy) * dimensions; - - float time2 = time * 0.0025; - - vec2 q = vec2(0.0); - q.x = fbm(p); - q.y = fbm(p); - vec2 r = vec2(-1.0); - r.x = fbm(p * q + vec2(1.7, 9.2) + .15 * time2); - r.y = fbm(p * q + vec2(9.3, 2.8) + .35 * time2); - float f = fbm(p*.2 + r*3.102); - - vec4 fogPixel = mix( - vec4(color,1.0), - vec4(1.5, 1.5, 1.5, 1.5), - clamp(length(r.x), 0.4, 1.) - ); - - return (f *f * f + 0.6 * f * f + 0.5 * f) * fogPixel; -} - -void main(void) -{ - vec4 pixel = texture2D(uSampler, vTextureCoord); - - // to avoid computation on an invisible pixel. - if (pixel.a == 0.) { - gl_FragColor = pixel; - return; - } - - vec4 fogPixel = contrastMatrix(3.0)*fog(); - gl_FragColor = mix(pixel, fogPixel, 1.*density) * pixel.a; -} -`;