diff --git a/bundle/package.json b/bundle/package.json index 640ee3b14..50dfa458f 100644 --- a/bundle/package.json +++ b/bundle/package.json @@ -65,6 +65,7 @@ "@pixi/filter-radial-blur": "8.0.0", "@pixi/filter-reflection": "8.0.0", "@pixi/filter-rgb-split": "8.0.0", + "@pixi/filter-shockwave": "8.0.0", "@pixi/filter-simple-lightmap": "8.0.0", "@pixi/filter-tilt-shift": "8.0.0", "@pixi/filter-twist": "8.0.0", diff --git a/bundle/src/index.ts b/bundle/src/index.ts index e02aa32bb..b146fdd9d 100644 --- a/bundle/src/index.ts +++ b/bundle/src/index.ts @@ -28,6 +28,7 @@ export * from '@pixi/filter-pixelate'; export * from '@pixi/filter-radial-blur'; export * from '@pixi/filter-reflection'; export * from '@pixi/filter-rgb-split'; +export * from '@pixi/filter-shockwave'; export * from '@pixi/filter-simple-lightmap'; export * from '@pixi/filter-tilt-shift'; export * from '@pixi/filter-twist'; diff --git a/filters/ascii/src/ascii.wgsl b/filters/ascii/src/ascii.wgsl index 8325a72f3..28c5e8aa2 100644 --- a/filters/ascii/src/ascii.wgsl +++ b/filters/ascii/src/ascii.wgsl @@ -7,7 +7,7 @@ struct AsciiUniforms { struct GlobalFilterUniforms { uInputSize:vec4, uInputPixel:vec4, - uuInputClamp:vec4, + uInputClamp:vec4, uOutputFrame:vec4, uGlobalFrame:vec4, uOutputTexture:vec4, diff --git a/filters/dot/src/dot.wgsl b/filters/dot/src/dot.wgsl index 9890bbc67..bb59acc4b 100644 --- a/filters/dot/src/dot.wgsl +++ b/filters/dot/src/dot.wgsl @@ -7,7 +7,7 @@ struct DotUniforms { struct GlobalFilterUniforms { uInputSize:vec4, uInputPixel:vec4, - uuInputClamp:vec4, + uInputClamp:vec4, uOutputFrame:vec4, uGlobalFrame:vec4, uOutputTexture:vec4, diff --git a/filters/glow/src/glow.wgsl b/filters/glow/src/glow.wgsl index f200f943b..62529f008 100644 --- a/filters/glow/src/glow.wgsl +++ b/filters/glow/src/glow.wgsl @@ -10,7 +10,7 @@ struct GlowUniforms { struct GlobalFilterUniforms { uInputSize:vec4, uInputPixel:vec4, - uuInputClamp:vec4, + uInputClamp:vec4, uOutputFrame:vec4, uGlobalFrame:vec4, uOutputTexture:vec4, diff --git a/filters/rgb-split/src/rgb-split.wgsl b/filters/rgb-split/src/rgb-split.wgsl index d7d887785..086bb244a 100644 --- a/filters/rgb-split/src/rgb-split.wgsl +++ b/filters/rgb-split/src/rgb-split.wgsl @@ -7,7 +7,7 @@ struct RgbSplitUniforms { struct GlobalFilterUniforms { uInputSize:vec4, uInputPixel:vec4, - uuInputClamp:vec4, + uInputClamp:vec4, uOutputFrame:vec4, uGlobalFrame:vec4, uOutputTexture:vec4, diff --git a/filters/shockwave/LICENSE b/filters/shockwave/LICENSE new file mode 100644 index 000000000..4bad10d8e --- /dev/null +++ b/filters/shockwave/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2013-2018 Mathew Groves, Chad Engler, Wei Zijun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/filters/shockwave/README.md b/filters/shockwave/README.md new file mode 100644 index 000000000..23fefaa77 --- /dev/null +++ b/filters/shockwave/README.md @@ -0,0 +1,25 @@ +# ShockwaveFilter + +> PixiJS filter to apply a shockwave-type effect. + +[View demo](https://filters.pixijs.download/main/demo/index.html?enabled=ShockwaveFilter) + +## Installation + +```bash +npm install @pixi/filter-shockwave +``` + +## Usage + +```js +import {ShockwaveFilter} from '@pixi/filter-shockwave'; +import {Container} from 'pixi.js'; + +const container = new Container(); +container.filters = [new ShockwaveFilter()]; +``` + +## Documentation + +See https://filters.pixijs.download/main/docs/ShockwaveFilter.html \ No newline at end of file diff --git a/filters/shockwave/package.json b/filters/shockwave/package.json new file mode 100644 index 000000000..1b528ef1b --- /dev/null +++ b/filters/shockwave/package.json @@ -0,0 +1,41 @@ +{ + "name": "@pixi/filter-shockwave", + "version": "8.0.0", + "main": "./dist/filter-shockwave.js", + "description": "PixiJS filter to apply a shockwave-type effect", + "author": "Mat Groves", + "contributors": [ + "Matt Karl " + ], + "module": "./dist/filter-shockwave.mjs", + "exports": { + ".": { + "import": "./dist/filter-shockwave.mjs", + "require": "./dist/filter-shockwave.js", + "types": "./index.d.ts" + } + }, + "types": "./index.d.ts", + "homepage": "http://pixijs.com/", + "bugs": "https://github.com/pixijs/filters/issues", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/pixijs/filters.git", + "directory": "filters/shockwave" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist", + "index.d.ts" + ], + "peerDependencies": { + "pixi.js": "^8.0.0-X" + }, + "devDependencies": { + "pixi.js": "^8.0.0-X", + "@tools/fragments": "8.0.0" + } +} diff --git a/filters/shockwave/src/ShockwaveFilter.ts b/filters/shockwave/src/ShockwaveFilter.ts new file mode 100644 index 000000000..363bfc3f4 --- /dev/null +++ b/filters/shockwave/src/ShockwaveFilter.ts @@ -0,0 +1,203 @@ +import { vertex, wgslVertex } from '@tools/fragments'; +import fragment from './shockwave.frag'; +import source from './shockwave.wgsl'; +import { + Filter, + FilterOptions, + FilterSystem, + GlProgram, + GpuProgram, + PointData, + RenderSurface, + Texture, + UniformGroup, +} from 'pixi.js'; + +/** + * Options for ShockwaveFilter + * @memberof filters + */ +export interface ShockwaveFilterOptions +{ + /** + * The `x` and `y` center coordinates to change the position of the center of the circle of effect. + * @default [0,0] + */ + center?: PointData; + /** + * The speed about the shockwave ripples out. The unit is `pixel-per-second` + * @default 500 + */ + speed?: number; + /** + * The amplitude of the shockwave + * @default 30 + */ + amplitude?: number; + /** + * The wavelength of the shockwave + * @default 160 + */ + wavelength?: number; + /** + * The brightness of the shockwave + * @default 1 + */ + brightness?: number; + /** + * The maximum radius of shockwave. less than `0` means the max is an infinite distance + * @default -1 + */ + radius?: number; +} + +/** + * A Noise effect filter. + * + * original filter: https://github.com/evanw/glfx.js/blob/master/src/filters/adjust/noise.js + * @memberof filters + * @author Vico @vicocotea + */ +export class ShockwaveFilter extends Filter +{ + /** Default shockwave filter options */ + public static readonly defaultOptions: ShockwaveFilterOptions & Partial = { + ...Filter.defaultOptions, + /** The `x` and `y` center coordinates to change the position of the center of the circle of effect. */ + center: { x: 0, y: 0 }, + /** The speed about the shockwave ripples out. The unit is `pixel-per-second` */ + speed: 500, + /** The amplitude of the shockwave */ + amplitude: 30, + /** The wavelength of the shockwave */ + wavelength: 160, + /** The brightness of the shockwave */ + brightness: 1, + /** The maximum radius of shockwave. less than `0` means the max is an infinite distance */ + radius: -1, + }; + + public uniforms: { + uTime: number; + uCenter: PointData; + uSpeed: number; + uWave: Float32Array; + }; + + /** Sets the elapsed time of the shockwave. It could control the current size of shockwave. */ + public time: number; + + /** + * @param options + */ + constructor(options: ShockwaveFilterOptions = {}) + { + options = { ...ShockwaveFilter.defaultOptions, ...options }; + + const gpuProgram = new GpuProgram({ + vertex: { + source: wgslVertex, + entryPoint: 'mainVertex', + }, + fragment: { + source, + entryPoint: 'mainFragment', + }, + }); + + const glProgram = new GlProgram({ + vertex, + fragment, + name: 'shockwave-filter' + }); + + super({ + gpuProgram, + glProgram, + resources: { + shockwaveUniforms: new UniformGroup({ + uTime: { value: 0, type: 'f32' }, + uCenter: { value: options.center, type: 'vec2' }, + uSpeed: { value: options.speed, type: 'f32' }, + uWave: { value: new Float32Array(4), type: 'vec4' }, + }) + }, + }); + + this.time = 0; + + this.uniforms = this.resources.shockwaveUniforms.uniforms; + + Object.assign(this, options); + } + + public override apply( + filterManager: FilterSystem, + input: Texture, + output: RenderSurface, + clearMode: boolean + ): void + { + // There is no set/get of `time`, for performance. + // Because in the most real cases, `time` will be changed in ever game tick. + // Use set/get will take more function-call. + this.uniforms.uTime = this.time; + filterManager.applyFilter(this, input, output, clearMode); + } + + /** + * The `x` and `y` center coordinates to change the position of the center of the circle of effect. + * @default [0,0] + */ + get center(): PointData { return this.uniforms.uCenter; } + set center(value: PointData) { this.uniforms.uCenter = value; } + + /** + * Sets the center of the effect in normalized screen coords on the `x` axis + * @default 0 + */ + get centerX(): number { return this.uniforms.uCenter.x; } + set centerX(value: number) { this.uniforms.uCenter.x = value; } + + /** + * Sets the center of the effect in normalized screen coords on the `y` axis + * @default 0 + */ + get centerY(): number { return this.uniforms.uCenter.y; } + set centerY(value: number) { this.uniforms.uCenter.y = value; } + + /** + * The speed about the shockwave ripples out. The unit is `pixel-per-second` + * @default 500 + */ + get speed(): number { return this.uniforms.uSpeed; } + set speed(value: number) { this.uniforms.uSpeed = value; } + + /** + * The amplitude of the shockwave + * @default 30 + */ + get amplitude(): number { return this.uniforms.uWave[0]; } + set amplitude(value: number) { this.uniforms.uWave[0] = value; } + + /** + * The wavelength of the shockwave + * @default 160 + */ + get wavelength(): number { return this.uniforms.uWave[1]; } + set wavelength(value: number) { this.uniforms.uWave[1] = value; } + + /** + * The brightness of the shockwave + * @default 1 + */ + get brightness(): number { return this.uniforms.uWave[2]; } + set brightness(value: number) { this.uniforms.uWave[2] = value; } + + /** + * The maximum radius of shockwave. less than `0` means the max is an infinite distance + * @default -1 + */ + get radius(): number { return this.uniforms.uWave[3]; } + set radius(value: number) { this.uniforms.uWave[3] = value; } +} diff --git a/filters/shockwave/src/index.ts b/filters/shockwave/src/index.ts new file mode 100644 index 000000000..3396f0c9a --- /dev/null +++ b/filters/shockwave/src/index.ts @@ -0,0 +1 @@ +export * from './ShockwaveFilter'; diff --git a/filters/shockwave/src/shockwave.frag b/filters/shockwave/src/shockwave.frag new file mode 100644 index 000000000..6077922de --- /dev/null +++ b/filters/shockwave/src/shockwave.frag @@ -0,0 +1,72 @@ + +precision highp float; +in vec2 vTextureCoord; +out vec4 finalColor; + +uniform sampler2D uSampler; +uniform vec2 uCenter; +uniform float uTime; +uniform float uSpeed; +uniform vec4 uWave; + +uniform vec4 uInputSize; +uniform vec4 uInputClamp; + +const float PI = 3.14159; + +void main() +{ + float uAmplitude = uWave[0]; + float uWavelength = uWave[1]; + float uBrightness = uWave[2]; + float uRadius = uWave[3]; + + float halfWavelength = uWavelength * 0.5 / uInputSize.x; + float maxRadius = uRadius / uInputSize.x; + float currentRadius = uTime * uSpeed / uInputSize.x; + + float fade = 1.0; + + if (maxRadius > 0.0) { + if (currentRadius > maxRadius) { + finalColor = texture(uSampler, vTextureCoord); + return; + } + fade = 1.0 - pow(currentRadius / maxRadius, 2.0); + } + + vec2 dir = vec2(vTextureCoord - uCenter / uInputSize.xy); + dir.y *= uInputSize.y / uInputSize.x; + float dist = length(dir); + + if (dist <= 0.0 || dist < currentRadius - halfWavelength || dist > currentRadius + halfWavelength) { + finalColor = texture(uSampler, vTextureCoord); + return; + } + + vec2 diffUV = normalize(dir); + + float diff = (dist - currentRadius) / halfWavelength; + + float p = 1.0 - pow(abs(diff), 2.0); + + // float powDiff = diff * pow(p, 2.0) * ( amplitude * fade ); + float powDiff = 1.25 * sin(diff * PI) * p * ( uAmplitude * fade ); + + vec2 offset = diffUV * powDiff / uInputSize.xy; + + // Do clamp : + vec2 coord = vTextureCoord + offset; + vec2 clampedCoord = clamp(coord, uInputClamp.xy, uInputClamp.zw); + vec4 color = texture(uSampler, clampedCoord); + if (coord != clampedCoord) { + color *= max(0.0, 1.0 - length(coord - clampedCoord)); + } + + // No clamp : + // finalColor = texture(uSampler, vTextureCoord + offset); + + color.rgb *= 1.0 + (uBrightness - 1.0) * p * fade; + + finalColor = color; +} diff --git a/filters/shockwave/src/shockwave.wgsl b/filters/shockwave/src/shockwave.wgsl new file mode 100644 index 000000000..9a9729326 --- /dev/null +++ b/filters/shockwave/src/shockwave.wgsl @@ -0,0 +1,86 @@ + +struct ShockWaveUniforms { + uTime: f32, + uOffset: vec2, + uSpeed: f32, + uWave: vec4, +}; + +struct GlobalFilterUniforms { + uInputSize:vec4, + uInputPixel:vec4, + uInputClamp:vec4, + uOutputFrame:vec4, + uGlobalFrame:vec4, + uOutputTexture:vec4, +}; + +@group(0) @binding(0) var gfu: GlobalFilterUniforms; + +@group(0) @binding(1) var uSampler: texture_2d; +@group(1) @binding(0) var shockwaveUniforms : ShockWaveUniforms; + +@fragment +fn mainFragment( + @builtin(position) position: vec4, + @location(0) uv : vec2 +) -> @location(0) vec4 { + + let uTime = shockwaveUniforms.uTime; + let uOffset = shockwaveUniforms.uOffset; + let uSpeed = shockwaveUniforms.uSpeed; + let uAmplitude = shockwaveUniforms.uWave[0]; + let uWavelength = shockwaveUniforms.uWave[1]; + let uBrightness = shockwaveUniforms.uWave[2]; + let uRadius = shockwaveUniforms.uWave[3]; + let halfWavelength: f32 = uWavelength * 0.5 / gfu.uInputSize.x; + let maxRadius: f32 = uRadius / gfu.uInputSize.x; + let currentRadius: f32 = uTime * uSpeed / gfu.uInputSize.x; + var fade: f32 = 1.0; + var returnColorOnly: bool = false; + + if (maxRadius > 0.0) { + if (currentRadius > maxRadius) { + returnColorOnly = true; + } + fade = 1.0 - pow(currentRadius / maxRadius, 2.0); + } + var dir: vec2 = vec2(uv - uOffset / gfu.uInputSize.xy); + dir.y *= gfu.uInputSize.y / gfu.uInputSize.x; + + let dist:f32 = length(dir); + + if (dist <= 0.0 || dist < currentRadius - halfWavelength || dist > currentRadius + halfWavelength) { + returnColorOnly = true; + } + + let diffUV: vec2 = normalize(dir); + let diff: f32 = (dist - currentRadius) / halfWavelength; + let p: f32 = 1.0 - pow(abs(diff), 2.0); + let powDiff: f32 = 1.25 * sin(diff * PI) * p * ( uAmplitude * fade ); + let offset: vec2 = diffUV * powDiff / gfu.uInputSize.xy; + // Do clamp : + let coord: vec2 = uv + offset; + let clampedCoord: vec2 = clamp(coord, gfu.uInputClamp.xy, gfu.uInputClamp.zw); + + var clampedColor: vec4 = textureSample(uSampler, uSampler, clampedCoord); + + if (boolVec2(coord, clampedCoord)) + { + clampedColor *= max(0.0, 1.0 - length(coord - clampedCoord)); + } + // No clamp : + return select(clampedColor * vec4(vec3(1.0 + (uBrightness - 1.0) * p * fade), clampedColor.a), textureSample(uTexture, uSampler, uv), returnColorOnly); +} + +fn boolVec2(x: vec2, y: vec2) -> bool +{ + if (x.x == y.x && x.y == y.y) + { + return true; + } + + return false; +} + +const PI: f32 = 3.14159265358979323846264; diff --git a/package-lock.json b/package-lock.json index 7980f8cf8..9a0aed8d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,6 +76,7 @@ "@pixi/filter-radial-blur": "8.0.0", "@pixi/filter-reflection": "8.0.0", "@pixi/filter-rgb-split": "8.0.0", + "@pixi/filter-shockwave": "8.0.0", "@pixi/filter-simple-lightmap": "8.0.0", "@pixi/filter-tilt-shift": "8.0.0", "@pixi/filter-twist": "8.0.0", @@ -454,6 +455,17 @@ "pixi.js": "^8.0.0-X" } }, + "filters/shockwave": { + "version": "8.0.0", + "license": "MIT", + "devDependencies": { + "@tools/fragments": "8.0.0", + "pixi.js": "^8.0.0-X" + }, + "peerDependencies": { + "pixi.js": "^8.0.0-X" + } + }, "filters/simple-lightmap": { "name": "@pixi/filter-simple-lightmap", "version": "8.0.0", @@ -6928,6 +6940,10 @@ "resolved": "filters/rgb-split", "link": true }, + "node_modules/@pixi/filter-shockwave": { + "resolved": "filters/shockwave", + "link": true + }, "node_modules/@pixi/filter-simple-lightmap": { "resolved": "filters/simple-lightmap", "link": true @@ -30493,6 +30509,13 @@ "pixi.js": "^8.0.0-X" } }, + "@pixi/filter-shockwave": { + "version": "file:filters/shockwave", + "requires": { + "@tools/fragments": "8.0.0", + "pixi.js": "^8.0.0-X" + } + }, "@pixi/filter-simple-lightmap": { "version": "file:filters/simple-lightmap", "requires": { @@ -41498,6 +41521,7 @@ "@pixi/filter-radial-blur": "8.0.0", "@pixi/filter-reflection": "8.0.0", "@pixi/filter-rgb-split": "8.0.0", + "@pixi/filter-shockwave": "8.0.0", "@pixi/filter-simple-lightmap": "8.0.0", "@pixi/filter-tilt-shift": "8.0.0", "@pixi/filter-twist": "8.0.0", diff --git a/tools/demo/src/filters/shockwave.js b/tools/demo/src/filters/shockwave.js index 68f53eaaf..42ff44f3f 100644 --- a/tools/demo/src/filters/shockwave.js +++ b/tools/demo/src/filters/shockwave.js @@ -4,7 +4,6 @@ export default function () this.addFilter('ShockwaveFilter', { enabled: false, - global: true, args: { center: { x: app.initWidth / 2, y: app.initHeight / 2 } }, oncreate(folder) { @@ -31,7 +30,7 @@ export default function () }); folder.add(this, 'animating').name('(animating)'); - folder.add(this, 'time', 0, maxTime); + folder.add(this, 'speed', 500, 2000); folder.add(this, 'amplitude', 1, 100); folder.add(this, 'wavelength', 2, 400); folder.add(this, 'brightness', 0.2, 2.0); diff --git a/tools/demo/src/index.js b/tools/demo/src/index.js index 1e8237c1a..b2b4639a4 100644 --- a/tools/demo/src/index.js +++ b/tools/demo/src/index.js @@ -44,6 +44,7 @@ const main = async () => filters.crossHatch.call(app); filters.convolution.call(app); filters.dot.call(app); + filters.shockwave.call(app); // filters.kawaseBlur.call(app); // TODO: Re-enable this in place of the above once v8 conversion is complete