From 7b721a80a9055fa8681ffb43a5bf2bd4b6b213e3 Mon Sep 17 00:00:00 2001 From: mehmetcanakbay <73856544+mehmetcanakbay@users.noreply.github.com> Date: Tue, 7 Jan 2025 03:37:15 +0300 Subject: [PATCH] feat: Add Simplex Noise Filter (#482) --- README.md | 2 + examples/src/filters/index.mjs | 1 + examples/src/filters/simplex-noise.mjs | 16 +++ package.json | 5 + scripts/screenshots/config.json | 8 ++ src/index.ts | 1 + src/simplex-noise/SimplexNoiseFilter.ts | 143 ++++++++++++++++++++++++ src/simplex-noise/index.ts | 1 + src/simplex-noise/simplex.frag | 61 ++++++++++ src/simplex-noise/simplex.wgsl | 61 ++++++++++ 10 files changed, 299 insertions(+) create mode 100644 examples/src/filters/simplex-noise.mjs create mode 100644 src/simplex-noise/SimplexNoiseFilter.ts create mode 100644 src/simplex-noise/index.ts create mode 100644 src/simplex-noise/simplex.frag create mode 100644 src/simplex-noise/simplex.wgsl diff --git a/README.md b/README.md index 30525df39..e2f1ce05a 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ If all else failes, you can manually download the bundled file from the [release | **RGBSplitFilter**
_pixi-filters/rgb-split_
[View demo][RGBSplit_demo] | ![rgb split](https://filters.pixijs.download/main/screenshots/rgb.png?v=3) | | | **ShockwaveFilter**
_pixi-filters/shockwave_
[View demo][Shockwave_demo] | ![shockwave](https://filters.pixijs.download/main/screenshots/shockwave.gif?v=3) | | **SimpleLightmapFilter**
_pixi-filters/simple-lightmap_
[View demo][SimpleLightmap_demo] | ![simple-lightmap](https://filters.pixijs.download/main/screenshots/simple-lightmap.png?v=3) | +| **SimplexNoiseFilter**
_pixi-filters/simplex-noise_
[View demo][SimplexNoise_demo] | ![simplex-noise](https://filters.pixijs.download/main/screenshots/simplex-noise.png?v=3) | | **TiltShiftFilter**
_pixi-filters/tilt-shift_
[View demo][TiltShift_demo] | ![tilt-shift](https://filters.pixijs.download/main/screenshots/tilt-shift.png?v=3) | | **TwistFilter**
_pixi-filters/twist_
[View demo][Twist_demo] | ![twist](https://filters.pixijs.download/main/screenshots/twist.png?v=3) | | **ZoomBlurFilter**
_pixi-filters/zoom-blur_
[View demo][ZoomBlur_demo] | ![zoom-blur](https://filters.pixijs.download/main/screenshots/zoom-blur.png?v=4) | @@ -152,6 +153,7 @@ API documention can be found [here](http://pixijs.io/filters/docs/). [RGBSplit_demo]: https://filters.pixijs.download/main/examples/index.html?enabled=RGBSplitFilter [Shockwave_demo]: https://filters.pixijs.download/main/examples/index.html?enabled=ShockwaveFilter [SimpleLightmap_demo]: https://filters.pixijs.download/main/examples/index.html?enabled=SimpleLightmapFilter +[SimplexNoise_demo]: https://filters.pixijs.download/main/examples/index.html?enabled=SimplexNoiseFilter [TiltShift_demo]: https://filters.pixijs.download/main/examples/index.html?enabled=TiltShiftFilter [Twist_demo]: https://filters.pixijs.download/main/examples/index.html?enabled=TwistFilter [ZoomBlur_demo]: https://filters.pixijs.download/main/examples/index.html?enabled=ZoomBlurFilter diff --git a/examples/src/filters/index.mjs b/examples/src/filters/index.mjs index e294af188..7c3c42a5b 100644 --- a/examples/src/filters/index.mjs +++ b/examples/src/filters/index.mjs @@ -38,6 +38,7 @@ export { default as reflection } from './reflection.mjs'; export { default as rgb } from './rgb.mjs'; export { default as shockwave } from './shockwave.mjs'; export { default as simpleLightmap } from './lightmap.mjs'; +export { default as simplexNoise } from './simplex-noise.mjs'; export { default as tiltShift } from './tilt-shift.mjs'; export { default as twist } from './twist.mjs'; export { default as zoomBlur } from './zoom-blur.mjs'; diff --git a/examples/src/filters/simplex-noise.mjs b/examples/src/filters/simplex-noise.mjs new file mode 100644 index 000000000..53855a457 --- /dev/null +++ b/examples/src/filters/simplex-noise.mjs @@ -0,0 +1,16 @@ +export default function () +{ + const app = this; + + app.addFilter('SimplexNoiseFilter', { + oncreate(folder) + { + folder.add(this, 'strength', 0, 1).name('strength'); + folder.add(this, 'noiseScale', 0, 50).name('noise scale'); + folder.add(this, 'offsetX', 0, 5).name('offsetX'); + folder.add(this, 'offsetY', 0, 5).name('offsetY'); + folder.add(this, 'offsetZ', 0, 5).name('offsetZ'); + folder.add(this, 'step', -1, 1).name('step'); + }, + }); +} diff --git a/package.json b/package.json index 8472e0338..14adc066f 100644 --- a/package.json +++ b/package.json @@ -168,6 +168,11 @@ "require": "./lib/simple-lightmap/index.js", "types": "./lib/simple-lightmap/index.d.ts" }, + "./simplex-noise": { + "import": "./lib/simplex-noise/index.mjs", + "require": "./lib/simplex-noise/index.js", + "types": "./lib/simplex-noise/index.d.ts" + }, "./tilt-shift": { "import": "./lib/tilt-shift/index.mjs", "require": "./lib/tilt-shift/index.js", diff --git a/scripts/screenshots/config.json b/scripts/screenshots/config.json index 5d4ef0678..c3d5623dc 100644 --- a/scripts/screenshots/config.json +++ b/scripts/screenshots/config.json @@ -585,6 +585,14 @@ "arguments": { "strength": 4 } + }, + { + "name": "SimplexNoiseFilter", + "filename": "simplex-noise", + "arguments": { + "strength": 0.5, + "noiseScale": 10 + } } ] } diff --git a/src/index.ts b/src/index.ts index 3a96e4b4a..9fc52c18f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,6 +32,7 @@ export * from './reflection'; export * from './rgb-split'; export * from './shockwave'; export * from './simple-lightmap'; +export * from './simplex-noise'; export * from './tilt-shift'; export * from './twist'; export * from './zoom-blur'; diff --git a/src/simplex-noise/SimplexNoiseFilter.ts b/src/simplex-noise/SimplexNoiseFilter.ts new file mode 100644 index 000000000..e02f339e8 --- /dev/null +++ b/src/simplex-noise/SimplexNoiseFilter.ts @@ -0,0 +1,143 @@ +import { Filter, GlProgram, GpuProgram } from 'pixi.js'; +import { vertex, wgslVertex } from '../defaults'; +import fragment from './simplex.frag'; +import source from './simplex.wgsl'; + +/** Options for the SimplexNoiseFilter constructor. */ +export interface SimplexNoiseFilterOptions +{ + /** + * Noise map strength. + * @default 0.5 + */ + strength?: number; + /** + * Noise map scale. + * @default 10.0 + */ + noiseScale?: number; + /** + * Horizontal offset for the noise map. + * @default 0 + */ + offsetX?: number; + /** + * Vertical offset for the noise map. + * @default 0 + */ + offsetY?: number; + /** + * Depth offset for the noise map. + * @default 0 + */ + offsetZ?: number; + /** + * The threshold used with the step function to create a blocky effect in the noise pattern. + * When this is greater than 0, the step function is used to compare the noise value to this threshold. + * @default -1 + */ + step?: number; +} + +/** + * The SimplexNoiseFilter multiplies simplex noise with the current texture data.
+ * ![original](../screenshots/original.png)![filter](../screenshots/simplex-noise.png) + * @class + * @extends Filter + * @see {@link https://www.npmjs.com/package/pixi-filters|pixi-filters} + */ +export class SimplexNoiseFilter extends Filter +{ + /** Default constructor options. */ + public static readonly defaults: SimplexNoiseFilterOptions = { + strength: 0.5, + noiseScale: 10.0, + offsetX: 0, + offsetY: 0, + offsetZ: 0, + step: -1, + }; + + /** + * @param options - Options for the SimplexNoise constructor. + */ + constructor(options?: SimplexNoiseFilterOptions) + { + options = { ...SimplexNoiseFilter.defaults, ...options }; + + const gpuProgram = GpuProgram.from({ + vertex: { + source: wgslVertex, + entryPoint: 'mainVertex', + }, + fragment: { + source, + entryPoint: 'mainFragment', + }, + }); + + const glProgram = GlProgram.from({ + vertex, + fragment, + name: 'simplex-filter', + }); + + super({ + gpuProgram, + glProgram, + resources: { + simplexUniforms: { + uStrength: { value: options?.strength ?? 0, type: 'f32' }, + uNoiseScale: { value: options?.noiseScale ?? 0, type: 'f32' }, + uOffsetX: { value: options?.offsetX ?? 0, type: 'f32' }, + uOffsetY: { value: options?.offsetY ?? 0, type: 'f32' }, + uOffsetZ: { value: options?.offsetZ ?? 0, type: 'f32' }, + uStep: { value: options?.step ?? 0, type: 'f32' }, + } + } + }); + } + + /** + * Strength of the noise (color = (noiseMap + strength) * texture) + * @default 0.5 + */ + get strength(): number { return this.resources.simplexUniforms.uniforms.uStrength; } + set strength(value: number) { this.resources.simplexUniforms.uniforms.uStrength = value; } + + /** + * Noise map scale. + * @default 10 + */ + get noiseScale(): number { return this.resources.simplexUniforms.uniforms.uNoiseScale; } + set noiseScale(value: number) { this.resources.simplexUniforms.uniforms.uNoiseScale = value; } + + /** + * Horizontal offset for the noise map. + * @default 0 + */ + get offsetX(): number { return this.resources.simplexUniforms.uniforms.uOffsetX; } + set offsetX(value: number) { this.resources.simplexUniforms.uniforms.uOffsetX = value; } + + /** + * Vertical offset for the noise map. + * @default 0 + */ + get offsetY(): number { return this.resources.simplexUniforms.uniforms.uOffsetY; } + set offsetY(value: number) { this.resources.simplexUniforms.uniforms.uOffsetY = value; } + + /** + * Depth offset for the noise map. + * @default 0 + */ + get offsetZ(): number { return this.resources.simplexUniforms.uniforms.uOffsetZ; } + set offsetZ(value: number) { this.resources.simplexUniforms.uniforms.uOffsetZ = value; } + + /** + * The threshold used with the step function to create a blocky effect in the noise pattern. + * When this is greater than 0, the step function is used to compare the noise value to this threshold. + * @default -1 + */ + get step(): number { return this.resources.simplexUniforms.uniforms.uStep; } + set step(value: number) { this.resources.simplexUniforms.uniforms.uStep = value; } +} diff --git a/src/simplex-noise/index.ts b/src/simplex-noise/index.ts new file mode 100644 index 000000000..e79f99fd3 --- /dev/null +++ b/src/simplex-noise/index.ts @@ -0,0 +1 @@ +export * from './SimplexNoiseFilter'; diff --git a/src/simplex-noise/simplex.frag b/src/simplex-noise/simplex.frag new file mode 100644 index 000000000..827f4a361 --- /dev/null +++ b/src/simplex-noise/simplex.frag @@ -0,0 +1,61 @@ +precision highp float; +in vec2 vTextureCoord; +out vec4 finalColor; + +uniform sampler2D uTexture; +uniform float uStrength; +uniform float uNoiseScale; +uniform float uOffsetX; +uniform float uOffsetY; +uniform float uOffsetZ; +uniform float uStep; + +uniform vec4 uInputSize; +uniform vec4 uInputClamp; + +//Noise from: https://www.shadertoy.com/view/4sc3z2 +const vec3 MOD3 = vec3(.1031,.11369,.13787); +vec3 hash33(vec3 p3) +{ + p3 = fract(p3 * MOD3); + p3 += dot(p3, p3.yxz+19.19); + return -1.0 + 2.0 * fract(vec3((p3.x + p3.y)*p3.z, (p3.x+p3.z)*p3.y, (p3.y+p3.z)*p3.x)); +} + +float simplex_noise(vec3 p) +{ + const float K1 = 0.333333333; + const float K2 = 0.166666667; + + vec3 i = floor(p + (p.x + p.y + p.z) * K1); + vec3 d0 = p - (i - (i.x + i.y + i.z) * K2); + + vec3 e = step(vec3(0.0), d0 - d0.yzx); + vec3 i1 = e * (1.0 - e.zxy); + vec3 i2 = 1.0 - e.zxy * (1.0 - e); + + vec3 d1 = d0 - (i1 - 1.0 * K2); + vec3 d2 = d0 - (i2 - 2.0 * K2); + vec3 d3 = d0 - (1.0 - 3.0 * K2); + + vec4 h = max(0.6 - vec4(dot(d0, d0), dot(d1, d1), dot(d2, d2), dot(d3, d3)), 0.0); + vec4 n = h * h * h * h * vec4(dot(d0, hash33(i)), dot(d1, hash33(i + i1)), dot(d2, hash33(i + i2)), dot(d3, hash33(i + 1.0))); + + return dot(vec4(31.316), n); +} + +void main(void) +{ + float noise = simplex_noise( + vec3(vTextureCoord*uNoiseScale+vec2(uOffsetX, uOffsetY), uOffsetZ) + ) * 0.5 + 0.5; + + noise += 2.0 * uStrength - 1.0; + noise = clamp(noise, 0.0, 1.0); + + if (uStep > 0.0) { //step > 0.5 + noise = 1.0 - step(noise, uStep); + } + + finalColor = texture(uTexture, vTextureCoord) * noise; +} diff --git a/src/simplex-noise/simplex.wgsl b/src/simplex-noise/simplex.wgsl new file mode 100644 index 000000000..ac089c332 --- /dev/null +++ b/src/simplex-noise/simplex.wgsl @@ -0,0 +1,61 @@ +struct SimplexUniforms { + uStrength:f32, + uNoiseScale:f32, + uOffsetX:f32, + uOffsetY:f32, + uOffsetZ:f32, + uStep:f32 +}; + +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 uTexture: texture_2d; +@group(0) @binding(2) var uSampler: sampler; +@group(1) @binding(0) var simplexUniforms : SimplexUniforms; + +@fragment +fn mainFragment( + @location(0) uv: vec2, + @builtin(position) position: vec4 +) -> @location(0) vec4 { + var noise: f32 = simplex_noise(vec3(uv * simplexUniforms.uNoiseScale + vec2(simplexUniforms.uOffsetX, simplexUniforms.uOffsetY), simplexUniforms.uOffsetZ)) * 0.5 + 0.5; + noise = noise + (2. * simplexUniforms.uStrength - 1.); + noise = clamp(noise, 0.0, 1.0); + if (simplexUniforms.uStep > 0.0) { + noise = 1. - step(noise, simplexUniforms.uStep); + } + return textureSample(uTexture, uSampler, uv) * noise; +} + +const MOD3: vec3 = vec3(0.1031, 0.11369, 0.13787); +fn hash33(p3: vec3) -> vec3 { + var p3_var = p3; + p3_var = fract(p3_var * MOD3); + p3_var = p3_var + (dot(p3_var, p3_var.yxz + 19.19)); + return -1. + 2. * fract(vec3((p3_var.x + p3_var.y) * p3_var.z, (p3_var.x + p3_var.z) * p3_var.y, (p3_var.y + p3_var.z) * p3_var.x)); +} + +fn simplex_noise(p: vec3) -> f32 { + let K1: f32 = 0.33333334; + let K2: f32 = 0.16666667; + let i: vec3 = floor(p + (p.x + p.y + p.z) * K1); + let d0: vec3 = p - (i - (i.x + i.y + i.z) * K2); + let e: vec3 = step(vec3(0.), d0 - d0.yzx); + let i1: vec3 = e * (1. - e.zxy); + let i2: vec3 = 1. - e.zxy * (1. - e); + let d1: vec3 = d0 - (i1 - 1. * K2); + let d2: vec3 = d0 - (i2 - 2. * K2); + let d3: vec3 = d0 - (1. - 3. * K2); + let h: vec4 = max(vec4(0.6) - vec4(dot(d0, d0), dot(d1, d1), dot(d2, d2), dot(d3, d3)), vec4(0.0)); + let n: vec4 = h * h * h * h * vec4(dot(d0, hash33(i)), dot(d1, hash33(i + i1)), dot(d2, hash33(i + i2)), dot(d3, hash33(i + 1.))); + return dot(vec4(31.316), n); +} \ No newline at end of file