How to get different PostFX Shader settings for different objects?

Title. I am testing a new PostFX Shader:

export class PaletteSwapPipeline extends Phaser.Renderer.WebGL.Pipelines.PostFXPipeline {
    maxColors: integer;

	constructor(game: Phaser.Game) {
        const maxColors: integer = 128;
		let config = {
			game,
			fragShader: `
                #define SHADER_NAME PALETTE_SWAP
                #define MAX_COLORS ${maxColors}

				#ifdef GL_FRAGMENT_PRECISION_HIGH
				#define highmedp highp
				#else
				#define highmedp mediump
				#endif

				precision highmedp float;

				uniform sampler2D uMainSampler;

                uniform float uStrength;
                varying vec2 outTexCoord;
				
				// Effect parameters
                uniform vec3 originalPalette[MAX_COLORS];
                uniform vec3 newPalette[MAX_COLORS];
				
				void main (void) {
                    gl_FragColor = texture2D(uMainSampler, outTexCoord);
                    gl_FragColor.rgb = newPalette[0];
				}`
		};
		super(config);

        this.maxColors = maxColors;
	}

How do I populate newPalette with different values for different GameObjects? I have tried to put add a postPipelineData object to the GameObject and then read it during onBind() (Which is what the documentation seems to be suggesting to do) but then onBind() is never called???

Trying to set the values during preRender() causes the whole screen to turn that color??? ?

I am using

this.set3fv('newPalette', extractedColors);

is there something wrong with that??? ???

Please help if you can.

For your shader, the reason the whole screen turns that color is due to how the PostFXPipelines work. These pipelines will receive the whole screen, and not just the pixels tied to the game object. Due to this, when you run gl_FragColor.rgb = newPalette[0];, this is causing all of the pixels on the screen to turn that color.

To fix this, if you multiply the the multiply the original pixel by the color you want to apply, this should have the effect you are looking for, since any pixel that is not part of the original game object will have 0 for the alpha value.

Example:

    vec4 texColor = texture2D(uMainSampler, outTexCoord);
    gl_FragColor = texColor * vec4(newPalette[0], 1);

Working example: Different shader object settings Phaser Sandbox Entry (v3.87)

Another option would be to switch to a PreFXPipeline, which will only receive the pixels that make up that game object. However, you would probably still want to multiply the original color, since if the image/game object has transparent pixels, you might have a similar issue.

1 Like

How would you switch this to a PreFXPipeline? That might work better, actually.

For the PreFXPipeline, to get unique values per game object, you will need to pass pipeline data for each game object instance to the PreFXPipeline instance. This data can then be referenced in the onBind method to set the newPalette value.

Example of using the PreFXPipeline and passing the custom data for a game object:

this.earth = this.add.image(150, 150, "earth").setPipeline('PaletteSwapPipeline', { color: [1, 0, 0] });

Then, in the Pipeline class, you can add the onBind method like so:

    onBind(gameObject) {
        super.onBind();
        const data = gameObject.pipelineData;
        this.set3f('newPalette', data.color[0], data.color[1], data.color[2]);
    }

Full example: Different shader object settings - PreFxPipeline Phaser Sandbox Entry (v3.87)

const fragShader = `
#define SHADER_NAME PALETTE_SWAP
#define MAX_COLORS 128

#ifdef GL_FRAGMENT_PRECISION_HIGH
#define highmedp highp
#else
#define highmedp mediump
#endif

precision highmedp float;

uniform sampler2D uMainSampler;

uniform float uStrength;
varying vec2 outTexCoord;

// Effect parameters
uniform vec3 originalPalette[MAX_COLORS];
uniform vec3 newPalette[MAX_COLORS];

void main (void) {
    vec4 texColor = texture2D(uMainSampler, outTexCoord);
    gl_FragColor = texColor * vec4(newPalette[0], 1);
}
`;

class PaletteSwapPipeline extends Phaser.Renderer.WebGL.Pipelines.PreFXPipeline {
    maxColors;
    newPalette;

