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