1
$\begingroup$

I'm trying to create an infinitely panable grid using fragment shaders (C++/OpenGL/GLSL), and I'm having a bit of difficulty understanding the coordinate system.

This is my fragment shader code, ported over from an example on ShaderToy:

#version 460 core

layout(location = 0) out vec4 color;

in vec2 v_Resolution;   // window size in pixels             [1000, 618]
in vec3 v_CamPos;       // camera position in pixels         [0, 0, 0]
in float v_CellSize;    // grid cell size in pixels          [50]

// TODO: make these variables in
float lineWidth      = 1.f;         // The width of each line (in pixels).
float darkenMultiple = 5.f;         // Multiple of which lines are darkened (e.g. every 5th line).

void main() {

    float width = lineWidth / v_CellSize;

    vec2 uv = vec2(gl_FragCoord.x, gl_FragCoord.y / v_Resolution.y + v_Resolution.y / 2 - v_CellSize / 2);

    uv.x *= 1.0 / v_CellSize;
    uv.y /= v_CellSize;

    // Adjust the origin of each line thicker than one pixel to its center.
    uv.xy += (lineWidth > 1) ? width * 0.5 : width;
    
    // Make the grid resolution independent by offsetting the Y coordinates based on the current viewport height.
    uv.y -= gl_FragCoord.y / v_CellSize;

    // Apply camera translation.
    uv.x += v_CamPos.x / v_CellSize;
    uv.y -= v_CamPos.y / v_CellSize;
    
    float grid = 0.0;
    grid = max(step(fract(uv.x), width), grid); // X lines (horizontal)
    grid = max(step(fract(uv.y), width), grid); // Y lines (vertical)
    
    vec4 col = vec4(grid);
    
    if (col.r != 0.0 && col.g != 0.0 && col.b != 0.0) {
    
        float mx = mod(uv.x, float(darkenMultiple));
        float my = mod(uv.y, float(darkenMultiple));
        
        // Make lines that are a multiple of `darkenMultiple` darker than the rest.
        col.a = (mx < width || my < width) ? 0.2 : 0.15;
    } else {
        // Turn all pixels that aren't apart of the grid invisible as to not cover the rest of the scene.
        col.a = 0.0;
    }
    
    color = col;
}

Everything in this code functions as intended, until I try to translate the entire grid (grid lines) to the center of the screen by adding v_Resolution.x / 2 to uv.x.

I believe the part that's tripping me up is when I normalize gl_FragCoord to uv by dividing it by the screen size (v_Resolution).

From this question I know why gl_FragCoord is normalized, but I don't understand why in this case uv.x = gl_FragCoord.x while uv.y = gl_FragCoord.y / v_Resolution.y. Why would I divide the y-component by the screen height, but not the x-component by the screen width? If I try to divide gl_FragCoord.x by the screen width, the entire shader breaks, and the grid columns are not rendered. This is especially strange to me, because in the ShaderToy example, the coordinates are not normalized at all, and it functions perfectly fine! I feel like I'm missing a very important concept here.

The entire code can be downloaded and compiled (Windows, OpenGL 4.5) from my GitHub repo: https://github.com/wooandrew/boomerang/tree/develop

EDIT:
Okay, I was able to get the translation working as intended, but I'm still confused on the normalized UV as well as the coordinate system of UV.

My solution (simplified):

vec2 uv = vec2(gl_FragCoord.x - (v_Resolution.x / 2.f), (gl_FragCoord.y / v_Resolution.y) + (v_Resolution.y / 2.f));

Now, if I remember correctly, the origin in a GLSL fragment shader is bottom left, so why do I have to subtract v_Resolution.x / 2.f instead of adding it? (also, why is the x-component not normalized, like the y-component?)

$\endgroup$
1
  • $\begingroup$ This website has an amazing introduction to shading, not only does it teach glsl, it introduces noise, fractal brownian motion, and frament shaders along with the coordinate system are discussed in depth:thebookofshaders.com $\endgroup$
    – pmw1234
    Commented Mar 7, 2021 at 12:42

1 Answer 1

5
$\begingroup$

Everything in this code functions as intended, until I try to translate the entire grid (grid lines) to the center of the screen by adding v_Resolution.x / 2 to uv.x.

Not sure what your intention is here since the Shadertoy example you referenced generates an infinite grid. So maybe you can clarify that.

However, I think I can help you with this problem:

... the ShaderToy example, the coordinates are not normalized at all, and it functions perfectly fine! I feel like I'm missing a very important concept here.

I think the basic problem here is, that you do not really understand how the mesh is generated and why the Shadertoy example isn't normalizing the uv-coordinates. If I can help you understand it, you can probably fix your other problems by yourself.

So let's walk through the Shadertoy code and what it actually does:

vec2 uv = fragCoord.xy;
    
// Correct the coordinates by taking into account the aspect ratio (turns the grid into perfect squares).
uv.x *= 1.0 / spacing;
uv.y /= spacing;