	constructor(game) {
		let config = {
            name: 'PaletteSwapPipeline',
			game,
            fragShader,
		};
		super(config);

        const maxColors = 128;
        this.maxColors = maxColors;
	}

    onBind(gameObject) {
        super.onBind();
        const data = gameObject.pipelineData;
        this.set3f('newPalette', data.color[0], data.color[1], data.color[2]);
    }
}

class MainScene extends Phaser.Scene {

    constructor() {
        super({ key: "MainScene" });
    }

    preload() {
        this.load.image("earth", "https://cdn.phaser.io/sandbox/square-earth.png");
    }

    create() {
        this.earth = this.add.image(150, 150, "earth").setPipeline('PaletteSwapPipeline', { color: [1, 0, 0] });
        this.earth2 = this.add.image(350, 350, "earth").setPipeline('PaletteSwapPipeline', { color: [0, 1, 0] });
        this.earth3 = this.add.image(550, 550, "earth").setPipeline('PaletteSwapPipeline', { color: [0, 0, 1] });
    }
}

const game = new Phaser.Game({
    type: Phaser.WEBGL,
    width: 800,
    height: 800,
    backgroundColor: '#111111',
    scale: {
        mode: Phaser.Scale.FIT,
        autoCenter: Phaser.Scale.CENTER_BOTH
    },
    scene: [ MainScene ],
    pipeline: { 'PaletteSwapPipeline': PaletteSwapPipeline }
})
1 Like

When I tried converting this over, the Fragment Shader started failing with some odd errors:

Fragment Shader failed:
ERROR: 0:15: 'outTexCoord' : undeclared identifier
ERROR: 0:15: 'texture2D' : no matching overloaded function found
ERROR: 0:15: '=' : dimension mismatch
ERROR: 0:15: '=' : cannot convert from 'const mediump float' to 'mediump 4-component vector of float'

Any idea what that’s about?

Would it be possible for you to share your current code that is causing the issue?

Not wanting to compromise the working PostFX, I switched the name. I’m also doing this in Typescript, but I don’t think that matters for this issue.

The main shader file:

import 'phaser';

interface TestData {
    r: number;
    g: number;
    b: number;
    a: number;
}

function isTestData(object: any): object is TestData {
    return typeof object.r === "number" && typeof object.g === "number" && typeof object.b === "number" && 
        typeof object.a === "number";
}

export class TestShader extends Phaser.Renderer.WebGL.Pipelines.PreFXPipeline {
    constructor(game: Phaser.Game)
    {
        super({
            game, 
            renderTarget: true,
            fragShader: `
                #define SHADER_NAME TEST_SHADER

                #ifdef GL_ES
                precision mediump float;
                #endif

                varying vec2 outTextCoord;
                uniform sampler2D uMainSampler;

                // Effect parameters
                uniform vec4 multColor;

                void main() {
                    vec4 texColor = texture2D(uMainSampler, outTexCoord);
                    gl_FragColor = texColor * multColor;
                }`
        });
    }

    onBind(gameObject?: Phaser.GameObjects.GameObject): void {
        super.onBind();
        if (gameObject instanceof Phaser.GameObjects.Sprite)
        {
            const data = gameObject.pipelineData;
            if (isTestData(data))
            {
                this.set4f("multColor", data.r, data.g, data.b, data.a);
            }
        }
    }
}

Scene preload:

    preload() {
        (this.renderer as Phaser.Renderer.WebGL.WebGLRenderer).pipelines.add("TestShader", new TestShader(this.game));
    }

Setting pipeline:

this.sprite = scene.add.sprite(0, 0, "base").setPipeline("TestShader", {r: 1.0, g: 0.4, b: 0.723, a: 1.0});

Thanks for sharing your code. The issue is tied to this line here. This should be outTexCoord instead of outTextCoord. If you update this, the shader should load.