Search code examples
shadergame-developmentphaser-frameworkphaserjs

How to prevent Phaser 3 shader from moving with camera?


I'm having trouble with a shader in my Phaser game. I've applied the shader as a post pipeline, hoping it would cover the whole screen with a water effect. But when I move my character around, the effect moves too. It's like the shader is stuck to the camera instead of staying put on the screen.

I've put together a quick demo so you can see what I mean. You can use the arrow keys to move around and check out the issue: https://phaser.io/sandbox/vFDT9xXy

Here's the relevant shader code:

const waterFragShader = `
#ifdef GL_ES
precision mediump float;
#endif

uniform float uTime;
uniform vec2 uResolution;
uniform sampler2D uMainSampler;

varying vec2 outTexCoord;

void main(void)
{
    vec2 uv = gl_FragCoord.xy / uResolution.xy;

    vec2 distortion = vec2(
      sin(uv.y * 10.0 + uTime) * 0.002,
      cos(uv.x * 10.0 + uTime) * 0.002
    );

    vec4 texture_color = texture2D(uMainSampler, uv + distortion);

    vec4 k = vec4(uTime)*0.6;
    k.xy = uv * 7.0;
    float val1 = length(0.5-fract(k.xyw*=mat3(vec3(-2.0,-1.0,0.0), vec3(3.0,-1.0,1.0), vec3(1.0,-1.0,-1.0))*0.5));
    float val2 = length(0.5-fract(k.xyw*=mat3(vec3(-2.0,-1.0,0.0), vec3(3.0,-1.0,1.0), vec3(1.0,-1.0,-1.0))*0.2));
    float val3 = length(0.5-fract(k.xyw*=mat3(vec3(-2.0,-1.0,0.0), vec3(3.0,-1.0,1.0), vec3(1.0,-1.0,-1.0))*0.5));

    float pattern = pow(min(min(val1,val2),val3), 7.0) * 2.0;
    vec4 pattern_color = vec4(1, 1, 1, pattern);
    vec4 color = vec4(pattern, pattern, pattern, pattern);

    gl_FragColor = mix(texture_color, pattern_color, pattern_color.a);
}
`;

class WaterPipeline extends Phaser.Renderer.WebGL.Pipelines
    .PostFXPipeline {
    constructor(game) {
        super({
            game,
            name: "Water",
            fragShader: waterFragShader,
        });
    }

    onPreRender() {
        this.set1f("uTime", this.game.loop.time / 1000);
    }

    onDraw(renderTarget) {
        this.set2f("uResolution", renderTarget.width, renderTarget.height);
        this.bindAndDraw(renderTarget);
    }
}

Any ideas on how I can get this shader to stay put and cover the whole screen, no matter where the camera moves?


Solution

  • Well I'm no shader expert, so take my answer with a grain of salt. I would initially say no it is not really possible, because the distortion is generated from the current image.

    But tha I remembered you can can pass variables into the shader, so if you pass the position of the player as an offset, the distortion would seem to be moving.

    Short Demo:

    (ShowCasing the player offset; use arrow keys to move player = red square)

    const waterFragShader = `
    #ifdef GL_ES
    precision mediump float;
    #endif
    
    uniform float uTime;
    uniform vec2 uResolution;
    uniform sampler2D uMainSampler;
    
    varying vec2 outTexCoord;
    
    // ADDED the OffSet Variable
    uniform vec2 offSet; 
    
    void main(void)
    {
        vec2 uv = (gl_FragCoord.xy / uResolution.xy) + offSet ;
    
        vec2 distortion = vec2(
          sin(uv.y * 10.0 + uTime) * 0.002,
          cos(uv.x * 10.0 + uTime) * 0.002
        );
    
        vec4 texture_color = texture2D(uMainSampler, uv + distortion);
    
        vec4 k = vec4(uTime)*0.6;
        k.xy = uv * 7.0;
        float val1 = length(0.5-fract(k.xyw*=mat3(vec3(-2.0,-1.0,0.0), vec3(3.0,-1.0,1.0), vec3(1.0,-1.0,-1.0))*0.5));
        float val2 = length(0.5-fract(k.xyw*=mat3(vec3(-2.0,-1.0,0.0), vec3(3.0,-1.0,1.0), vec3(1.0,-1.0,-1.0))*0.2));
        float val3 = length(0.5-fract(k.xyw*=mat3(vec3(-2.0,-1.0,0.0), vec3(3.0,-1.0,1.0), vec3(1.0,-1.0,-1.0))*0.5));
    
        float pattern = pow(min(min(val1,val2),val3), 7.0) * 2.0;
        vec4 pattern_color = vec4(1, 1, 1, pattern + .2); // added .2 to make it more visible
        vec4 color = vec4(pattern, pattern, pattern, pattern);
    
        gl_FragColor = mix(texture_color, pattern_color, pattern_color.a);
    }
    `;
    
    class WaterPipeline extends Phaser.Renderer.WebGL.Pipelines
        .PostFXPipeline {
        constructor(game) {
            super({
                game,
                name: "Water",
                fragShader: waterFragShader,
            });
            // Initialize the OffSet Variable
            this.offset = { x: 10, y: 10 };
        }
    
        setOffset(x,y){
            // Update OffSet Variable
            this.offset = { x, y};
        }
    
        onPreRender() {
            this.set1f("uTime", this.game.loop.time / 1000);
            // pass OffSet Variable to shader
            this.set2f("offSet", this.offset.x / 1000, this.offset.y/ 1000);
        }
    
        onDraw(renderTarget) {
            this.set2f("uResolution", renderTarget.width, renderTarget.height);
            this.bindAndDraw(renderTarget);
        }
    }
    
    
    class MainScene extends Phaser.Scene {
    
        constructor() {
            super({ key: "MainScene" });
        }
    
        create() {
            this.renderer.pipelines.addPostPipeline("Water", WaterPipeline);
    
            this.bg = this.add.rectangle(100, 100, 100, 100, 0x0000ff)
              .setPostPipeline("Water");
    
            this.player = this.add
                .rectangle(200, 100, 10, 10, 0xff0000)
                .setOrigin(0, 0);
    
            this.cameras.main.startFollow(this.player)
        }
    
        update() {
            const cursors = this.input.keyboard.createCursorKeys();
            if (cursors.left.isDown) {
                this.player.x -= 10;
            } else if (cursors.right.isDown) {
                this.player.x += 10;
            } else if (cursors.up.isDown) {
                this.player.y -= 10;
            } else if (cursors.down.isDown) {
                this.player.y += 10;
            }
    
            // Pass the players position
            this.bg.getPostPipeline("Water").setOffset(this.player.x, this.player.y)
        }
    
    }
    
    var config = {
        width: 540,
        height: 180,
        scene: [MainScene]
    }; 
    
    new Phaser.Game(config);
    
    console.clear();
    document.body.style = 'margin:0;';
    <script src="//cdn.jsdelivr.net/npm/phaser/dist/phaser.min.js"></script>

    There might be a better way, and/or de shader code might/will need some more tweaking, but this is the main idea.