Here you first copy the frag coordinates into uv and afterward divide it by the spacing of your lines. Not sure why the author used those 'complicated' last two lines, but you can replace them simply with uv /= spacing;. The purpose of this step is to transform into a new space where the lines of your grid are located at every whole number. If your spacing is 20, like in the shader toy example, only the fragments with one of its frag coordinates being one of 0, 20, 40, etc. would produce a whole number and would be located on a line. Maybe you can already see where this is going.

// Adjust the origin of each line thicker than one pixel to its center.
uv.xy += (lineWidth > 1) ? width * 0.5 : width;
    
// Make the grid resolution independent by offsetting the Y coordinates based on the current viewport height.
uv.y -= (iResolution.y - 600.0) / spacing;
    
// Apply camera translation.
uv.x -= translation.x / spacing;
uv.y += translation.y / spacing;

Here he is just translating the grid to consider several things like line thickness and camera position. The camera position is probably what you are interested in. I have no clue, about the purpose of the second line, even though the comment specifies it, but it is just a y-shift of your grid. However, note that all translations must be in the "grid-space" like the uv coordinates. Hence he always divides by spacing. For width, he already applied the transformation several lines above -> float width = float(lineWidth) / spacing;

float grid = 0.0;
grid = max(step(fract(uv.x), width), grid); // X lines (horizontal)
grid = max(step(fract(uv.y), width), grid); // Y lines (vertical)

vec4 col = vec4(grid);

Now here comes the magic that actually produces the grid. Let's dissect the second line. fract(uv.x) just returns the fractional part of uv.x. So a value of 12.125 is reduced to 0.125. The step function returns 0 if the second value is smaller than the first and 1 otherwise. So the point of step(fract(uv.x), width) frac is to get a 1 if the uv coordinate is on a vertical line and 0 if not. The max in this line is pretty pointless since grid (which is set to 0.0) is either equal to the returned value or less. The third line does the same thing for horizontal lines, but here the max is actually needed. So to sum it up, grid is 1 if the fragment lies on a horizontal and/or vertical line and 0 if not. The last line just turns that into a vector. So you either get pure white ((1.0, 1.0, 1.0, 1.0)) as pixel color if the fragment is part of a line or pure black ((0.0, 0.0, 0.0, 0.0)) if not. Note that if alpha blending is enabled, black is actually translucent since the alpha channel is also 0.

if (col.r != 0.0 && col.g != 0.0 && col.b != 0.0) {
    float mx = mod(uv.x, float(darkenMultiple));
    float my = mod(uv.y, float(darkenMultiple));
        
    // Make lines that are a multiple of `darkenMultiple` darker than the rest.
    col.a = (mx < width || my < width) ? 0.2 : 0.15;
} else {
    // Turn all pixels that aren't apart of the grid invisible as to not cover the rest of the scene.
    col.a = 0.0;
}
    
col = showNyanCat(col, fragCoord);
    
fragColor = col;

This just adjusting the color of the grid and plotting the cat and not relevant to your question.

I optimized the shader from the Shadertoy example a little bit by removing obsolete instructions, postponing the transformation into "grid space" after applying the translations, and vectorizing some operations. Maybe it makes it easier for you to understand what is happening. I kept the original comments and added some own ones:

void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
    // Define some variables we use ---------------------------------------------
    // Simulate camera translation, just for this example.
    vec2 velocity = vec2(-30.0, 10.0);  
    translation += scale * iTime * velocity;
    
    float spacing = float(lineSpacing) * scale;
    float width   = float(lineWidth)   / spacing;
        
    // Add all translations to uv -----------------------------------------------
    // Apply camera translation.
    vec2 uv = fragCoord - translation;
    
    // Make the grid resolution independent by offsetting the Y coordinates based on the current viewport height.
    uv.y -= (iResolution.y - 600.0);
    
    // Adjust the origin of each line thicker than one pixel to its center.
    uv += (lineWidth > 1) ? float(lineWidth) * 0.5 : float(lineWidth);

    // Transform to "grid space" ------------------------------------------------    
    // Correct the coordinates by taking into account the aspect ratio (turns the grid into perfect squares).
    uv /= spacing;
    

    // Identify if pixel is on a line -------------------------------------------
    float grid =     step(fract(uv.x), width)       ; // X lines (horizontal)
    grid       = max(step(fract(uv.y), width), grid); // Y lines (vertical)
    
    // Set the correct color ----------------------------------------------------
    vec4 col = vec4(grid);
    if (grid != 0.0) {
        float mx = mod(uv.x, float(darkenMultiple));
        float my = mod(uv.y, float(darkenMultiple));        
        
        // Make lines that are a multiple of `darkenMultiple` darker than the rest.
        col.a = (mx < width || my < width) ? 0.2 : 0.15;
    } 
    
    // Plot the cat -------------------------------------------------------------
    col = showNyanCat(col, fragCoord);
    
    fragColor = col;
}

In this version, you can simply add your translations in pixels without any need to transform them to "grid space" or to normalize them. Hope that helps. You can copy it into the Shadertoy example to see that the results are the same.

Now, if I remember correctly, the origin in a GLSL fragment shader is bottom left, so why do I have to subtract v_Resolution.x / 2.f instead of adding it?

Probably because you use different signs here:

