Skip to content

Instantly share code, notes, and snippets.

@ssube
Last active June 25, 2020 05:06
Show Gist options
  • Select an option

  • Save ssube/96e9db52bbb6e310fc4096872ed027bb to your computer and use it in GitHub Desktop.

Select an option

Save ssube/96e9db52bbb6e310fc4096872ed027bb to your computer and use it in GitHub Desktop.

Revisions

  1. ssube revised this gist Jun 25, 2020. 3 changed files with 218 additions and 51 deletions.
    28 changes: 24 additions & 4 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -18,6 +18,7 @@ A flexible, layered post-processing system for PhaserJS 3. Apply fullscreen shad
    - [`PostProcessor.update`](#postprocessorupdate)
    - [`PostProcessor.stop`](#postprocessorstop)
    - [Data](#data)
    - [Data `buffers`](#data-buffers)
    - [Data `effects`](#data-effects)
    - [Data `layers`](#data-layers)
    - [Data `mipmaps`](#data-mipmaps)
    @@ -48,6 +49,12 @@ A flexible, layered post-processing system for PhaserJS 3. Apply fullscreen shad

    ## Features

    - multiple input and output textures
    - offscreen buffers
    - access to textures loaded by Phaser
    - reusable `effects` composed into `layers`
    - set uniforms per layer

    ### Caveats

    - creates N+2 screen-sized textures for N effects
    @@ -63,7 +70,6 @@ A flexible, layered post-processing system for PhaserJS 3. Apply fullscreen shad

    - more effects
    - support binding sampler to TEXTURE0 other than `stage` buffer (bound by draw call)
    - support writing to any target buffer, not just `stage`

    ## API

    @@ -102,11 +108,21 @@ The data structure used to initialize a `PostProcessor` instance describes the l
    example is attached.

    - `name`
    - `buffers`
    - `effects`
    - `layers`
    - `mipmaps`
    - `screen`

    ### Data `buffers`

    Additional offscreen buffers to create. The `scene` and `stage` buffers will always be created.

    - `name`
    - `size`
    - `x`
    - `y`

    ### Data `effects`

    The effect pipelines with shader source.
    @@ -146,10 +162,14 @@ The screen size.

    #### Dither

    Reduce color space to pseudo 8-bit.
    TODO

    Reduce color space to pseudo 8-bit and scatter pixels.

    #### Edge Detect

    TODO

    Laplacian edge detection.

    #### Invert
    @@ -172,7 +192,7 @@ Gradiated desaturation with circular black edge.

    #### Madd (Mult, Sum)

    Add the previos `stage` to the original `scene`.
    Add the previous `stage` to the original `scene`.

    #### Diff

    @@ -190,7 +210,7 @@ Offset pixels from the previous `stage` using a normal map from another texture.

    #### Gaussian

    TODO
    Separable blur of 7 pixels using the Gaussian kernel: `[0.00038771, 0.01330373, 0.11098164, 0.22508352, 0.00038771, 0.01330373, 0.11098164]`.

    ### Tone Mapping

    119 changes: 110 additions & 9 deletions data.yml
    Original file line number Diff line number Diff line change
    @@ -1,24 +1,41 @@
    pipelines:
    - name: basic
    buffers:
    - name: offscreen
    layers:
    - name: clamp
    targets:
    - name: stage
    - name: offscreen
    uniforms:
    - name: uMin
    type: float
    value: 0.5
    - name: uMax
    type: float
    value: 0.9
    - name: blurX
    - name: blurY
    - name: blurX
    - name: blurY
    - name: madd
    - name: distort
    - name: exposure
    uniforms:
    - name: uTarget
    type: float
    value: 0.5
    # name: sepia
    - name: sepia
    - name: vignette
    - name: color-curve
    uniforms:
    - name: anchor1
    type: vec2
    value: [1.0, 0.0]
    - name: anchor2
    type: vec2
    value: [0.0, 1.0]
    - name: distort
    - name: recombine
    mipmaps: true
    screen:
    x: 1024
    @@ -245,11 +262,14 @@ pipelines:
    {
    vec2 uv = outTexCoord.xy;
    vec4 texel = texture2D(foo, uv);
    vec4 faded = texel;

    /* kernel from https://gist.github.com/rasteron/2019a4890e0d6311297f */
    gl_FragColor.x = dot(texel.xyz, vec3(0.393, 0.769, 0.189));
    gl_FragColor.y = dot(texel.xyz, vec3(0.349, 0.686, 0.168));
    gl_FragColor.z = dot(texel.xyz, vec3(0.272, 0.534, 0.131));
    faded.x = dot(texel.xyz, vec3(0.393, 0.769, 0.189));
    faded.y = dot(texel.xyz, vec3(0.349, 0.686, 0.168));
    faded.z = dot(texel.xyz, vec3(0.272, 0.534, 0.131));

    gl_FragColor.xyz = mix(texel.xyz, faded.xyz, step(0.5, uv.x));
    gl_FragColor.w = 1.0;
    }
    - name: vignette
    @@ -300,11 +320,11 @@ pipelines:
    vec2 normal = texture2D(normal, uv).xy;
    normal = normalize(normal * 2.0 - 1.0);

    vec2 offset = normal * pixel * 10.0;
    vec2 offset = normal * pixel * 8.0;
    vec4 texel = texture2D(stage, uv + offset);
    vec4 original = texture2D(stage, uv);

    gl_FragColor.xyz = mix(texel.xyz, original.xyz, step(0.5, uv.x));
    gl_FragColor.xyz = (original.xyz + mix(texel.xyz, original.xyz, step(0.5, uv.x))) / 2.0;
    gl_FragColor.w = 1.0;
    }
    - name: color-curve
    @@ -316,10 +336,11 @@ pipelines:

    const vec3 lumF = vec3(0.2125, 0.7145, 0.0721);
    const vec2 anchor0 = vec2(0.0, 0.0);
    const vec2 anchor1 = vec2(0.0, 1.0);
    const vec2 anchor2 = vec2(1.0, 0.0);
    const vec2 anchor3 = vec2(1.0, 1.0);

    uniform vec2 anchor1;
    uniform vec2 anchor2;

    uniform sampler2D stage;

    varying vec2 outTexCoord;
    @@ -343,3 +364,83 @@ pipelines:
    /* gl_FragColor.xyz = vec3(dist); */
    gl_FragColor.w = 1.0;
    }
    - name: gaussianX
    samplers:
    - name: foo
    source: stage
    fragment:
    precision mediump float;

    uniform sampler2D foo;

    varying vec2 outTexCoord;
    varying vec4 outTint;

    void main()
    {
    vec2 pixel = vec2(1.0) / vec2(1024.0, 768.0);
    vec2 uv = outTexCoord.xy;

    vec4 texN3 = texture2D(foo, vec2(uv.x - (pixel.x * 3.0), uv.y)) * 0.00038771;
    vec4 texN2 = texture2D(foo, vec2(uv.x - (pixel.x * 2.0), uv.y)) * 0.01330373;
    vec4 texN1 = texture2D(foo, vec2(uv.x - (pixel.x * 1.0), uv.y)) * 0.11098164;
    vec4 tex00 = texture2D(foo, uv) * 0.22508352;
    vec4 texP1 = texture2D(foo, vec2(uv.x + (pixel.x * 1.0), uv.y)) * 0.11098164;
    vec4 texP2 = texture2D(foo, vec2(uv.x + (pixel.x * 2.0), uv.y)) * 0.01330373;
    vec4 texP3 = texture2D(foo, vec2(uv.x + (pixel.x * 3.0), uv.y)) * 0.00038771;

    gl_FragColor = (texN3 + texN2 + texN1 + tex00 + texP1 + texP2 + texP3);
    gl_FragColor.w = 1.0;
    }
    - name: gaussianY
    samplers:
    - name: foo
    source: stage
    fragment:
    precision mediump float;

    uniform sampler2D foo;

    varying vec2 outTexCoord;
    varying vec4 outTint;

    void main()
    {
    vec2 pixel = vec2(1.0) / vec2(1024.0, 768.0);
    vec2 uv = outTexCoord.xy;

    vec4 texN3 = texture2D(foo, vec2(uv.x, uv.y - (pixel.y * 3.0))) * 0.00038771;
    vec4 texN2 = texture2D(foo, vec2(uv.x, uv.y - (pixel.y * 2.0))) * 0.01330373;
    vec4 texN1 = texture2D(foo, vec2(uv.x, uv.y - (pixel.y * 1.0))) * 0.11098164;
    vec4 tex00 = texture2D(foo, uv) * 0.22508352;
    vec4 texP1 = texture2D(foo, vec2(uv.x, uv.y + (pixel.y * 1.0))) * 0.11098164;
    vec4 texP2 = texture2D(foo, vec2(uv.x, uv.y + (pixel.y * 2.0))) * 0.01330373;
    vec4 texP3 = texture2D(foo, vec2(uv.x, uv.y + (pixel.y * 3.0))) * 0.00038771;

    gl_FragColor = (texN3 + texN2 + texN1 + tex00 + texP1 + texP2 + texP3);
    gl_FragColor.w = 1.0;
    }
    - name: recombine
    samplers:
    - name: stage
    source: stage
    - name: offscreen
    source: offscreen
    fragment:
    precision mediump float;

    uniform sampler2D stage;
    uniform sampler2D offscreen;

    varying vec2 outTexCoord;

    void main() {
    vec2 uv = outTexCoord.xy;
    uv.y = 1.0 - uv.y;

    vec4 texel = texture2D(stage, outTexCoord);
    vec4 glow = texture2D(offscreen, uv);

    gl_FragColor.xyz = texel.xyz + glow.xyz;
    gl_FragColor.w = 1.0;
    }
    122 changes: 84 additions & 38 deletions postprocessor.ts
    Original file line number Diff line number Diff line change
    @@ -10,6 +10,10 @@ export interface EffectSampler {
    source: string;
    }

    export interface EffectTarget {
    name: string;
    }

    export interface EffectUniformSingle {
    name: string;
    type: 'float';
    @@ -24,23 +28,31 @@ export interface EffectUniformVector {

    export type EffectUniform = EffectUniformSingle | EffectUniformVector;

    export interface PipelineEffect {
    export interface PipelineBuffer {
    mipmaps: boolean;
    name: string;
    size: Point;
    }

    export interface PipelineEffect {
    fragment: string;
    name: string;
    samplers: Array<EffectSampler>;
    vertex: string;
    }

    export interface PipelineLayer {
    name: string;
    targets: Array<EffectTarget>;
    uniforms: Array<EffectUniform>;
    }

    export interface PipelineData {
    name: string;
    buffers: Array<PipelineBuffer>;
    effects: Array<PipelineEffect>;
    layers: Array<PipelineLayer>;
    mipmaps: boolean;
    name: string;
    screen: Point;
    }

    @@ -105,25 +117,9 @@ class EffectPipeline extends Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline
    return this;
    }

    public cacheTextures(samplers: Array<EffectSampler>, buffers: Map<string, Phaser.GameObjects.RenderTexture>, textures: Phaser.Textures.TextureManager) {
    for (const { name, source } of samplers) {
    if (buffers.has(source)) {
    const texture = mustGet(buffers, source);
    this.samplers.push({
    name,
    texture,
    type: 'buffer',
    });
    } else {
    const texture = textures.get(source);
    this.samplers.push({
    frame: texture.firstFrame,
    name,
    texture,
    type: 'texture',
    });
    }
    }
    public cacheTextures(samplers: Array<CachedSampler>) {
    this.samplers.splice(0);
    this.samplers.push(...samplers);
    }
    }

    @@ -170,40 +166,73 @@ export class PostProcessor {
    this.buffers.set(buffer, rt);
    }

    for (const buffer of this.data.buffers) {
    const rt = this.createBuffer(buffer.name);
    this.buffers.set(buffer.name, rt);
    }

    for (const effect of this.data.effects) {
    const game = this.scene.game;
    const pipeline = new EffectPipeline(game, effect);
    this.effects.set(effect.name, pipeline);

    this.renderer.addPipeline(effect.name, pipeline);
    const qn = this.qualifyName(effect.name);
    this.renderer.addPipeline(qn, pipeline);

    const rt = this.createBuffer(effect.name);
    rt.setPipeline(effect.name);
    rt.setPipeline(qn);
    this.buffers.set(effect.name, rt);
    }

    this.cacheTextures();
    }

    public cacheTextures() {
    /* this should happen after all effects (and their buffers) have been created and registered */
    for (const data of this.data.effects) {
    const effect = mustGet(this.effects, data.name);
    effect.cacheTextures(data.samplers, this.buffers, this.scene.textures);
    const cache: Array<CachedSampler> = [];

    for (const { name, source } of data.samplers) {
    if (this.buffers.has(source)) {
    const texture = mustGet(this.buffers, source);
    cache.push({
    name,
    texture,
    type: 'buffer',
    });
    } else {
    // source is not a name and should not be qualified
    const texture = this.scene.textures.get(source);
    cache.push({
    frame: texture.firstFrame,
    name,
    texture,
    type: 'texture',
    });
    }
    }

    effect.cacheTextures(cache);
    }

    this.running = true;
    }

    public createBuffer(name: string) {
    const qn = this.qualifyName(name);
    const rt = this.scene.make.renderTexture({
    height: this.data.screen.y,
    width: this.data.screen.x,
    x: 0,
    y: 0,
    });

    rt.setName(name);
    rt.setName(qn);
    rt.setScrollFactor(0, 0);
    rt.setVisible(false);

    this.scene.textures.addRenderTexture(name, rt);
    this.scene.textures.addRenderTexture(qn, rt);

    return rt;
    }
    @@ -230,21 +259,33 @@ export class PostProcessor {
    stageBuffer.draw(sceneBuffer);

    for (const layer of layers) {
    const buffer = mustGet(this.buffers, layer.name);
    const effect = mustGet(this.effects, layer.name);
    this.updateLayer(layer, stageBuffer);
    }
    }

    if (doesExist(layer.uniforms)) {
    for (const uniform of layer.uniforms) {
    this.updateUniform(effect, uniform);
    }
    public updateLayer(layer: PipelineLayer, stageBuffer: Phaser.GameObjects.RenderTexture) {
    const buffer = mustGet(this.buffers, layer.name);
    const effect = mustGet(this.effects, layer.name);

    if (doesExist(layer.uniforms)) {
    for (const uniform of layer.uniforms) {
    this.updateUniform(effect, uniform);
    }
    }

    buffer.draw(stageBuffer);
    stageBuffer.draw(buffer);
    buffer.draw(stageBuffer);

    if (this.data.mipmaps) {
    this.updateMips(stageBuffer);
    if (doesExist(layer.targets)) {
    for (const data of layer.targets) {
    const target = mustGet(this.buffers, data.name);
    target.draw(buffer);
    }
    } else {
    stageBuffer.draw(buffer);
    }

    if (this.data.mipmaps) {
    this.updateMips(stageBuffer);
    }
    }

    @@ -302,8 +343,13 @@ export class PostProcessor {
    this.scene.children.remove(fillRect);

    for (const name of this.buffers.keys()) {
    this.renderer.removePipeline(name);
    this.scene.textures.remove(name);
    const qn = this.qualifyName(name);
    this.renderer.removePipeline(qn);
    this.scene.textures.remove(qn);
    }
    }

    protected qualifyName(name: string): string {
    return `postfx-${this.data.name}-${name}`;
    }
    }
  2. ssube revised this gist Jun 25, 2020. 1 changed file with 18 additions and 8 deletions.
    26 changes: 18 additions & 8 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -24,6 +24,8 @@ A flexible, layered post-processing system for PhaserJS 3. Apply fullscreen shad
    - [Data `screen`](#data-screen)
    - [Effects](#effects)
    - [Basics](#basics)
    - [Dither](#dither)
    - [Edge Detect](#edge-detect)
    - [Invert](#invert)
    - [Monochrome](#monochrome)
    - [Sepia](#sepia)
    @@ -51,15 +53,17 @@ A flexible, layered post-processing system for PhaserJS 3. Apply fullscreen shad
    - creates N+2 screen-sized textures for N effects
    - this can potentially be reduced to 2 or 3 (no N) by calling `buffer.setPipeline()` before `stage.draw(buffer)`
    - generating mipmaps for NP2 textures require a WebGL2 context
    - the `textureLod` GLSL function requires a WebGL2 context and `#version 300 es` pragmas
    - the versions must match between fragment and vertex programs in the same layer, so this requires a `300 es` vertex shader
    - mixing versions on different layers is allowed
    - the `exposure` effect requires WebGL2
    - the Phaser game must be created with the same WebGL2 context
    - `textureLod` GLSL function requires `#version 300 es` shaders
    - the fragment and vertex program version must match within the same layer (a `300 es` vertex shader is provided)
    - mixing shader versions on different layers is allowed

    ### TODO

    - more effects
    - support binding sampler to TEXTURE0 other than stage (forced by draw call)
    - set uniforms other than float1
    - support binding sampler to TEXTURE0 other than `stage` buffer (bound by draw call)
    - support writing to any target buffer, not just `stage`

    ## API

    @@ -140,6 +144,14 @@ The screen size.

    ### Basics

    #### Dither

    Reduce color space to pseudo 8-bit.

    #### Edge Detect

    Laplacian edge detection.

    #### Invert

    Invert colors per-channel.
    @@ -197,9 +209,7 @@ This effect can act as a low-pass/high-cut or high-pass/low-cut filter by settin

    #### Curve

    TODO

    Adjust scene colors using a 2-point curve.
    Adjust scene luminance using a 4-point curve.

    #### Color

  3. ssube revised this gist Jun 25, 2020. 1 changed file with 15 additions and 9 deletions.
    24 changes: 15 additions & 9 deletions postprocessor.ts
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,5 @@
    import { doesExist, mustExist, mustGet } from '@apextoaster/js-utils';
    import { defaultTo } from 'lodash';
    import * as Phaser from 'phaser';

    import { CENTER_SPLIT } from '../constants';
    @@ -51,8 +52,13 @@ export type CachedSampler = {
    name: string;
    type: 'texture';
    texture: Phaser.Textures.Texture;
    frame: string;
    };

    export interface FrameDict {
    [key: string]: Phaser.Textures.Frame;
    }

    const REQUIRED_BUFFERS = [
    'scene',
    'stage',
    @@ -87,13 +93,13 @@ class EffectPipeline extends Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline
    const sampler = this.samplers[i];
    if (sampler.type === 'buffer') {
    renderer.setTexture2D(sampler.texture.glTexture, i);
    renderer.setInt1(program, sampler.name, i);
    } else {
    /* eslint-disable-next-line */
    const frame = (sampler.texture.frames as any)[sampler.texture.firstFrame] as Phaser.Textures.Frame;
    const frameName = defaultTo(sampler.frame, sampler.texture.firstFrame);
    const frame = (sampler.texture.frames as FrameDict)[frameName];
    renderer.setTexture2D(frame.glTexture, i);
    renderer.setInt1(program, sampler.name, i);
    }

    renderer.setInt1(program, sampler.name, i);
    }

    return this;
    @@ -111,6 +117,7 @@ class EffectPipeline extends Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline
    } else {
    const texture = textures.get(source);
    this.samplers.push({
    frame: texture.firstFrame,
    name,
    texture,
    type: 'texture',
    @@ -148,10 +155,10 @@ export class PostProcessor {
    }

    public getContext(): WebGL2RenderingContext {
    return this.getRenderer().gl as WebGL2RenderingContext;
    return this.renderer.gl as WebGL2RenderingContext;
    }

    public getRenderer(): Phaser.Renderer.WebGL.WebGLRenderer {
    public get renderer(): Phaser.Renderer.WebGL.WebGLRenderer {
    return this.scene.game.renderer as Phaser.Renderer.WebGL.WebGLRenderer;
    }

    @@ -168,7 +175,7 @@ export class PostProcessor {
    const pipeline = new EffectPipeline(game, effect);
    this.effects.set(effect.name, pipeline);

    this.getRenderer().addPipeline(effect.name, pipeline);
    this.renderer.addPipeline(effect.name, pipeline);

    const rt = this.createBuffer(effect.name);
    rt.setPipeline(effect.name);
    @@ -294,9 +301,8 @@ export class PostProcessor {
    const fillRect = mustExist(this.fillRect);
    this.scene.children.remove(fillRect);

    const renderer = this.getRenderer();
    for (const name of this.buffers.keys()) {
    renderer.removePipeline(name);
    this.renderer.removePipeline(name);
    this.scene.textures.remove(name);
    }
    }
  4. ssube revised this gist Jun 24, 2020. 1 changed file with 37 additions and 3 deletions.
    40 changes: 37 additions & 3 deletions postprocessor.ts
    Original file line number Diff line number Diff line change
    @@ -9,11 +9,20 @@ export interface EffectSampler {
    source: string;
    }

    export interface EffectUniform {
    export interface EffectUniformSingle {
    name: string;
    type: 'float';
    value: number;
    }

    export interface EffectUniformVector {
    name: string;
    type: 'float1' | 'float2' | 'float3' | 'float4' | 'vec1' | 'vec2' | 'vec3' | 'vec4';
    value: Array<number>;
    }

    export type EffectUniform = EffectUniformSingle | EffectUniformVector;

    export interface PipelineEffect {
    name: string;
    fragment: string;
    @@ -219,7 +228,7 @@ export class PostProcessor {

    if (doesExist(layer.uniforms)) {
    for (const uniform of layer.uniforms) {
    effect.setFloat1(uniform.name, uniform.value);
    this.updateUniform(effect, uniform);
    }
    }

    @@ -254,14 +263,39 @@ export class PostProcessor {
    gl.activeTexture(active);
    }

    public updateUniform(effect: EffectPipeline, uniform: EffectUniform) {
    switch (uniform.type) {
    case 'float1':
    case 'vec1':
    effect.setFloat1v(uniform.name, Float32Array.from(uniform.value));
    break;
    case 'float2':
    case 'vec2':
    effect.setFloat2v(uniform.name, Float32Array.from(uniform.value));
    break;
    case 'float3':
    case 'vec3':
    effect.setFloat3v(uniform.name, Float32Array.from(uniform.value));
    break;
    case 'float4':
    case 'vec4':
    effect.setFloat4v(uniform.name, Float32Array.from(uniform.value));
    break;
    case 'float':
    default:
    effect.setFloat1(uniform.name, uniform.value);
    break;
    }
    }

    public stop() {
    this.running = false;

    const fillRect = mustExist(this.fillRect);
    this.scene.children.remove(fillRect);

    const renderer = this.getRenderer();
    for (const name of this.buffers.keys()) {
    const renderer = this.scene.game.renderer as Phaser.Renderer.WebGL.WebGLRenderer;
    renderer.removePipeline(name);
    this.scene.textures.remove(name);
    }
  5. ssube revised this gist Jun 24, 2020. 1 changed file with 37 additions and 1 deletion.
    38 changes: 37 additions & 1 deletion data.yml
    Original file line number Diff line number Diff line change
    @@ -306,4 +306,40 @@ pipelines:

    gl_FragColor.xyz = mix(texel.xyz, original.xyz, step(0.5, uv.x));
    gl_FragColor.w = 1.0;
    }
    }
    - name: color-curve
    samplers:
    - name: stage
    source: stage
    fragment:
    precision mediump float;

    const vec3 lumF = vec3(0.2125, 0.7145, 0.0721);
    const vec2 anchor0 = vec2(0.0, 0.0);
    const vec2 anchor1 = vec2(0.0, 1.0);
    const vec2 anchor2 = vec2(1.0, 0.0);
    const vec2 anchor3 = vec2(1.0, 1.0);

    uniform sampler2D stage;

    varying vec2 outTexCoord;
    varying vec4 outTint;

    void main()
    {
    vec2 uv = outTexCoord.xy;
    vec4 texel = texture2D(stage, uv);

    float lum = dot(texel.xyz, lumF);
    vec2 curve = /* 4-point bezier */
    (pow(1.0 - lum, 3.0) * anchor0) +
    (pow(1.0 - lum, 2.0) * anchor1 * 3.0 * lum) +
    ((1.0 - lum) * anchor2 * 3.0 * pow(lum, 2.0)) +
    (pow(lum, 3.0) * anchor3);

    float dist = 0.5 + pow(curve.y - lum, 0.5);

    gl_FragColor.xyz = texel.xyz * dist;
    /* gl_FragColor.xyz = vec3(dist); */
    gl_FragColor.w = 1.0;
    }
  6. ssube revised this gist Jun 24, 2020. 1 changed file with 18 additions and 6 deletions.
    24 changes: 18 additions & 6 deletions postprocessor.ts
    Original file line number Diff line number Diff line change
    @@ -138,6 +138,14 @@ export class PostProcessor {
    return mustGet(this.buffers, key);
    }

    public getContext(): WebGL2RenderingContext {
    return this.getRenderer().gl as WebGL2RenderingContext;
    }

    public getRenderer(): Phaser.Renderer.WebGL.WebGLRenderer {
    return this.scene.game.renderer as Phaser.Renderer.WebGL.WebGLRenderer;
    }

    public create() {
    this.fillRect = this.scene.add.rectangle(0, 0, this.data.screen.x, this.data.screen.y, 0x00, 1.0);

    @@ -151,14 +159,14 @@ export class PostProcessor {
    const pipeline = new EffectPipeline(game, effect);
    this.effects.set(effect.name, pipeline);

    const renderer = game.renderer as Phaser.Renderer.WebGL.WebGLRenderer;
    renderer.addPipeline(effect.name, pipeline);
    this.getRenderer().addPipeline(effect.name, pipeline);

    const rt = this.createBuffer(effect.name);
    rt.setPipeline(effect.name);
    this.buffers.set(effect.name, rt);
    }

    /* this should happen after all effects (and their buffers) have been created and registered */
    for (const data of this.data.effects) {
    const effect = mustGet(this.effects, data.name);
    effect.cacheTextures(data.samplers, this.buffers, this.scene.textures);
    @@ -184,7 +192,7 @@ export class PostProcessor {
    return rt;
    }

    public update(objects: Array<RenderObject>, layers: Array<PipelineLayer>, camera: Phaser.Cameras.Scene2D.Camera, gl: WebGL2RenderingContext) {
    public update(objects: Array<RenderObject>, layers: Array<PipelineLayer>, camera: Phaser.Cameras.Scene2D.Camera) {
    if (!this.running) {
    return;
    }
    @@ -194,12 +202,13 @@ export class PostProcessor {

    const fillRect = mustExist(this.fillRect);
    sceneBuffer.draw(fillRect, fillRect.displayWidth / CENTER_SPLIT, fillRect.displayHeight / CENTER_SPLIT);

    for (const obj of objects) {
    sceneBuffer.draw(obj, Math.floor(-camera.scrollX + obj.x), Math.floor(-camera.scrollY + obj.y));
    }

    if (this.data.mipmaps) {
    this.updateMips(sceneBuffer, gl);
    this.updateMips(sceneBuffer);
    }

    stageBuffer.draw(sceneBuffer);
    @@ -218,12 +227,15 @@ export class PostProcessor {
    stageBuffer.draw(buffer);

    if (this.data.mipmaps) {
    this.updateMips(stageBuffer, gl);
    this.updateMips(stageBuffer);
    }
    }
    }

    public updateMips(buffer: Phaser.GameObjects.RenderTexture, gl: WebGL2RenderingContext) {
    public updateMips(buffer: Phaser.GameObjects.RenderTexture) {
    // use the RenderTexture's context in case it is different
    const gl = (buffer.renderer as Phaser.Renderer.WebGL.WebGLRenderer).gl as WebGL2RenderingContext;

    const active = gl.getParameter(gl.ACTIVE_TEXTURE);
    const levels = Math.ceil(Math.log2(Math.max(buffer.height, buffer.width)));

  7. ssube revised this gist Jun 24, 2020. 2 changed files with 19 additions and 9 deletions.
    8 changes: 8 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -42,6 +42,7 @@ A flexible, layered post-processing system for PhaserJS 3. Apply fullscreen shad
    - [Exposure](#exposure)
    - [Compound Effects](#compound-effects)
    - [Bloom](#bloom)
    - [License](#license)

    ## Features

    @@ -226,3 +227,10 @@ much more complex effects.
    - X
    - Y
    - Sum

    ## License

    This is not really much code, needs plenty of cleanup, and none of the shaders are original.

    The `postprocessor.ts` and `data.yml` files are public domain, excluding the `exposure` effect's `vertex` shader,
    which was copied from Phaser's source.
    20 changes: 11 additions & 9 deletions postprocessor.ts
    Original file line number Diff line number Diff line change
    @@ -34,6 +34,16 @@ export interface PipelineData {
    screen: Point;
    }

    export type CachedSampler = {
    name: string;
    type: 'buffer';
    texture: Phaser.GameObjects.RenderTexture;
    } | {
    name: string;
    type: 'texture';
    texture: Phaser.Textures.Texture;
    };

    const REQUIRED_BUFFERS = [
    'scene',
    'stage',
    @@ -44,15 +54,7 @@ const MAX_SAMPLERS = 8;
    export type RenderObject = Phaser.GameObjects.GameObject & Phaser.GameObjects.Components.Transform;

    class EffectPipeline extends Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline {
    protected samplers: Array<{
    name: string;
    type: 'buffer';
    texture: Phaser.GameObjects.RenderTexture;
    } | {
    name: string;
    type: 'texture';
    texture: Phaser.Textures.Texture;
    }>;
    protected samplers: Array<CachedSampler>;

    constructor(game: Phaser.Game, effect: PipelineEffect) {
    super({
  8. ssube revised this gist Jun 24, 2020. 1 changed file with 60 additions and 6 deletions.
    66 changes: 60 additions & 6 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -17,6 +17,11 @@ A flexible, layered post-processing system for PhaserJS 3. Apply fullscreen shad
    - [`PostProcessor.create`](#postprocessorcreate)
    - [`PostProcessor.update`](#postprocessorupdate)
    - [`PostProcessor.stop`](#postprocessorstop)
    - [Data](#data)
    - [Data `effects`](#data-effects)
    - [Data `layers`](#data-layers)
    - [Data `mipmaps`](#data-mipmaps)
    - [Data `screen`](#data-screen)
    - [Effects](#effects)
    - [Basics](#basics)
    - [Invert](#invert)
    @@ -27,8 +32,9 @@ A flexible, layered post-processing system for PhaserJS 3. Apply fullscreen shad
    - [Madd (Mult, Sum)](#madd-mult-sum)
    - [Diff](#diff)
    - [Blur](#blur)
    - [Basic X](#basic-x)
    - [Basic Y](#basic-y)
    - [Basic](#basic)
    - [Distort](#distort)
    - [Gaussian](#gaussian)
    - [Tone Mapping](#tone-mapping)
    - [Clamp](#clamp)
    - [Curve](#curve)
    @@ -85,6 +91,50 @@ Render a list of `objects` using the post-processing `layers` positioned for the

    Shut down the post-processing pipelines and clean up cached textures and WebGL resources.

    ## Data

    The data structure used to initialize a `PostProcessor` instance describes the layers and effects they use. An
    example is attached.

    - `name`
    - `effects`
    - `layers`
    - `mipmaps`
    - `screen`

    ### Data `effects`

    The effect pipelines with shader source.

    - `name`
    - `fragment`: fragment program source
    - `samplers`:
    - `name`: uniform name as it appears in the fragment program
    - `source`: buffer (`scene`, `stage`) or texture name
    - `vertex`: vertex program source

    ### Data `layers`

    A list of effects to be applied in order, with optional uniforms for each.

    - `name`: the effect `name`
    - `uniforms`:
    - `name`: the uniform name within the shader program
    - `value`: a float1 value

    ### Data `mipmaps`

    Update mipmaps for the `scene` and `stage` buffers each frame.

    *Note:* This requires WebGL2 for NP2 framebuffers.

    ### Data `screen`

    The screen size.

    - `x`
    - `y`

    ## Effects

    ### Basics
    @@ -117,13 +167,17 @@ TODO

    ### Blur

    #### Basic X
    #### Basic

    Separable blur of 7 pixels using the kernel `[0.125, 0.25, 0.5, 1.0, 0.5, 0.25, 0.125]`.

    Blurs 7 pixels in the X direction using the kernel `[0.125, 0.25, 0.5, 1.0, 0.5, 0.25, 0.125]`.
    #### Distort

    #### Basic Y
    Offset pixels from the previous `stage` using a normal map from another texture.

    Blurs 7 pixels in the Y direction using the kernel `[0.125, 0.25, 0.5, 1.0, 0.5, 0.25, 0.125]`.
    #### Gaussian

    TODO

    ### Tone Mapping

  9. ssube revised this gist Jun 24, 2020. 2 changed files with 4 additions and 4 deletions.
    5 changes: 2 additions & 3 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    # Phaser PostFX

    A flexible layered post-processing system for PhaserJS 3. Apply fullscreen shaders to select scene elements.
    A flexible, layered post-processing system for PhaserJS 3. Apply fullscreen shaders to select scene elements.

    ## Contents

    @@ -51,8 +51,7 @@ A flexible layered post-processing system for PhaserJS 3. Apply fullscreen shade
    ### TODO

    - more effects
    - bind static textures to samplers (other than buffers)
    - support binding sampler to TEXTURE0 other than stage (forced by draw call)
    - support binding sampler to TEXTURE0 other than stage (forced by draw call)
    - set uniforms other than float1

    ## API
    3 changes: 2 additions & 1 deletion data.yml
    Original file line number Diff line number Diff line change
    @@ -302,7 +302,8 @@ pipelines:

    vec2 offset = normal * pixel * 10.0;
    vec4 texel = texture2D(stage, uv + offset);
    vec4 original = texture2D(stage, uv);

    gl_FragColor.xyz = texel.xyz;
    gl_FragColor.xyz = mix(texel.xyz, original.xyz, step(0.5, uv.x));
    gl_FragColor.w = 1.0;
    }
  10. ssube revised this gist Jun 24, 2020. 3 changed files with 80 additions and 28 deletions.
    29 changes: 15 additions & 14 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -23,12 +23,12 @@ A flexible layered post-processing system for PhaserJS 3. Apply fullscreen shade
    - [Monochrome](#monochrome)
    - [Sepia](#sepia)
    - [Vignette](#vignette)
    - [Blend](#blend)
    - [Madd (Mult, Sum)](#madd-mult-sum)
    - [Diff](#diff)
    - [Blur](#blur)
    - [Basic X](#basic-x)
    - [Basic Y](#basic-y)
    - [Combinators](#combinators)
    - [Madd (Mult, Sum)](#madd-mult-sum)
    - [Diff](#diff)
    - [Tone Mapping](#tone-mapping)
    - [Clamp](#clamp)
    - [Curve](#curve)
    @@ -63,7 +63,8 @@ Tint-based pipeline for shader effects with texture caching, sampler binding, an

    #### `EffectPipeline.cacheTextures`

    Look up required textures from `buffers` and cache them with the uniform name to which they will be bound.
    Look up required textures from `buffers` and the scene's texture manager, and cache them with the uniform name to
    which they will be bound.

    ### `PostProcessor`

    @@ -105,25 +106,25 @@ Partial desaturation with overall brown tint, like old-fashioned photographs.

    Gradiated desaturation with circular black edge.

    ### Blur
    ### Blend

    #### Basic X
    #### Madd (Mult, Sum)

    Blurs 7 pixels in the X direction using the kernel `[0.125, 0.25, 0.5, 1.0, 0.5, 0.25, 0.125]`.
    Add the previos `stage` to the original `scene`.

    #### Basic Y
    #### Diff

    Blurs 7 pixels in the Y direction using the kernel `[0.125, 0.25, 0.5, 1.0, 0.5, 0.25, 0.125]`.
    TODO

    ### Combinators
    ### Blur

    #### Madd (Mult, Sum)
    #### Basic X

    Add the previos `stage` to the original `scene`.
    Blurs 7 pixels in the X direction using the kernel `[0.125, 0.25, 0.5, 1.0, 0.5, 0.25, 0.125]`.

    #### Diff
    #### Basic Y

    TODO
    Blurs 7 pixels in the Y direction using the kernel `[0.125, 0.25, 0.5, 1.0, 0.5, 0.25, 0.125]`.

    ### Tone Mapping

    36 changes: 33 additions & 3 deletions data.yml
    Original file line number Diff line number Diff line change
    @@ -4,14 +4,15 @@ pipelines:
    - name: clamp
    uniforms:
    - name: uMin
    value: 0.8
    value: 0.5
    - name: uMax
    value: 1.0
    value: 0.9
    - name: blurX
    - name: blurY
    - name: blurX
    - name: blurY
    - name: madd
    - name: distort
    - name: exposure
    uniforms:
    - name: uTarget
    @@ -272,7 +273,36 @@ pipelines:
    float dist = pow(uv.x - 0.5, 2.0) + pow(uv.y - 0.5, 2.0);
    dist = 1.0 - clamp(dist, 0.0, 1.0);

    gl_FragColor.xyz = mix(texel.xyz, vec3(lum), 1.0 - pow(dist, 12.0)); /* desat */
    gl_FragColor.xyz = mix(texel.xyz, vec3(lum), 1.0 - pow(dist, 9.0)); /* desat */
    gl_FragColor.xyz = gl_FragColor.xyz * vec3(pow(dist, 3.0)); /* darken */
    gl_FragColor.w = 1.0;
    }
    - name: distort
    samplers:
    - name: stage
    source: stage
    - name: normal
    source: glass-normal
    fragment:
    precision mediump float;

    uniform sampler2D stage;
    uniform sampler2D normal;

    varying vec2 outTexCoord;
    varying vec4 outTint;

    void main()
    {
    vec2 pixel = vec2(1.0 / 1024.0, 1.0 / 768.0);
    vec2 uv = outTexCoord.xy;

    vec2 normal = texture2D(normal, uv).xy;
    normal = normalize(normal * 2.0 - 1.0);

    vec2 offset = normal * pixel * 10.0;
    vec4 texel = texture2D(stage, uv + offset);

    gl_FragColor.xyz = texel.xyz;
    gl_FragColor.w = 1.0;
    }
    43 changes: 32 additions & 11 deletions postprocessor.ts
    Original file line number Diff line number Diff line change
    @@ -46,7 +46,12 @@ export type RenderObject = Phaser.GameObjects.GameObject & Phaser.GameObjects.Co
    class EffectPipeline extends Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline {
    protected samplers: Array<{
    name: string;
    type: 'buffer';
    texture: Phaser.GameObjects.RenderTexture;
    } | {
    name: string;
    type: 'texture';
    texture: Phaser.Textures.Texture;
    }>;

    constructor(game: Phaser.Game, effect: PipelineEffect) {
    @@ -68,22 +73,38 @@ class EffectPipeline extends Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline

    const sl = Math.min(MAX_SAMPLERS, this.samplers.length);
    for (let i = 0; i < sl; ++i) {
    const { name, texture } = this.samplers[i];

    renderer.setTexture2D(texture.glTexture, i);
    renderer.setInt1(program, name, i);
    const sampler = this.samplers[i];
    if (sampler.type === 'buffer') {
    renderer.setTexture2D(sampler.texture.glTexture, i);
    renderer.setInt1(program, sampler.name, i);
    } else {
    /* eslint-disable-next-line */
    const frame = (sampler.texture.frames as any)[sampler.texture.firstFrame] as Phaser.Textures.Frame;
    renderer.setTexture2D(frame.glTexture, i);
    renderer.setInt1(program, sampler.name, i);
    }
    }

    return this;
    }

    public cacheTextures(samplers: Array<EffectSampler>, buffers: Map<string, Phaser.GameObjects.RenderTexture>) {
    public cacheTextures(samplers: Array<EffectSampler>, buffers: Map<string, Phaser.GameObjects.RenderTexture>, textures: Phaser.Textures.TextureManager) {
    for (const { name, source } of samplers) {
    const texture = mustGet(buffers, source);
    this.samplers.push({
    name,
    texture,
    });
    if (buffers.has(source)) {
    const texture = mustGet(buffers, source);
    this.samplers.push({
    name,
    texture,
    type: 'buffer',
    });
    } else {
    const texture = textures.get(source);
    this.samplers.push({
    name,
    texture,
    type: 'texture',
    });
    }
    }
    }
    }
    @@ -138,7 +159,7 @@ export class PostProcessor {

    for (const data of this.data.effects) {
    const effect = mustGet(this.effects, data.name);
    effect.cacheTextures(data.samplers, this.buffers);
    effect.cacheTextures(data.samplers, this.buffers, this.scene.textures);
    }

    this.running = true;
  11. ssube revised this gist Jun 24, 2020. 1 changed file with 10 additions and 0 deletions.
    10 changes: 10 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -7,6 +7,7 @@ A flexible layered post-processing system for PhaserJS 3. Apply fullscreen shade
    - [Phaser PostFX](#phaser-postfx)
    - [Contents](#contents)
    - [Features](#features)
    - [Caveats](#caveats)
    - [TODO](#todo)
    - [API](#api)
    - [`EffectPipeline`](#effectpipeline)
    @@ -38,6 +39,15 @@ A flexible layered post-processing system for PhaserJS 3. Apply fullscreen shade

    ## Features

    ### Caveats

    - creates N+2 screen-sized textures for N effects
    - this can potentially be reduced to 2 or 3 (no N) by calling `buffer.setPipeline()` before `stage.draw(buffer)`
    - generating mipmaps for NP2 textures require a WebGL2 context
    - the `textureLod` GLSL function requires a WebGL2 context and `#version 300 es` pragmas
    - the versions must match between fragment and vertex programs in the same layer, so this requires a `300 es` vertex shader
    - mixing versions on different layers is allowed

    ### TODO

    - more effects
  12. ssube revised this gist Jun 24, 2020. 1 changed file with 0 additions and 4 deletions.
    4 changes: 0 additions & 4 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -89,14 +89,10 @@ Luminance-based grayscale.

    #### Sepia

    TODO

    Partial desaturation with overall brown tint, like old-fashioned photographs.

    #### Vignette

    TODO

    Gradiated desaturation with circular black edge.

    ### Blur
  13. ssube revised this gist Jun 24, 2020. 1 changed file with 6 additions and 3 deletions.
    9 changes: 6 additions & 3 deletions data.yml
    Original file line number Diff line number Diff line change
    @@ -16,7 +16,7 @@ pipelines:
    uniforms:
    - name: uTarget
    value: 0.5
    - name: sepia
    # name: sepia
    - name: vignette
    mipmaps: true
    screen:
    @@ -267,9 +267,12 @@ pipelines:
    {
    vec2 uv = outTexCoord.xy;
    vec4 texel = texture2D(stage, uv);

    float lum = (texel.x * 0.3) + (texel.y * 0.6) + (texel.z * 0.1);
    float dist = pow(uv.x - 0.5, 2.0) + pow(uv.y - 0.5, 2.0);
    float cd = pow(1.0 - clamp(dist, 0.0, 1.0), 6.0);
    dist = 1.0 - clamp(dist, 0.0, 1.0);

    gl_FragColor.xyz = texel.xyz * vec3(cd);
    gl_FragColor.xyz = mix(texel.xyz, vec3(lum), 1.0 - pow(dist, 12.0)); /* desat */
    gl_FragColor.xyz = gl_FragColor.xyz * vec3(pow(dist, 3.0)); /* darken */
    gl_FragColor.w = 1.0;
    }
  14. ssube revised this gist Jun 24, 2020. 2 changed files with 68 additions and 11 deletions.
    23 changes: 18 additions & 5 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -12,6 +12,7 @@ A flexible layered post-processing system for PhaserJS 3. Apply fullscreen shade
    - [`EffectPipeline`](#effectpipeline)
    - [`EffectPipeline.cacheTextures`](#effectpipelinecachetextures)
    - [`PostProcessor`](#postprocessor)
    - [`new PostProcessor`](#new-postprocessor)
    - [`PostProcessor.create`](#postprocessorcreate)
    - [`PostProcessor.update`](#postprocessorupdate)
    - [`PostProcessor.stop`](#postprocessorstop)
    @@ -41,19 +42,27 @@ A flexible layered post-processing system for PhaserJS 3. Apply fullscreen shade

    - more effects
    - bind static textures to samplers (other than buffers)
    - support binding sampler to `TEXTURE0` other than `stage` (forced by draw call)
    - set uniforms other than `float1`
    - support binding sampler to TEXTURE0 other than stage (forced by draw call)
    - set uniforms other than float1

    ## API

    ### `EffectPipeline`

    Tint-based pipeline for shader effects with texture caching, sampler binding, and other niceties.

    #### `EffectPipeline.cacheTextures`

    Look up required textures and cache them with the uniform name to which they will be bound.
    Look up required textures from `buffers` and cache them with the uniform name to which they will be bound.

    ### `PostProcessor`

    Manages the buffers and pipelines for post-processing a particular scene. Methods match the scene lifecycle.

    #### `new PostProcessor`

    Create a new post-processing pipeline from `data` and bound to the `scene`.

    #### `PostProcessor.create`

    Set up the post-processing pipelines and render textures, compiling shaders and caching textures.
    @@ -72,20 +81,24 @@ Shut down the post-processing pipelines and clean up cached textures and WebGL r

    #### Invert

    Invert colors: replace black with white and vice versa, except for alpha channel.
    Invert colors per-channel.

    #### Monochrome

    Luminance-based grayscale with tints.
    Luminance-based grayscale.

    #### Sepia

    TODO

    Partial desaturation with overall brown tint, like old-fashioned photographs.

    #### Vignette

    TODO

    Gradiated desaturation with circular black edge.

    ### Blur

    #### Basic X
    56 changes: 50 additions & 6 deletions data.yml
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,4 @@

    post:
    pipelines:
    - name: basic
    layers:
    - name: clamp
    @@ -17,8 +16,8 @@ post:
    uniforms:
    - name: uTarget
    value: 0.5
    - name: monochrome
    - name: invert
    - name: sepia
    - name: vignette
    mipmaps: true
    screen:
    x: 1024
    @@ -43,7 +42,7 @@ post:
    vec4 texel = texture2D(foo, uv);
    vec4 invert = vec4(1.0) - texel;

    gl_FragColor = mix(invert, texel, step(uv.y, 0.5));
    gl_FragColor = mix(invert, texel, step(uv.y, 0.6));
    gl_FragColor.w = 1.0;
    }
    - name: monochrome
    @@ -66,7 +65,7 @@ post:
    float lum = (texel.x * 0.3) + (texel.y * 0.6) + (texel.z * 0.1);
    vec4 lumtex = vec4(lum, lum, lum, 1.0);

    gl_FragColor = mix(texel, lumtex, step(0.5, uv.y));
    gl_FragColor = mix(texel, lumtex, step(0.3, uv.y));
    gl_FragColor.w = 1.0;
    }
    - name: blurX
    @@ -229,3 +228,48 @@ post:
    gl_FragColor = texel;
    gl_FragColor.w = 1.0;
    }
    - name: sepia
    samplers:
    - name: stage
    source: stage
    fragment:
    precision mediump float;

    uniform sampler2D foo;

    varying vec2 outTexCoord;
    varying vec4 outTint;

    void main()
    {
    vec2 uv = outTexCoord.xy;
    vec4 texel = texture2D(foo, uv);

    /* kernel from https://gist.github.com/rasteron/2019a4890e0d6311297f */
    gl_FragColor.x = dot(texel.xyz, vec3(0.393, 0.769, 0.189));
    gl_FragColor.y = dot(texel.xyz, vec3(0.349, 0.686, 0.168));
    gl_FragColor.z = dot(texel.xyz, vec3(0.272, 0.534, 0.131));
    gl_FragColor.w = 1.0;
    }
    - name: vignette
    samplers:
    - name: stage
    source: stage
    fragment:
    precision mediump float;

    uniform sampler2D stage;

    varying vec2 outTexCoord;
    varying vec4 outTint;

    void main()
    {
    vec2 uv = outTexCoord.xy;
    vec4 texel = texture2D(stage, uv);
    float dist = pow(uv.x - 0.5, 2.0) + pow(uv.y - 0.5, 2.0);
    float cd = pow(1.0 - clamp(dist, 0.0, 1.0), 6.0);

    gl_FragColor.xyz = texel.xyz * vec3(cd);
    gl_FragColor.w = 1.0;
    }
  15. ssube revised this gist Jun 24, 2020. 1 changed file with 3 additions and 2 deletions.
    5 changes: 3 additions & 2 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -41,7 +41,8 @@ A flexible layered post-processing system for PhaserJS 3. Apply fullscreen shade

    - more effects
    - bind static textures to samplers (other than buffers)
    - set uniforms other than `float`
    - support binding sampler to `TEXTURE0` other than `stage` (forced by draw call)
    - set uniforms other than `float1`

    ## API

    @@ -145,7 +146,7 @@ much more complex effects.

    ### Bloom

    - Clamp (high threshold pass)
    - Clamp (high pass)
    - high: 1.0
    - low: 0.8
    - Blur (repeat as needed)
  16. ssube revised this gist Jun 24, 2020. 1 changed file with 15 additions and 1 deletion.
    16 changes: 15 additions & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -6,6 +6,8 @@ A flexible layered post-processing system for PhaserJS 3. Apply fullscreen shade

    - [Phaser PostFX](#phaser-postfx)
    - [Contents](#contents)
    - [Features](#features)
    - [TODO](#todo)
    - [API](#api)
    - [`EffectPipeline`](#effectpipeline)
    - [`EffectPipeline.cacheTextures`](#effectpipelinecachetextures)
    @@ -33,6 +35,14 @@ A flexible layered post-processing system for PhaserJS 3. Apply fullscreen shade
    - [Compound Effects](#compound-effects)
    - [Bloom](#bloom)

    ## Features

    ### TODO

    - more effects
    - bind static textures to samplers (other than buffers)
    - set uniforms other than `float`

    ## API

    ### `EffectPipeline`
    @@ -89,7 +99,7 @@ Blurs 7 pixels in the Y direction using the kernel `[0.125, 0.25, 0.5, 1.0, 0.5,

    #### Madd (Mult, Sum)

    TODO
    Add the previos `stage` to the original `scene`.

    #### Diff

    @@ -112,10 +122,14 @@ This effect can act as a low-pass/high-cut or high-pass/low-cut filter by settin

    #### Curve

    TODO

    Adjust scene colors using a 2-point curve.

    #### Color

    TODO

    Adjust scene colors using a 2D tonemap texture, based on the biome tint/color palette effect from Crysis 1.

    #### Exposure
  17. ssube revised this gist Jun 24, 2020. 1 changed file with 2 additions and 8 deletions.
    10 changes: 2 additions & 8 deletions postprocessor.ts
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,6 @@ import * as Phaser from 'phaser';

    import { CENTER_SPLIT } from '../constants';
    import { Point } from '../entity';
    import { ControlledScene } from '../scene';

    export interface EffectSampler {
    name: string;
    @@ -12,8 +11,7 @@ export interface EffectSampler {

    export interface EffectUniform {
    name: string;
    /* eslint-disable-next-line */
    value: any;
    value: number;
    }

    export interface PipelineEffect {
    @@ -39,7 +37,6 @@ export interface PipelineData {
    const REQUIRED_BUFFERS = [
    'scene',
    'stage',
    'write',
    ];

    const MAX_SAMPLERS = 8;
    @@ -171,7 +168,6 @@ export class PostProcessor {

    const sceneBuffer = mustGet(this.buffers, 'scene');
    const stageBuffer = mustGet(this.buffers, 'stage');
    const writeBuffer = mustGet(this.buffers, 'write');

    const fillRect = mustExist(this.fillRect);
    sceneBuffer.draw(fillRect, fillRect.displayWidth / CENTER_SPLIT, fillRect.displayHeight / CENTER_SPLIT);
    @@ -196,9 +192,7 @@ export class PostProcessor {
    }

    buffer.draw(stageBuffer);

    writeBuffer.draw(buffer);
    stageBuffer.draw(writeBuffer);
    stageBuffer.draw(buffer);

    if (this.data.mipmaps) {
    this.updateMips(stageBuffer, gl);
  18. ssube revised this gist Jun 24, 2020. 1 changed file with 12 additions and 12 deletions.
    24 changes: 12 additions & 12 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -7,12 +7,12 @@ A flexible layered post-processing system for PhaserJS 3. Apply fullscreen shade
    - [Phaser PostFX](#phaser-postfx)
    - [Contents](#contents)
    - [API](#api)
    - [`Pipeline`](#pipeline)
    - [`Pipeline.cacheTextures`](#pipelinecachetextures)
    - [`PostPipeline`](#postpipeline)
    - [`PostPipeline.create`](#postpipelinecreate)
    - [`PostPipeline.update`](#postpipelineupdate)
    - [`PostPipeline.stop`](#postpipelinestop)
    - [`EffectPipeline`](#effectpipeline)
    - [`EffectPipeline.cacheTextures`](#effectpipelinecachetextures)
    - [`PostProcessor`](#postprocessor)
    - [`PostProcessor.create`](#postprocessorcreate)
    - [`PostProcessor.update`](#postprocessorupdate)
    - [`PostProcessor.stop`](#postprocessorstop)
    - [Effects](#effects)
    - [Basics](#basics)
    - [Invert](#invert)
    @@ -35,23 +35,23 @@ A flexible layered post-processing system for PhaserJS 3. Apply fullscreen shade

    ## API

    ### `Pipeline`
    ### `EffectPipeline`

    #### `Pipeline.cacheTextures`
    #### `EffectPipeline.cacheTextures`

    Look up required textures and cache them with the uniform name to which they will be bound.

    ### `PostPipeline`
    ### `PostProcessor`

    #### `PostPipeline.create`
    #### `PostProcessor.create`

    Set up the post-processing pipelines and render textures, compiling shaders and caching textures.

    #### `PostPipeline.update`
    #### `PostProcessor.update`

    Render a list of `objects` using the post-processing `layers` positioned for the `camera`.

    #### `PostPipeline.stop`
    #### `PostProcessor.stop`

    Shut down the post-processing pipelines and clean up cached textures and WebGL resources.

  19. ssube revised this gist Jun 24, 2020. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions postprocessor.ts
    Original file line number Diff line number Diff line change
    @@ -93,15 +93,15 @@ class EffectPipeline extends Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline

    export class PostProcessor {
    protected readonly data: PipelineData;
    protected readonly scene: ControlledScene;
    protected readonly scene: Phaser.Scene;

    protected readonly buffers: Map<string, Phaser.GameObjects.RenderTexture>;
    protected readonly effects: Map<string, EffectPipeline>;

    protected running: boolean;
    protected fillRect?: Phaser.GameObjects.Rectangle;

    constructor(data: PipelineData, scene: ControlledScene) {
    constructor(data: PipelineData, scene: Phaser.Scene) {
    this.data = data;
    this.scene = scene;
    this.running = false;
  20. ssube revised this gist Jun 24, 2020. 1 changed file with 140 additions and 0 deletions.
    140 changes: 140 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,140 @@
    # Phaser PostFX

    A flexible layered post-processing system for PhaserJS 3. Apply fullscreen shaders to select scene elements.

    ## Contents

    - [Phaser PostFX](#phaser-postfx)
    - [Contents](#contents)
    - [API](#api)
    - [`Pipeline`](#pipeline)
    - [`Pipeline.cacheTextures`](#pipelinecachetextures)
    - [`PostPipeline`](#postpipeline)
    - [`PostPipeline.create`](#postpipelinecreate)
    - [`PostPipeline.update`](#postpipelineupdate)
    - [`PostPipeline.stop`](#postpipelinestop)
    - [Effects](#effects)
    - [Basics](#basics)
    - [Invert](#invert)
    - [Monochrome](#monochrome)
    - [Sepia](#sepia)
    - [Vignette](#vignette)
    - [Blur](#blur)
    - [Basic X](#basic-x)
    - [Basic Y](#basic-y)
    - [Combinators](#combinators)
    - [Madd (Mult, Sum)](#madd-mult-sum)
    - [Diff](#diff)
    - [Tone Mapping](#tone-mapping)
    - [Clamp](#clamp)
    - [Curve](#curve)
    - [Color](#color)
    - [Exposure](#exposure)
    - [Compound Effects](#compound-effects)
    - [Bloom](#bloom)

    ## API

    ### `Pipeline`

    #### `Pipeline.cacheTextures`

    Look up required textures and cache them with the uniform name to which they will be bound.

    ### `PostPipeline`

    #### `PostPipeline.create`

    Set up the post-processing pipelines and render textures, compiling shaders and caching textures.

    #### `PostPipeline.update`

    Render a list of `objects` using the post-processing `layers` positioned for the `camera`.

    #### `PostPipeline.stop`

    Shut down the post-processing pipelines and clean up cached textures and WebGL resources.

    ## Effects

    ### Basics

    #### Invert

    Invert colors: replace black with white and vice versa, except for alpha channel.

    #### Monochrome

    Luminance-based grayscale with tints.

    #### Sepia

    TODO

    #### Vignette

    TODO

    ### Blur

    #### Basic X

    Blurs 7 pixels in the X direction using the kernel `[0.125, 0.25, 0.5, 1.0, 0.5, 0.25, 0.125]`.

    #### Basic Y

    Blurs 7 pixels in the Y direction using the kernel `[0.125, 0.25, 0.5, 1.0, 0.5, 0.25, 0.125]`.

    ### Combinators

    #### Madd (Mult, Sum)

    TODO

    #### Diff

    TODO

    ### Tone Mapping

    #### Clamp

    Clamp scene colors between the high and low points, then expand that to fill the full `[0.0, 1.0)` texture range.

    This effect can act as a low-pass/high-cut or high-pass/low-cut filter by setting the range:

    - high pass:
    - `uMin`: desired cutoff
    - `uMax`: 1.0
    - low pass:
    - `uMin`: 0.0
    - `uMax`: desired cutoff

    #### Curve

    Adjust scene colors using a 2-point curve.

    #### Color

    Adjust scene colors using a 2D tonemap texture, based on the biome tint/color palette effect from Crysis 1.

    #### Exposure

    A _very_ rudimentary and not at all production-suitable fake HDR effect.

    *Note:* Requires WebGL2 and GLSL version 300 for the `textureLod` function and generating mipmaps on NP2 textures.

    ## Compound Effects

    Most effects can be used on their own, but may not be useful. Including a few layers in the right order can produce
    much more complex effects.

    ### Bloom

    - Clamp (high threshold pass)
    - high: 1.0
    - low: 0.8
    - Blur (repeat as needed)
    - X
    - Y
    - Sum
  21. ssube created this gist Jun 24, 2020.
    231 changes: 231 additions & 0 deletions data.yml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,231 @@

    post:
    - name: basic
    layers:
    - name: clamp
    uniforms:
    - name: uMin
    value: 0.8
    - name: uMax
    value: 1.0
    - name: blurX
    - name: blurY
    - name: blurX
    - name: blurY
    - name: madd
    - name: exposure
    uniforms:
    - name: uTarget
    value: 0.5
    - name: monochrome
    - name: invert
    mipmaps: true
    screen:
    x: 1024
    y: 768
    effects:
    - name: invert
    samplers:
    - name: foo
    source: stage
    fragment:
    precision mediump float;

    uniform sampler2D foo;

    varying vec2 outTexCoord;
    varying vec4 outTint;

    void main()
    {
    vec2 uv = outTexCoord.xy;

    vec4 texel = texture2D(foo, uv);
    vec4 invert = vec4(1.0) - texel;

    gl_FragColor = mix(invert, texel, step(uv.y, 0.5));
    gl_FragColor.w = 1.0;
    }
    - name: monochrome
    samplers:
    - name: foo
    source: stage
    fragment:
    precision mediump float;

    uniform sampler2D foo;

    varying vec2 outTexCoord;
    varying vec4 outTint;

    void main()
    {
    vec2 uv = outTexCoord.xy;
    vec4 texel = texture2D(foo, uv);

    float lum = (texel.x * 0.3) + (texel.y * 0.6) + (texel.z * 0.1);
    vec4 lumtex = vec4(lum, lum, lum, 1.0);

    gl_FragColor = mix(texel, lumtex, step(0.5, uv.y));
    gl_FragColor.w = 1.0;
    }
    - name: blurX
    samplers:
    - name: foo
    source: stage
    fragment:
    precision mediump float;

    uniform sampler2D foo;

    varying vec2 outTexCoord;
    varying vec4 outTint;

    void main()
    {
    vec2 pixel = vec2(1.0) / vec2(1024.0, 768.0);
    vec2 uv = outTexCoord.xy;

    vec4 texN3 = texture2D(foo, vec2(uv.x - (pixel.x * 3.0), uv.y)) * 0.125;
    vec4 texN2 = texture2D(foo, vec2(uv.x - (pixel.x * 2.0), uv.y)) * 0.25;
    vec4 texN1 = texture2D(foo, vec2(uv.x - (pixel.x * 1.0), uv.y)) * 0.5;
    vec4 tex00 = texture2D(foo, uv);
    vec4 texP1 = texture2D(foo, vec2(uv.x + (pixel.x * 1.0), uv.y)) * 0.5;
    vec4 texP2 = texture2D(foo, vec2(uv.x + (pixel.x * 2.0), uv.y)) * 0.25;
    vec4 texP3 = texture2D(foo, vec2(uv.x + (pixel.x * 3.0), uv.y)) * 0.125;

    gl_FragColor = (texN3 + texN2 + texN1 + tex00 + texP1 + texP2 + texP3) / 2.75;
    gl_FragColor.w = 1.0;
    }
    - name: blurY
    samplers:
    - name: foo
    source: stage
    fragment:
    precision mediump float;

    uniform sampler2D foo;

    varying vec2 outTexCoord;
    varying vec4 outTint;

    void main()
    {
    vec2 pixel = vec2(1.0) / vec2(1024.0, 768.0);
    vec2 uv = outTexCoord.xy;

    vec4 texN3 = texture2D(foo, vec2(uv.x, uv.y - (pixel.y * 3.0))) * 0.125;
    vec4 texN2 = texture2D(foo, vec2(uv.x, uv.y - (pixel.y * 2.0))) * 0.25;
    vec4 texN1 = texture2D(foo, vec2(uv.x, uv.y - (pixel.y * 1.0))) * 0.5;
    vec4 tex00 = texture2D(foo, uv);
    vec4 texP1 = texture2D(foo, vec2(uv.x, uv.y + (pixel.y * 1.0))) * 0.5;
    vec4 texP2 = texture2D(foo, vec2(uv.x, uv.y + (pixel.y * 2.0))) * 0.25;
    vec4 texP3 = texture2D(foo, vec2(uv.x, uv.y + (pixel.y * 3.0))) * 0.125;

    gl_FragColor = (texN3 + texN2 + texN1 + tex00 + texP1 + texP2 + texP3) / 2.75;
    gl_FragColor.w = 1.0;
    }
    - name: exposure
    samplers:
    - name: foo
    source: stage
    - name: bar
    source: scene
    fragment: |
    #version 300 es
    precision mediump float;
    uniform sampler2D foo;
    uniform sampler2D bar;
    uniform vec2 uResolution;
    uniform float uTime;
    uniform float uLum;
    uniform float uTarget;
    in vec2 outTexCoord;
    in vec4 outTint;
    out vec4 outColor;
    void main()
    {
    vec2 uv = outTexCoord.xy;
    vec2 luv = outTexCoord.xy;
    luv.y = 1.0 - luv.y;
    vec4 global_lum = textureLod(bar, luv, 7.0);
    vec4 local_lum = textureLod(bar, luv, 3.0);
    vec4 texel = texture(foo, uv);
    outColor = mix((local_lum - global_lum) + texel, texel, step(uTarget, uv.x));
    outColor.w = 1.0f;
    }
    vertex: |
    #version 300 es
    precision mediump float;
    uniform mat4 uProjectionMatrix;
    uniform mat4 uViewMatrix;
    uniform mat4 uModelMatrix;
    in vec2 inPosition;
    in vec2 inTexCoord;
    in float inTintEffect;
    in vec4 inTint;
    out vec2 outTexCoord;
    out float outTintEffect;
    out vec4 outTint;
    void main ()
    {
    gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * vec4(inPosition, 1.0, 1.0);
    outTexCoord = inTexCoord;
    outTint = inTint;
    outTintEffect = inTintEffect;
    }
    - name: clamp
    samplers:
    - name: stage
    source: stage
    fragment: |
    precision mediump float;
    uniform float uMin;
    uniform float uMax;
    uniform sampler2D stage;
    varying vec2 outTexCoord;
    void main() {
    float range = uMax - uMin;
    vec4 base = vec4(uMin);
    vec4 texel = texture2D(stage, outTexCoord);
    gl_FragColor = (clamp(texel, base, vec4(uMax)) - base) / range;
    gl_FragColor.w = 1.0;
    }
    - name: madd
    samplers:
    - name: stage
    source: stage
    - name: scene
    source: scene
    fragment: |
    precision mediump float;
    uniform sampler2D scene;
    uniform sampler2D stage;
    varying vec2 outTexCoord;
    void main() {
    vec2 uv = outTexCoord.xy;
    uv.y = 1.0 - uv.y;
    vec4 texel = texture2D(stage, outTexCoord) + texture2D(scene, uv);
    gl_FragColor = texel;
    gl_FragColor.w = 1.0;
    }
    240 changes: 240 additions & 0 deletions postprocessor.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,240 @@
    import { doesExist, mustExist, mustGet } from '@apextoaster/js-utils';
    import * as Phaser from 'phaser';

    import { CENTER_SPLIT } from '../constants';
    import { Point } from '../entity';
    import { ControlledScene } from '../scene';

    export interface EffectSampler {
    name: string;
    source: string;
    }

    export interface EffectUniform {
    name: string;
    /* eslint-disable-next-line */
    value: any;
    }

    export interface PipelineEffect {
    name: string;
    fragment: string;
    samplers: Array<EffectSampler>;
    vertex: string;
    }

    export interface PipelineLayer {
    name: string;
    uniforms: Array<EffectUniform>;
    }

    export interface PipelineData {
    name: string;
    effects: Array<PipelineEffect>;
    layers: Array<PipelineLayer>;
    mipmaps: boolean;
    screen: Point;
    }

    const REQUIRED_BUFFERS = [
    'scene',
    'stage',
    'write',
    ];

    const MAX_SAMPLERS = 8;

    export type RenderObject = Phaser.GameObjects.GameObject & Phaser.GameObjects.Components.Transform;

    class EffectPipeline extends Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline {
    protected samplers: Array<{
    name: string;
    texture: Phaser.GameObjects.RenderTexture;
    }>;

    constructor(game: Phaser.Game, effect: PipelineEffect) {
    super({
    fragShader: effect.fragment,
    game,
    renderer: game.renderer,
    vertShader: effect.vertex,
    });

    this.samplers = [];
    }

    public onBind() {
    super.onBind();

    const program = this.program;
    const renderer = this.renderer;

    const sl = Math.min(MAX_SAMPLERS, this.samplers.length);
    for (let i = 0; i < sl; ++i) {
    const { name, texture } = this.samplers[i];

    renderer.setTexture2D(texture.glTexture, i);
    renderer.setInt1(program, name, i);
    }

    return this;
    }

    public cacheTextures(samplers: Array<EffectSampler>, buffers: Map<string, Phaser.GameObjects.RenderTexture>) {
    for (const { name, source } of samplers) {
    const texture = mustGet(buffers, source);
    this.samplers.push({
    name,
    texture,
    });
    }
    }
    }

    export class PostProcessor {
    protected readonly data: PipelineData;
    protected readonly scene: ControlledScene;

    protected readonly buffers: Map<string, Phaser.GameObjects.RenderTexture>;
    protected readonly effects: Map<string, EffectPipeline>;

    protected running: boolean;
    protected fillRect?: Phaser.GameObjects.Rectangle;

    constructor(data: PipelineData, scene: ControlledScene) {
    this.data = data;
    this.scene = scene;
    this.running = false;

    this.effects = new Map();
    this.buffers = new Map();
    }

    public get screenBuffer() {
    return this.getBuffer('stage');
    }

    public getBuffer(key: string) {
    return mustGet(this.buffers, key);
    }

    public create() {
    this.fillRect = this.scene.add.rectangle(0, 0, this.data.screen.x, this.data.screen.y, 0x00, 1.0);

    for (const buffer of REQUIRED_BUFFERS) {
    const rt = this.createBuffer(buffer);
    this.buffers.set(buffer, rt);
    }

    for (const effect of this.data.effects) {
    const game = this.scene.game;
    const pipeline = new EffectPipeline(game, effect);
    this.effects.set(effect.name, pipeline);

    const renderer = game.renderer as Phaser.Renderer.WebGL.WebGLRenderer;
    renderer.addPipeline(effect.name, pipeline);

    const rt = this.createBuffer(effect.name);
    rt.setPipeline(effect.name);
    this.buffers.set(effect.name, rt);
    }

    for (const data of this.data.effects) {
    const effect = mustGet(this.effects, data.name);
    effect.cacheTextures(data.samplers, this.buffers);
    }

    this.running = true;
    }

    public createBuffer(name: string) {
    const rt = this.scene.make.renderTexture({
    height: this.data.screen.y,
    width: this.data.screen.x,
    x: 0,
    y: 0,
    });

    rt.setName(name);
    rt.setScrollFactor(0, 0);
    rt.setVisible(false);

    this.scene.textures.addRenderTexture(name, rt);

    return rt;
    }

    public update(objects: Array<RenderObject>, layers: Array<PipelineLayer>, camera: Phaser.Cameras.Scene2D.Camera, gl: WebGL2RenderingContext) {
    if (!this.running) {
    return;
    }

    const sceneBuffer = mustGet(this.buffers, 'scene');
    const stageBuffer = mustGet(this.buffers, 'stage');
    const writeBuffer = mustGet(this.buffers, 'write');

    const fillRect = mustExist(this.fillRect);
    sceneBuffer.draw(fillRect, fillRect.displayWidth / CENTER_SPLIT, fillRect.displayHeight / CENTER_SPLIT);
    for (const obj of objects) {
    sceneBuffer.draw(obj, Math.floor(-camera.scrollX + obj.x), Math.floor(-camera.scrollY + obj.y));
    }

    if (this.data.mipmaps) {
    this.updateMips(sceneBuffer, gl);
    }

    stageBuffer.draw(sceneBuffer);

    for (const layer of layers) {
    const buffer = mustGet(this.buffers, layer.name);
    const effect = mustGet(this.effects, layer.name);

    if (doesExist(layer.uniforms)) {
    for (const uniform of layer.uniforms) {
    effect.setFloat1(uniform.name, uniform.value);
    }
    }

    buffer.draw(stageBuffer);

    writeBuffer.draw(buffer);
    stageBuffer.draw(writeBuffer);

    if (this.data.mipmaps) {
    this.updateMips(stageBuffer, gl);
    }
    }
    }

    public updateMips(buffer: Phaser.GameObjects.RenderTexture, gl: WebGL2RenderingContext) {
    const active = gl.getParameter(gl.ACTIVE_TEXTURE);
    const levels = Math.ceil(Math.log2(Math.max(buffer.height, buffer.width)));

    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, buffer.glTexture);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, levels);
    gl.generateMipmap(gl.TEXTURE_2D);

    const err = gl.getError();
    if (err > 0) {
    /* eslint-disable-next-line */
    console.warn('mipmap error', err);
    }

    gl.activeTexture(active);
    }

    public stop() {
    this.running = false;

    const fillRect = mustExist(this.fillRect);
    this.scene.children.remove(fillRect);

    for (const name of this.buffers.keys()) {
    const renderer = this.scene.game.renderer as Phaser.Renderer.WebGL.WebGLRenderer;
    renderer.removePipeline(name);
    this.scene.textures.remove(name);
    }
    }
    }