diff --git a/tokenmagic/fx/filters/FilterBlur.js b/tokenmagic/fx/filters/FilterBlur.js index a005acf..8f0dd69 100644 --- a/tokenmagic/fx/filters/FilterBlur.js +++ b/tokenmagic/fx/filters/FilterBlur.js @@ -1,7 +1,8 @@ -import { Anime } from "../Anime.js"; +import {Anime} from "../Anime.js"; import "./proto/FilterProto.js"; +import {FilterBlurEx} from "./FilterBlurEx.js"; -export class FilterBlur extends PIXI.filters.BlurFilter { +export class FilterBlur extends FilterBlurEx { constructor(params) { super(); this.enabled = false; @@ -43,12 +44,9 @@ export class FilterBlur extends PIXI.filters.BlurFilter { calculatePadding() { const scale = this.targetPlaceable.worldTransform.a; - this.blurXFilter.blur = scale * this.strengthX; this.blurYFilter.blur = scale * this.strengthY; - this.updatePadding(); - super.calculatePadding(); } -} +} \ No newline at end of file diff --git a/tokenmagic/fx/filters/FilterBlurEx.js b/tokenmagic/fx/filters/FilterBlurEx.js new file mode 100644 index 0000000..3755626 --- /dev/null +++ b/tokenmagic/fx/filters/FilterBlurEx.js @@ -0,0 +1,318 @@ +import { CustomFilter } from './CustomFilter.js'; + +export class FilterBlurEx extends CustomFilter { + blurXFilter; + blurYFilter; + + _repeatEdgePixels; + + constructor(strength = 8, quality = 4, resolution = PIXI.settings.FILTER_RESOLUTION, kernelSize = 5) { + super(); + + this.blurXFilter = new BlurFilterPassEx(true, strength, quality, resolution, kernelSize); + this.blurYFilter = new BlurFilterPassEx(false, strength, quality, resolution, kernelSize); + + this.resolution = resolution; + this.quality = quality; + this.blur = strength; + + this.repeatEdgePixels = false; + } + + apply(filterManager, input, output, clearMode) { + const xStrength = Math.abs(this.blurXFilter.strength); + const yStrength = Math.abs(this.blurYFilter.strength); + + if (xStrength && yStrength) { + const renderTarget = filterManager.getFilterTexture(); + + this.blurXFilter.apply(filterManager, input, renderTarget, PIXI.CLEAR_MODES.CLEAR); + this.blurYFilter.apply(filterManager, renderTarget, output, clearMode); + + filterManager.returnFilterTexture(renderTarget); + } + else if (yStrength) { + this.blurYFilter.apply(filterManager, input, output, clearMode); + } + else { + this.blurXFilter.apply(filterManager, input, output, clearMode); + } + } + + updatePadding() { + if (this._repeatEdgePixels) { + this.padding = 0; + } + else { + this.padding = Math.max(Math.abs(this.blurXFilter.strength), Math.abs(this.blurYFilter.strength)) * 2; + } + } + + get blur() { + return this.blurXFilter.blur; + } + + set blur(value) { + this.blurXFilter.blur = this.blurYFilter.blur = value; + this.updatePadding(); + } + + get quality() { + return this.blurXFilter.quality; + } + + set quality(value) { + this.blurXFilter.quality = this.blurYFilter.quality = value; + } + + get blurX() { + return this.blurXFilter.blur; + } + + set blurX(value) { + this.blurXFilter.blur = value; + this.updatePadding(); + } + + get blurY() { + return this.blurYFilter.blur; + } + + set blurY(value) { + this.blurYFilter.blur = value; + this.updatePadding(); + } + + get blendMode() { + return this.blurYFilter.blendMode; + } + + set blendMode(value) { + this.blurYFilter.blendMode = value; + } + + get repeatEdgePixels() { + return this._repeatEdgePixels; + } + + set repeatEdgePixels(value) { + this._repeatEdgePixels = value; + this.updatePadding(); + } +} + +export class BlurFilterPassEx extends CustomFilter { + horizontal; + strength; + passes; + _quality; + + constructor(horizontal, strength = 8, quality = 4, resolution = PIXI.settings.FILTER_RESOLUTION, kernelSize = 5) { + const vertSrc = generateBlurVertSource(kernelSize, horizontal); + const fragSrc = generateBlurFragSource(kernelSize); + + super( + // vertex shader + vertSrc, + // fragment shader + fragSrc + ); + + this.horizontal = horizontal; + + this.resolution = resolution; + + this._quality = 0; + + this.quality = quality; + + this.blur = strength; + } + + apply(filterManager, input, output, clearMode) { + if (output) { + if (this.horizontal) { + this.uniforms.strength = (1 / output.width) * (output.width / input.width); + } else { + this.uniforms.strength = (1 / output.height) * (output.height / input.height); + } + } + else { + if (this.horizontal) { + this.uniforms.strength = (1 / filterManager.renderer.width) * (filterManager.renderer.width / input.width); + } else { + this.uniforms.strength = (1 / filterManager.renderer.height) * (filterManager.renderer.height / input.height); + } + } + + // screen space! + this.uniforms.strength *= this.strength; + this.uniforms.strength /= this.passes; + + if (this.passes === 1) { + filterManager.applyFilter(this, input, output, clearMode); + } + else { + const renderTarget = filterManager.getFilterTexture(); + const renderer = filterManager.renderer; + + let flip = input; + let flop = renderTarget; + + this.state.blend = false; + filterManager.applyFilter(this, flip, flop, PIXI.CLEAR_MODES.CLEAR); + + for (let i = 1; i < this.passes - 1; i++) { + filterManager.bindAndClear(flip, PIXI.CLEAR_MODES.BLIT); + + this.uniforms.uSampler = flop; + + const temp = flop; + + flop = flip; + flip = temp; + + renderer.shader.bind(this); + renderer.geometry.draw(5); + } + + this.state.blend = true; + filterManager.applyFilter(this, flop, output, clearMode); + filterManager.returnFilterTexture(renderTarget); + } + } + + get blur() { + return this.strength; + } + + set blur(value) { + this.padding = 1 + (Math.abs(value) * 2); + this.strength = value; + } + + get quality() { + return this._quality; + } + + set quality(value) { + this._quality = value; + this.passes = value; + } +} + +const GAUSSIAN_VALUES = { + 5: [0.153388, 0.221461, 0.250301], + 7: [0.071303, 0.131514, 0.189879, 0.214607], + 9: [0.028532, 0.067234, 0.124009, 0.179044, 0.20236], + 11: [0.0093, 0.028002, 0.065984, 0.121703, 0.175713, 0.198596], + 13: [0.002406, 0.009255, 0.027867, 0.065666, 0.121117, 0.174868, 0.197641], + 15: [0.000489, 0.002403, 0.009246, 0.02784, 0.065602, 0.120999, 0.174697, 0.197448], +}; + +const fragTemplate = [ + 'varying vec2 vBlurTexCoords[%size%];', + 'uniform sampler2D uSampler;', + + 'void main(void)', + '{', + ' gl_FragColor = vec4(0.0);', + ' %blur%', + '}', + +].join('\n'); + +export function generateBlurFragSource(kernelSize) { + const kernel = GAUSSIAN_VALUES[kernelSize]; + const halfLength = kernel.length; + + let fragSource = fragTemplate; + + let blurLoop = ''; + const template = 'gl_FragColor += texture2D(uSampler, vBlurTexCoords[%index%]) * %value%;'; + let value; + + for (let i = 0; i < kernelSize; i++) { + let blur = template.replace('%index%', i.toString()); + + value = i; + + if (i >= halfLength) { + value = kernelSize - i - 1; + } + + blur = blur.replace('%value%', kernel[value].toString()); + + blurLoop += blur; + blurLoop += '\n'; + } + + fragSource = fragSource.replace('%blur%', blurLoop); + fragSource = fragSource.replace('%size%', kernelSize.toString()); + + return fragSource; +} + +const vertTemplate = ` + attribute vec2 aVertexPosition; + uniform mat3 projectionMatrix; + uniform float strength; + varying vec2 vBlurTexCoords[%size%]; + uniform vec4 inputSize; + uniform vec4 outputFrame; + vec4 filterVertexPosition( void ) + { + vec2 position = aVertexPosition * max(outputFrame.zw, vec2(0.)) + outputFrame.xy; + return vec4((projectionMatrix * vec3(position, 1.0)).xy, 0.0, 1.0); + } + vec2 filterTextureCoord( void ) + { + return aVertexPosition * (outputFrame.zw * inputSize.zw); + } + void main(void) + { + gl_Position = filterVertexPosition(); + vec2 textureCoord = filterTextureCoord(); + %blur% + }`; + +export function generateBlurVertSource(kernelSize, x) { + const halfLength = Math.ceil(kernelSize / 2); + + let vertSource = vertTemplate; + + let blurLoop = ''; + let template; + + if (x) { + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(%sampleIndex% * strength, 0.0);'; + } + else { + template = 'vBlurTexCoords[%index%] = textureCoord + vec2(0.0, %sampleIndex% * strength);'; + } + + for (let i = 0; i < kernelSize; i++) { + let blur = template.replace('%index%', i.toString()); + + blur = blur.replace('%sampleIndex%', `${i - (halfLength - 1)}.0`); + + blurLoop += blur; + blurLoop += '\n'; + } + + vertSource = vertSource.replace('%blur%', blurLoop); + vertSource = vertSource.replace('%size%', kernelSize.toString()); + + return vertSource; +} + +export function getMaxKernelSize(gl) { + const maxVaryings = (PIXI.gl.getParameter(PIXI.gl.MAX_VARYING_VECTORS)); + let kernelSize = 15; + + while (kernelSize > maxVaryings) { + kernelSize -= 2; + } + + return kernelSize; +} diff --git a/tokenmagic/fx/filters/FilterDropShadow.js b/tokenmagic/fx/filters/FilterDropShadow.js index f237d47..d503caf 100644 --- a/tokenmagic/fx/filters/FilterDropShadow.js +++ b/tokenmagic/fx/filters/FilterDropShadow.js @@ -1,7 +1,8 @@ import { Anime } from "../Anime.js"; import "./proto/FilterProto.js"; +import {FilterDropShadowEx} from "./FilterDropShadowEx.js"; -export class FilterDropShadow extends PIXI.filters.DropShadowFilter { +export class FilterDropShadow extends FilterDropShadowEx { constructor(params) { super(); this.enabled = false; @@ -20,5 +21,6 @@ export class FilterDropShadow extends PIXI.filters.DropShadowFilter { this.anime = new Anime(this); this.normalizeTMParams(); } + this.autoFit = false; } } \ No newline at end of file diff --git a/tokenmagic/fx/filters/FilterDropShadowEx.js b/tokenmagic/fx/filters/FilterDropShadowEx.js new file mode 100644 index 0000000..12148c5 --- /dev/null +++ b/tokenmagic/fx/filters/FilterDropShadowEx.js @@ -0,0 +1,149 @@ +import {CustomFilter} from './CustomFilter.js'; +import {dropShadow} from '../glsl/fragmentshaders/dropshadow.js'; +import {customVertex2D} from '../glsl/vertexshaders/customvertex2D.js'; +import "./proto/FilterProto.js"; + +export class FilterDropShadowEx extends CustomFilter { + + shadowOnly; + angle = 45; + + _distance = 5; + _resolution = PIXI.settings.FILTER_RESOLUTION; + _tintFilter; + _blurFilter; + + constructor(options = {}) { + super(); + + const opt = options + ? {...FilterDropShadowEx.defaults, ...options} + : FilterDropShadowEx.defaults; + + const {kernels, blur, quality, resolution} = opt; + + this._tintFilter = new PIXI.Filter(customVertex2D, dropShadow); + this._tintFilter.uniforms.color = new Float32Array(4); + this._tintFilter.uniforms.shift = new PIXI.Point(); + this._tintFilter.resolution = resolution; + this._blurFilter = kernels + ? new PIXI.filters.KawaseBlurFilter(kernels) + : new PIXI.filters.KawaseBlurFilter(blur, quality); + + this._pixelSize = 1.0; + this.resolution = resolution; + + const {shadowOnly, rotation, distance, alpha, color} = opt; + + this.shadowOnly = shadowOnly; + this.rotation = rotation; + this.distance = distance; + this.alpha = alpha; + this.color = color; + } + + apply(filterManager, input, output, clear) { + this._updateShiftAndScale(); + const target = filterManager.getFilterTexture(); + + this._tintFilter.apply(filterManager, input, target, 1); + this._blurFilter.apply(filterManager, target, output, clear); + + if (this.shadowOnly !== true) { + filterManager.applyFilter(this, input, output, 0); + } + + filterManager.returnFilterTexture(target); + } + + _updateShiftAndScale() { + const scale = this.targetPlaceable?.worldTransform.a ?? 1.0; + this._tintFilter.uniforms.shift.set( + this.distance * Math.cos(this.angle) * scale, + this.distance * Math.sin(this.angle) * scale, + ); + this._pixelSize = Math.max(1.0, 1.0 * scale); + } + + get resolution() { + return this._resolution; + } + set resolution(value) { + this._resolution = value; + + if (this._tintFilter) { + this._tintFilter.resolution = value; + } + if (this._blurFilter) { + this._blurFilter.resolution = value; + } + } + + get distance() { + return this._distance; + } + set distance(value) { + this._distance = value; + } + + get rotation() { + return this.angle / PIXI.DEG_TO_RAD; + } + set rotation(value) { + this.angle = value * PIXI.DEG_TO_RAD; + } + + get alpha() { + return this._tintFilter.uniforms.alpha; + } + set alpha(value) { + this._tintFilter.uniforms.alpha = value; + } + + get color() { + return PIXI.utils.rgb2hex(this._tintFilter.uniforms.color); + } + set color(value) { + PIXI.utils.hex2rgb(value, this._tintFilter.uniforms.color); + } + + get kernels() { + return this._blurFilter.kernels; + } + set kernels(value) { + this._blurFilter.kernels = value; + } + + get blur() { + return this._blurFilter.blur; + } + set blur(value) { + this._blurFilter.blur = value; + } + + get quality() { + return this._blurFilter.quality; + } + set quality(value) { + this._blurFilter.quality = value; + } + + get _pixelSize() { + return this._blurFilter.pixelSize; + } + set _pixelSize(value) { + this._blurFilter.pixelSize = value; + } +} + +FilterDropShadowEx.defaults = { + rotation: 45, + distance: 5, + color: 0x000000, + alpha: 0.5, + shadowOnly: false, + kernels: null, + blur: 2, + quality: 3, + resolution: PIXI.settings.FILTER_RESOLUTION, +}; \ No newline at end of file diff --git a/tokenmagic/fx/filters/FilterPixelate.js b/tokenmagic/fx/filters/FilterPixelate.js index 74ef3b3..02967ec 100644 --- a/tokenmagic/fx/filters/FilterPixelate.js +++ b/tokenmagic/fx/filters/FilterPixelate.js @@ -16,22 +16,6 @@ export class FilterPixelate extends PIXI.filters.PixelateFilter { } } - //get sizeX() { - // return this.size.x; - //} - - //set sizeX(value) { - // this.size.x = value; - //} - - //get sizeY() { - // return this.size.y; - //} - - //set sizeY(value) { - // this.size.y = value; - //} - handleTransform() { this.size.x = this.sizeX * this.placeableImg.parent.worldTransform.a; this.size.y = this.sizeY * this.placeableImg.parent.worldTransform.a; diff --git a/tokenmagic/fx/glsl/fragmentshaders/dropshadow.js b/tokenmagic/fx/glsl/fragmentshaders/dropshadow.js new file mode 100644 index 0000000..91d5e14 --- /dev/null +++ b/tokenmagic/fx/glsl/fragmentshaders/dropshadow.js @@ -0,0 +1,21 @@ +export const dropShadow = ` +varying vec2 vTextureCoord; +uniform sampler2D uSampler; +uniform float alpha; +uniform vec3 color; + +uniform vec2 shift; +uniform vec4 inputSize; + +void main(void){ + vec4 sample = texture2D(uSampler, vTextureCoord - shift * inputSize.zw); + + // Premultiply alpha + sample.rgb = color.rgb * sample.a; + + // alpha user alpha + sample *= alpha; + + gl_FragColor = sample; +} +` \ No newline at end of file diff --git a/tokenmagic/module.json b/tokenmagic/module.json index 00944de..89d0256 100644 --- a/tokenmagic/module.json +++ b/tokenmagic/module.json @@ -2,7 +2,7 @@ "name": "tokenmagic", "title": "Token Magic FX", "description": "

Add special effects and animations on your tokens, tiles, drawings and templates.

", - "version": "0.5.1", + "version": "0.5.2", "compatibleCoreVersion": "0.8.6", "minimumCoreVersion": "0.8.6", "author": "SecretFire, sPOiDar, Moerill, dev7355608", @@ -30,6 +30,9 @@ }, { "name": "Mestre Digital" + }, + { + "name": "BrotherSharper-Touge" } ], "scripts": [ @@ -40,6 +43,8 @@ "module/tokenmagic.js", "module/proto/PlaceableObjectProto.js", "fx/Anime.js", + "fx/filters/FilterBlurEx.js", + "fx/filters/FilterDropShadowEx.js", "fx/filters/FilterBevel.js", "fx/filters/FilterAdjustment.js", "fx/filters/FilterAdvancedBloom.js", @@ -138,5 +143,5 @@ "socket": true, "url": "https://github.com/Feu-Secret/Tokenmagic", "manifest": "https://raw.githubusercontent.com/Feu-Secret/Tokenmagic/master/tokenmagic/module.json", - "download": "https://github.com/Feu-Secret/Tokenmagic/releases/download/v0.5.1-beta/Tokenmagic.zip" + "download": "https://github.com/Feu-Secret/Tokenmagic/releases/download/v0.5.2-beta/Tokenmagic.zip" }