// Apply camera translation.
uv.x += v_CamPos.x / v_CellSize;
uv.y -= v_CamPos.y / v_CellSize;

(also, why is the x-component not normalized, like the y-component?)

I guess you fiddled around until "something" produced the expected image rather than knowing what happens. I see no point in investigating the "why" here. Understand the Shadertoy code (or my version) and then apply your changes while actually knowing what you are doing. If you use my version, all you have to add are the translations in pixels that you want to apply.


Update

Regarding the comment of Andrew Woo:

I really appreciate the in-depth explanation, it helped a lot in my understanding, however it doesn't really answer my [original] question, which was why the x-component of uv isn't normalized.

I was addressing that at the end of the original answer when I said that you fiddled around until "something" worked and I didn't saw any point in investigating why "obviously wrong" code produces the correct result.

Well, let's get something clear: uv coordinates are not screen coordinates. So gl_FragCoord.y / v_Resolution.y is not the general definition of v. I would rather call them "normalized screen coordinates". uv coordinates are (usually) used to map regular 2-dimensional data like an image onto a surface. You can read more about them on Wikipedia. Don't be fooled by the fact that the author of the question you linked used the "normalized screen coordinates" as uv coordinates. They might be the same if you want to map an image to your screen, but usually, they are not.

In the shadertoy example, the uv coordinates are used to map a single cell of your grid. You start using uv coordinates of the grid after you divided the frag coordinates by the cell size. Before that, they are no uv coordinates, at least not in relation to the grid.

So let's summarize what the shader does: You start with pixel coordinates inside your shader with values between 0 and the screen resolution in either direction and you want to calculate the uv coordinates of the grid in order to draw it. That's the whole purpose of the code we discussed. Do we need to divide by the screen resolution for that? No, we don't. We could if it would make our life easier to get from A to B, but we simply do not need it here and the original shadertoy algorithm doesn't do it.

So the question shouldn't be "why the x-component of uv isn't normalized" but:

"Why do I get the correct result when I use normalized screen coordinates instead of fragment coordinates for the y-component and wrong ones if not"?

I have not tested your code, but by just looking at it, I would say by normalizing the y component here: (gl_FragCoord.y / v_Resolution.y) + (v_Resolution.y / 2.f) you are more or less "removing" them from the line. Just insert the numbers:

  • gl_FragCoord.y / v_Resolution.y is a number between 0 and 1
  • v_Resolution.y / 2.f is 309

So at this point, you have v-values between 309 and 310. It is almost a constant. Simply remove gl_FragCoord.y / v_Resolution.y and you should still get the same result. Now the question arises, why is the grid still rendered correctly? I guess because of this line:

// Make the grid resolution independent by offsetting the Y coordinates based on the current viewport height.
    uv.y -= gl_FragCoord.y / v_CellSize;

Note that you use gl_FragCoord.y while the Shadertoy code used iResolution.y, which is a constant. What you do here is correcting the effective removal of gl_FragCoord.y I explained before. You need to divide by v_CellSize because the uv coordinates were also divided by v_CellSize 2 instructions before (uv.y /= v_CellSize;).

How to correct it?

Simply remove this line:

uv.y -= gl_FragCoord.y / v_CellSize;

Then remove the division through v_Resolution.y from here:

vec2 uv = vec2(gl_FragCoord.x, gl_FragCoord.y / v_Resolution.y + v_Resolution.y / 2 - v_CellSize / 2);

It should look like this:

void main() {

    float width = lineWidth / v_CellSize;

    vec2 uv = vec2(gl_FragCoord.x, gl_FragCoord.y + v_Resolution.y / 2 - v_CellSize / 2);

    uv.x *= 1.0 / v_CellSize;
    uv.y /= v_CellSize;

    // Adjust the origin of each line thicker than one pixel to its center.
    uv.xy += (lineWidth > 1) ? width * 0.5 : width;
    

    // Apply camera translation.
    uv.x += v_CamPos.x / v_CellSize;
    uv.y -= v_CamPos.y / v_CellSize;
    
    float grid = 0.0;
    grid = max(step(fract(uv.x), width), grid); // X lines (horizontal)
    grid = max(step(fract(uv.y), width), grid); // Y lines (vertical)
    
    vec4 col = vec4(grid);

    // ... insert rest of your code 
}

Now you should have the "correct" result and the x and y components are treated identically.

$\endgroup$
3
  • $\begingroup$ I really appreciate the in-depth explanation, it helped a lot in my understanding, however it doesn't really answer my [original] question, which was why the x-component of uv isn't normalized. $\endgroup$
    – Andrew Woo
    Commented Mar 4, 2021 at 19:11
  • $\begingroup$ @AndrewWoo Added an update. This should give you all the missing answers. $\endgroup$
    – wychmaster
    Commented Mar 6, 2021 at 18:12
  • $\begingroup$ Thanks, the update clears my question! I really appreciate it. I'm still a bit confused on the math, but I'm sure I can figure it out if I look at it more carefully when I have the time to do so. $\endgroup$
    – Andrew Woo
    Commented Mar 7, 2021 at 19:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.