Apply shader only to visible frame of spritesheet

Here is a fragment of a spritesheet I’m using for the main character of my game:

excerpt

In-game, when the mouse hovers the character, I’m applying an outline shader to it. Here is the shader and the effect:

        precision mediump float;
        uniform sampler2D uMainSampler;
        varying vec2 outTexCoord;
        float glow = 0.004;
        float transparent = 0.9;
        void main(void) {
            vec4 color = texture2D(uMainSampler, outTexCoord);
            vec4 colorU = texture2D(uMainSampler, vec2(outTexCoord.x, outTexCoord.y - glow));
            vec4 colorD = texture2D(uMainSampler, vec2(outTexCoord.x, outTexCoord.y + glow));
            vec4 colorL = texture2D(uMainSampler, vec2(outTexCoord.x + glow, outTexCoord.y));
            vec4 colorR = texture2D(uMainSampler, vec2(outTexCoord.x - glow, outTexCoord.y));
            bool hasNeighbor = (colorU.a > transparent || colorD.a > transparent || colorL.a > transparent || colorR.a > transparent);
            
            gl_FragColor = color;
            if (color.a <= transparent && hasNeighbor) {
                gl_FragColor = vec4(1.0, 1.0, 1.0, .2);
            }
        }

excerpt2

As you can see, the frame above the visible frame is “bleeding” onto the visible one, because of the feet of the frame above. It seems that the shader is applied to the whole spritesheet, and then that only one frame is displayed in the game. The problem is that:

  • It seems less efficient to do it in this order
  • It leads to the kind of bleeding effect I’m having now

My question is: is there a way to apply a shader only to the frame that is currently visible, and not to the whole spritesheet?

Sprites, like other quads, are rendered as two triangles. Each vertex has a coordinate inside the texture (the spritesheet), which is passed to your fragment shader as the outTexCoord. The fragment shader runs for each pixel which gets rendered (the coordinates it receives are just interpolated between the values for the vertices). It’s not being applied to the entire spritesheet - just to the part which gets rendered. The reason other frames bleed into the result is that you apply an offset to the texture coordinates when sampling the texture - in the edges of your sprite, you’ll grab a pixel from another frame (the division between frames exists only in Phaser’s code; the shaders are just told which part of the whole texture to render).

The problem here is to figure out how to ignore the surrounding frames. Splitting them into separate textures is a possible solution, but it would likely make it harder to work with your sprite’s animation (it wouldn’t be a lot less efficient unless all of your characters are in the same spritesheet). A simpler solution could be to just add padding between the frames (see the frameConfig parameter to scene.load.spritesheet) - this wouldn’t eradicate the problem, but at least it won’t be visible.

Thanks for that explanation, which solves the problem.

Btw since we are at it, do you know if Phaser passes a uniform containing the dimensions of the texture? Something like uTextureSize? And/or a uniform indicating the dimensions of the part of the texture that are being rendered perhaps? I can’t reliably find a proper list of what additional data is sent by Phaser to the shader, and with what names.

Neither the texture’s nor the frame’s dimensions are given to the shaders, but you can make add your own uniform if you need them. Generally, a shader’s code should work in normalized coordinates without relying on the texture’s dimensions (as far as I know), but there’s no good reason to follow that rule if you don’t want to.

The fragment shader can receive data from the vertex shader and from the uniforms (set either by the pipeline or by the Shader object). You can find the vertex shader for the Texture Tint Pipeline in src/renderer/webgl/shaders/src/TextureTint.vert; the varying variables it sets are passed to the fragment shader. As for the uniforms, you can look in TextureTint.frag - by default, only the texture is passed. If you’re using a Shader object (which seems unlikely given the fact you have a spritesheet), you can find the input in the same way, provided you know where the shaders’ source code is.

2 Likes