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.
phaser-post

Phaser PostFX

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

Contents

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
  • 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 from buffers and the scene's texture manager, 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.

PostProcessor.update

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

PostProcessor.stop

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

Invert

Invert colors per-channel.

Monochrome

Luminance-based grayscale.

Sepia

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

Vignette

Gradiated desaturation with circular black edge.

Blend

Madd (Mult, Sum)

Add the previos stage to the original scene.

Diff

TODO

Blur

Basic

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

Distort

Offset pixels from the previous stage using a normal map from another texture.

Gaussian

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

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

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 pass)
    • high: 1.0
    • low: 0.8
  • Blur (repeat as needed)
    • 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.

pipelines:
- name: basic
layers:
- name: clamp
uniforms:
- name: uMin
value: 0.5
- name: uMax
value: 0.9
- name: blurX
- name: blurY
- name: blurX
- name: blurY
- name: madd
- name: distort
- name: exposure
uniforms:
- name: uTarget
value: 0.5
# name: sepia
- name: vignette
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.6));
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.3, 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;
}
- 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 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);
dist = 1.0 - clamp(dist, 0.0, 1.0);
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);
vec4 original = texture2D(stage, uv);
gl_FragColor.xyz = mix(texel.xyz, original.xyz, step(0.5, uv.x));
gl_FragColor.w = 1.0;
}
import { doesExist, mustExist, mustGet } from '@apextoaster/js-utils';
import * as Phaser from 'phaser';
import { CENTER_SPLIT } from '../constants';
import { Point } from '../entity';
export interface EffectSampler {
name: string;
source: string;
}
export interface EffectUniform {
name: string;
value: number;
}
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;
}
export type CachedSampler = {
name: string;
type: 'buffer';
texture: Phaser.GameObjects.RenderTexture;
} | {
name: string;
type: 'texture';
texture: Phaser.Textures.Texture;
};
const REQUIRED_BUFFERS = [
'scene',
'stage',
];
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<CachedSampler>;
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 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>, 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({
name,
texture,
type: 'texture',
});
}
}
}
}
export class PostProcessor {
protected readonly data: PipelineData;
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: Phaser.Scene) {
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.scene.textures);
}
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 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);
stageBuffer.draw(buffer);
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);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment