diff --git a/filters/ascii/src/AsciiFilter.ts b/filters/ascii/src/AsciiFilter.ts index 67a41010e..a5006247e 100644 --- a/filters/ascii/src/AsciiFilter.ts +++ b/filters/ascii/src/AsciiFilter.ts @@ -1,11 +1,32 @@ -import { vertex } from '@tools/fragments'; +import { vertex, wgslVertex } from '@tools/fragments'; import fragment from './ascii.frag'; -import { Filter, GlProgram } from 'pixi.js'; +import source from './ascii.wgsl'; +import { Color, ColorSource, Filter, GlProgram, GpuProgram } from 'pixi.js'; -// TODO (cengler) - The Y is flipped in this shader for some reason. +// This WebGPU filter has been ported from the WebGL renderer that was originally created by Vico (@vicocotea) -// @author Vico @vicocotea -// original shader : https://www.shadertoy.com/view/lssGDj by @movAX13h +export interface AsciiFilterOptions +{ + /** + * The pixel size used by the filter + * @default 8 + */ + size?: number; + /** + * A color to set the ascii characters to. If not set, the color will be taken from the source. + * @example [1.0, 1.0, 1.0] = 0xffffff + * @default 0x000000 + */ + color?: ColorSource; + /** + * Determine whether or not to replace the source colors with the provided. + * + * Will automatically be assigned to `true` if `color` is provided. + * Set `replaceColor` to `false` to prevent that. + * @default false + */ + replaceColor?: boolean; +} /** * An ASCII filter.
@@ -18,11 +39,32 @@ import { Filter, GlProgram } from 'pixi.js'; */ export class AsciiFilter extends Filter { - /** - * @param {number} [size=8] - Size of the font - */ - constructor(size = 8) + /** Default values for options. */ + public static readonly DEFAULT_OPTIONS: AsciiFilterOptions = { + size: 8, + color: 0xffffff, + replaceColor: false, + }; + + private _color: Color; + + constructor(options?: AsciiFilterOptions) { + const replaceColor = options?.color && options.replaceColor !== false; + + options = { ...AsciiFilter.DEFAULT_OPTIONS, ...options } as AsciiFilterOptions; + + const gpuProgram = new GpuProgram({ + vertex: { + source: wgslVertex, + entryPoint: 'mainVertex', + }, + fragment: { + source, + entryPoint: 'mainFragment', + }, + }); + const glProgram = new GlProgram({ vertex, fragment, @@ -30,21 +72,43 @@ export class AsciiFilter extends Filter }); super({ + gpuProgram, glProgram, - resources: {}, + resources: { + asciiUniforms: { + uSize: { value: options.size, type: 'f32' }, + uColor: { value: new Float32Array(3), type: 'vec3' }, + uReplaceColor: { value: Number(replaceColor), type: 'f32' }, + } + }, }); - // this.size = size; + + this._color = new Color(); + this.color = options.color ?? 0xffffff; } /** * The pixel size used by the filter. + * @default 8 + */ + get size(): number { return this.resources.asciiUniforms.uniforms.uSize; } + set size(value: number) { this.resources.asciiUniforms.uniforms.uSize = value; } + + /** + * The resulting color of the ascii characters, as a 3 component RGB or numerical hex + * @example [1.0, 1.0, 1.0] = 0xffffff + * @default 0xffffff + */ + get color(): ColorSource { return this._color.value as ColorSource; } + set color(value: ColorSource) + { + this._color.setValue(value); + this.resources.asciiUniforms.uniforms.uColor = this._color.toArray().slice(0, 3); + } + + /** + * Determine whether or not to replace the source colors with the provided. */ - // get size(): number - // { - // return this.uniforms.pixelSize; - // } - // set size(value: number) - // { - // this.uniforms.pixelSize = value; - // } + get replaceColor(): boolean { return this.resources.asciiUniforms.uniforms.uReplaceColor > 0.5; } + set replaceColor(value: boolean) { this.resources.asciiUniforms.uniforms.uReplaceColor = value ? 1 : 0; } } diff --git a/filters/ascii/src/ascii.frag b/filters/ascii/src/ascii.frag index 192ff46e5..1ae7d8dcb 100644 --- a/filters/ascii/src/ascii.frag +++ b/filters/ascii/src/ascii.frag @@ -1,21 +1,26 @@ -varying vec2 vTextureCoord; +precision highp float; +in vec2 vTextureCoord; +out vec4 finalColor; -uniform vec4 filterArea; -uniform float pixelSize; uniform sampler2D uSampler; +uniform float uSize; +uniform vec3 uColor; +uniform float uReplaceColor; + +uniform vec4 uInputSize; vec2 mapCoord( vec2 coord ) { - coord *= filterArea.xy; - coord += filterArea.zw; + coord *= uInputSize.xy; + coord += uInputSize.zw; return coord; } vec2 unmapCoord( vec2 coord ) { - coord -= filterArea.zw; - coord /= filterArea.xy; + coord -= uInputSize.zw; + coord /= uInputSize.xy; return coord; } @@ -49,11 +54,11 @@ void main() vec2 coord = mapCoord(vTextureCoord); // get the grid position - vec2 pixCoord = pixelate(coord, vec2(pixelSize)); + vec2 pixCoord = pixelate(coord, vec2(uSize)); pixCoord = unmapCoord(pixCoord); // sample the color at grid position - vec4 color = texture2D(uSampler, pixCoord); + vec4 color = texture(uSampler, pixCoord); // brightness of the color as it's perceived by the human eye float gray = 0.3 * color.r + 0.59 * color.g + 0.11 * color.b; @@ -69,8 +74,7 @@ void main() if (gray > 0.8) n = 11512810.0; // # // get the mod.. - vec2 modd = getMod(coord, vec2(pixelSize)); - - gl_FragColor = color * character( n, vec2(-1.0) + modd * 2.0); + vec2 modd = getMod(coord, vec2(uSize)); + finalColor = (uReplaceColor > 0.5 ? vec4(uColor, 1.) : color) * character( n, vec2(-1.0) + modd * 2.0); } diff --git a/filters/ascii/src/ascii.wgsl b/filters/ascii/src/ascii.wgsl new file mode 100644 index 000000000..8325a72f3 --- /dev/null +++ b/filters/ascii/src/ascii.wgsl @@ -0,0 +1,108 @@ +struct AsciiUniforms { + uSize: f32, + uColor: vec3, + uReplaceColor: f32, +}; + +struct GlobalFilterUniforms { + uInputSize:vec4, + uInputPixel:vec4, + uuInputClamp: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 asciiUniforms : AsciiUniforms; + +@fragment +fn mainFragment( + @location(0) uv: vec2, + @builtin(position) position: vec4 +) -> @location(0) vec4 { + let pixelSize: f32 = asciiUniforms.uSize; + let coord: vec2 = mapCoord(uv); + + // get the rounded color.. + var pixCoord: vec2 = pixelate(coord, vec2(pixelSize)); + pixCoord = unmapCoord(pixCoord); + + var color = textureSample(uSampler, uSampler, pixCoord); + + // determine the character to use + let gray: f32 = 0.3 * color.r + 0.59 * color.g + 0.11 * color.b; + + var n: f32 = 65536.0; // . + if (gray > 0.2) { + n = 65600.0; // : + } + if (gray > 0.3) { + n = 332772.0; // * + } + if (gray > 0.4) { + n = 15255086.0; // o + } + if (gray > 0.5) { + n = 23385164.0; // & + } + if (gray > 0.6) { + n = 15252014.0; // 8 + } + if (gray > 0.7) { + n = 13199452.0; // @ + } + if (gray > 0.8) { + n = 11512810.0; // # + } + + // get the mod.. + let modd: vec2 = getMod(coord, vec2(pixelSize)); + return select(color, vec4(asciiUniforms.uColor, 1.), asciiUniforms.uReplaceColor > 0.5) * character(n, vec2(-1.0) + modd * 2.0); +} + +fn pixelate(coord: vec2, size: vec2) -> vec2 +{ + return floor( coord / size ) * size; +} + +fn getMod(coord: vec2, size: vec2) -> vec2 +{ + return moduloVec2( coord , size) / size; +} + +fn character(n: f32, p: vec2) -> f32 +{ + var q: vec2 = floor(p*vec2(4.0, 4.0) + 2.5); + + if (clamp(q.x, 0.0, 4.0) == q.x) + { + if (clamp(q.y, 0.0, 4.0) == q.y) + { + if (i32(modulo(n/exp2(q.x + 5.0*q.y), 2.0)) == 1) + { + return 1.0; + } + } + } + + return 0.0; +} + +fn mapCoord(coord: vec2 ) -> vec2 +{ + var mappedCoord: vec2 = coord; + mappedCoord *= gfu.inputSize.xy; + mappedCoord += gfu.outputFrame.xy; + return mappedCoord; +} + +fn unmapCoord(coord: vec2 ) -> vec2 +{ + var mappedCoord: vec2 = coord; + mappedCoord -= gfu.outputFrame.xy; + mappedCoord /= gfu.inputSize.xy; + return mappedCoord; +} \ No newline at end of file diff --git a/tools/demo/src/filters/ascii.js b/tools/demo/src/filters/ascii.js index cae229f60..5238e64e7 100644 --- a/tools/demo/src/filters/ascii.js +++ b/tools/demo/src/filters/ascii.js @@ -3,5 +3,7 @@ export default function () this.addFilter('AsciiFilter', function (folder) { folder.add(this, 'size', 2, 20); + folder.addColor(this, 'color'); + folder.add(this, 'replaceColor'); }); } diff --git a/tools/demo/src/index.js b/tools/demo/src/index.js index 5aef3fe2c..a2d33ae05 100644 --- a/tools/demo/src/index.js +++ b/tools/demo/src/index.js @@ -40,6 +40,7 @@ const main = async () => filters.glow.call(app); filters.hslAdjustment.call(app); filters.rgb.call(app); + filters.ascii.call(app); // filters.kawaseBlur.call(app); // TODO: Re-enable this in place of the above once v8 conversion is complete