Search code examples
opengl-esglslshaderprecisionfragment-shader

GLSL - different precision in different parts of fragment shader


I have a simple fragment shader that draws test grid pattern.

I don't really have a problem - but I've noticed a weird behavior that's inexplicable to me. Don't mind weird constants - they get filled during shader assembly before compilation. Also, vertexPosition is actual calculated position in world space, so I can move the shader texture when the mesh itself moves.

Here's the code of my shader:

#version 300 es

precision highp float;

in highp vec3 vertexPosition;

out mediump vec4 fragColor;

const float squareSize = __CONSTANT_SQUARE_SIZE;
const vec3 color_base = __CONSTANT_COLOR_BASE;
const vec3 color_l1 = __CONSTANT_COLOR_L1;

float minWidthX;
float minWidthY;

vec3 color_green = vec3(0.0,1.0,0.0);

void main()
{
    // calculate l1 border positions
    float dimention = squareSize;
    int roundX = int(vertexPosition.x / dimention);
    int roundY = int(vertexPosition.z / dimention);
    float remainderX = vertexPosition.x - float(roundX)*dimention;
    float remainderY = vertexPosition.z - float(roundY)*dimention;

    vec3 dyX = dFdy(vec3(vertexPosition.x, vertexPosition.y, 0));
    vec3 dxX = dFdx(vec3(vertexPosition.x, vertexPosition.y, 0));
    minWidthX = max(length(dxX),length(dyX));
    vec3 dyY = dFdy(vec3(0, vertexPosition.y, vertexPosition.z));
    vec3 dxY = dFdx(vec3(0, vertexPosition.y, vertexPosition.z));
    minWidthY = max(length(dxY),length(dyY));


    //Fill l1 suqares
    if (remainderX <= minWidthX)
    {
        fragColor = vec4(color_l1, 1.0);
        return;
    }

    if (remainderY <= minWidthY)
    {
        fragColor = vec4(color_l1, 1.0);
        return;
    }

    // fill base color
    fragColor = vec4(color_base, 1.0);
    return;
}

So, with this code everything works well. I then wanted to optimize it a little bit by moving calculations that only concern horizontal lines after the vertical lines are drawn. Because these calculations are useless if the vertical lines check is true. Like this:

#version 300 es

precision highp float;

in highp vec3 vertexPosition;

out mediump vec4 fragColor;

const float squareSize = __CONSTANT_SQUARE_SIZE;
const vec3 color_base = __CONSTANT_COLOR_BASE;
const vec3 color_l1 = __CONSTANT_COLOR_L1;

float minWidthX;
float minWidthY;

vec3 color_green = vec3(0.0,1.0,0.0);

void main()
{
    // calculate l1 border positions
    float dimention = squareSize;
    int roundX = int(vertexPosition.x / dimention);
    int roundY = int(vertexPosition.z / dimention);
    float remainderX = vertexPosition.x - float(roundX)*dimention;
    float remainderY = vertexPosition.z - float(roundY)*dimention;

    vec3 dyX = dFdy(vec3(vertexPosition.x, vertexPosition.y, 0));
    vec3 dxX = dFdx(vec3(vertexPosition.x, vertexPosition.y, 0));
    minWidthX = max(length(dxX),length(dyX));

    //Fill l1 suqares
    if (remainderX <= minWidthX)
    {
        fragColor = vec4(color_l1, 1.0);
        return;
    }

    vec3 dyY = dFdy(vec3(0, vertexPosition.y, vertexPosition.z));
    vec3 dxY = dFdx(vec3(0, vertexPosition.y, vertexPosition.z));
    minWidthY = max(length(dxY),length(dyY));

    if (remainderY <= minWidthY)
    {
        fragColor = vec4(color_l1, 1.0);
        return;
    }

    // fill base color
    fragColor = vec4(color_base, 1.0);
    return;
}

But even while seemingly this should not affect the result - it does. By quite a bit. Below are the two screenshots. The first one is the original code, the second - is the "optimized" one. Which works bad.

Original version:

enter image description here

Optimized version (looks much worse):

enter image description here

Notice how the lines became "fuzzy" even though seemingly no numbers should have changed at all.

Note: this isn't because minwidthX/Y are global. I initially optimized by making them local. I also initially moved RoundY and remainderY calculation below the X check as well, and the result is the same.

Note 2: I tried adding highp keyword for each of those calculations specifically, but that doesn't change anything (not that I expected it to, but I tried nevertheless)

Could anyone please explain to me why this happens? I would like to know for my future shaders, and actually I would like to optimize this one as well. I would like to understand the principle behind precision loss here, because it doesn't make any sense to me.


Solution

  • For the answer I'll refer to OpenGL ES Shading Language 3.20 Specification, which is the same as OpenGL ES Shading Language 3.00 Specification in this point.

    8.14.1. Derivative Functions

    [...] Derivatives are undefined within non-uniform control flow.

    and further

    3.9.2. Uniform and Non-Uniform Control Flow

    When executing statements in a fragment shader, control flow starts as uniform control flow; all fragments enter the same control path into main(). Control flow becomes non-uniform when different fragments take different paths through control-flow statements (selection, iteration, and jumps).[...]

    That means, that the result of the derivative functions in the first case (of your question) is well defined.

    But in the second case it is not:

    if (remainderX <= minWidthX)
    {
       fragColor = vec4(color_l1, 1.0);
       return;
    }
    
    vec3 dyY = dFdy(vec3(0, vertexPosition.y, vertexPosition.z));
    vec3 dxY = dFdx(vec3(0, vertexPosition.y, vertexPosition.z));
    

    because the return statement acts like a selection. And all the code after the code block with the return statement is in non-uniform control flow.