Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Simple "Infinite" Grid Shader
Javier Salcedo
Javier Salcedo

Posted on • Edited on

     

Simple "Infinite" Grid Shader

I tried to follow a "recipe" for an infinite grid shader, but after hitting a bug and not being able to fix it because I didn't understand the code, I decided to write it from scratch myself so I could understand it properly.

In the process I found better implementations than the original one I was following, and better than my own.

In the end I came up with a different, simpler approach, although more limited in some aspects, that ticks all my boxes at the moment.

You can see the codehere.
Here's a detailed step-by-step explanation of how it works.

⚠️ DISCLAMER 1:
The goal of this article is just to document and organise my thought process while I was trying to understand the code behind another implementation.
If you want something robust / production-worthy, you should readthis instead.
this one is mine

⚠️ DISCLAIMER 2:
I'm using MSL (Metal Shading Language) because I like it and because my toy renderer uses Metal as the graphics API anyway, but it should be pretty straightforward to translate it to GLSL or HLSL.

⚠️ DISCLAIMER 3:
This article will only focus on the shader part, but the setup in the host (CPU) side should be very simple regardless of API:

  • Set everything up to render a quad (2 triangles)
  • Enable depth testing
  • Disable face culling (we want to be able to see it from bellow too)
  • Because it's transparent it should be renderedafter the opaque objects

1. Finite version

1.1 Vertex Shader

staticconstantautogrid_size=1.0f;staticconstantfloat4positions[4]{  {-0.5,0.0, 0.5,1.0},  { 0.5,0.0, 0.5,1.0},  {-0.5,0.0,-0.5,1.0},  { 0.5,0.0,-0.5,1.0}};structVertexOut{  float4position[[position]][[invariant]];  float2coords;};vertexVertexOutmain(uintid[[vertex_id]],float4x4view_proj[[buffer(N)]]){autoworld_pos=positions[id];world_pos.xyz*=grid_size;return{.position=view_proj*world_pos,.coords=world_pos.xz};}
Enter fullscreen modeExit fullscreen mode

This will create a quad of size 1x1 centred at the origin.

If we output the normalised coordinates in the fragment shader...

usingFragmentIn=VertexOut;fragmentfloat4main(FragmentInfrag[[stage_in]]){  autoc=float4(frag.coords/grid_size+0.5,0.0,1.0);  returnc;}
Enter fullscreen modeExit fullscreen mode

...you'll get something like this:
quad UVs

1.2 Fragment Shader

1.2.0 Modulo

⚠️ IMPORTANT in case you're using MSL or HLSL

GLSL defines its modulo function asx - y * floor(x/y)
which produces this type of repeating pattern:
GLSL's mod

However, bothMSL andHLSL define it asx - y * trunc(x/y),
which produces this:
fmod

Since I'm using MSL, I'll add a template function that mimics GLSL'smod:

template<typenameT,typenameU>constexprTmod(Tx,Uy){returnx-y*floor(x/y);}
Enter fullscreen modeExit fullscreen mode

For HLSL you'll have to rely on macros:

#define mod(x,y) ((x) - (y) * floor((x)/(y)))
Enter fullscreen modeExit fullscreen mode

EDIT: From HLSL 2021 you can also use templates.

1.2.1 Draw the cells

We'll subdivide the grid into 2 types of cells:

  • Big 1x1m cells
  • Small 0.1x0.1 (sub)cellsTo make it clearer to see, I'm increasing the plane size to 2x2m```C++

static constant auto grid_size = 2.0f;

static constant auto cell_size = 1.0f;
static constant auto half_cell_size = cell_size * 0.5f;

static constant auto subcell_size = 0.1f;
static constant auto half_subcell_size = subcell_size * 0.5f;

We start by calculating the coordinates inside the cells and subcells.1. First, displace the plane coordinates so the world's origin is in a corner rather than in the middle of the (sub)cell.2. Then get the coordinates inside the (sub)cell```C++auto cell_coords    = mod( frag.coords + half_cell_size,    cell_size    );auto subcell_coords = mod( frag.coords + half_subcell_size, subcell_size );
Enter fullscreen modeExit fullscreen mode

If we normalise and output this as a colour, we'll get something like this:

Cell UVsSubcell UVs
Cell UVsSubcell UVs

Next we calculate the distances in U and V to the (sub)cell's edge.
The coordinates we calculated before are in the [0, (sub)cell_size] range so:

  1. First, we transform them into the [-(sub)cell_size/2, (sub)cell_size/2] range so the center of the (sub)cell has the (0,0) coordinates.
  2. Then, the distance to the edge is simply the absolute of these new coordinates
autodistance_to_cell=abs(cell_coords-half_cell_size);autodistance_to_subcell=abs(subcell_coords-half_subcell_size);
Enter fullscreen modeExit fullscreen mode

Now it's time to draw the lines.
Comparing that distance to the (sub)cell line thickness, we determine if we should draw the line or not:

staticconstantautocell_line_thickness=0.01f;staticconstantautosubcell_line_thickness=0.001f;staticconstantautocell_colour=float4(0.75,0.75,0.75,0.5);staticconstantautosubcell_colour=float4(0.5,0.5,0.5,0.5);...// In the fragment shaderautocolor=float4(0);if(any(distance_to_subcell<subcell_line_thickness*0.5))color=subcell_color;if(any(distance_to_cell<cell_line_thickness*0.5))color=cell_color;returncolor;
Enter fullscreen modeExit fullscreen mode

The line thicknesses are halved because only half the line is within a given (sub)cell, as the other half is in the neighbouring (sub)cell

However, this has obvious issues (I made the plane opaque and the lines yellow to make it clearer):
First Grid

1.2.2 Get uniform lines

So the problem here is that, due to perspective, the coordinates can change drastically from one fragment to another, meaning that the exact coordinates that fall within the line might be skipped from one fragment to the next.

An easy solution is to increase the line width by how much the coordinates change across fragments.
Thankfully, we get a very handy tool to get that change: partial derivatives.
If you don't know what they are or how they work, I recommend readingthis article.

autod=fwidth(frag.coords);autoadjusted_cell_line_thickness  =0.5*(cell_line_thickness  +d);autoadjusted_subcell_line_thickness=0.5*(subcell_line_thickness+d);autocolor=float4(0);if(any(distance_to_subcell<adjusted_subcell_line_thickness))color=subcell_color;if(any(distance_to_cell<adjusted_cell_line_thickness))color=cell_color;returncolor;
Enter fullscreen modeExit fullscreen mode

Corrected Grid

And this is how it looks with dimensions 100x100m and the actual colour:
Corrected Grid 2

1.3 Fade out

This already looks pretty good, but you can see issues in the distance: mainly aliasing andMoiré patterns.
There's a lot of literature about how to fix these kind of issues, but I decided to take a different approach...

...just fade out the grid around the camera so you can't see them!
Lazy Filtering

First, we need the camera position.
Let's make the necessary changes in the vertex shader:

structVertexIn{float4x4view_proj;float4camera_pos;};structVertexOut{float4position[[position]][[invariant]];float3camera_pos[[flat]];float2coords;};vertexVertexOutmain(uintid[[vertex_id]],constantVertexIn&vert_in[[buffer(N)]]){autoworld_pos=positions[id];world_pos.xyz*=grid_size;return{.position=vert_in.view_proj*world_pos,.camera_pos=vert_in.camera_pos,.coords=world_pos.xz};}
Enter fullscreen modeExit fullscreen mode

Note the[[ flat ]]: this is an attribute that disables interpolation, so we have the same value for all fragments.

Of course, in the host you'll need to add the camera position to the buffer too.

After that we calculate the horizontal distance to the camera, and use that to interpolate:

staticconstautomax_fade_distance=25.0f;...// In the fragment shaderfloatopacity_falloff;{autodistance_to_camera=length(frag.coords-frag.camera_pos.xz);opacity_falloff=smoothstep(1.0,0.0,distance_to_camera/max_fade_distance);}returncolor*opacity_falloff;
Enter fullscreen modeExit fullscreen mode

I've found that a 25m fade radius works pretty well at a camera height of 1m, but it's too small when the camera is very high, and too big if it's very low.
So the fade radius will now be adjusted by height, keeping that 1:25 ratio.

staticconstantautoheight_to_fade_distance_ratio=25.0f;...// In the fragment shaderautofade_distance=abs(frag.camera_pos.y)*height_to_fade_distance_ratio;opacity_falloff=smoothstep(1.0,0.0,distance_to_camera/fade_distance);
Enter fullscreen modeExit fullscreen mode

However, if the camera gets very close to the plane, that radius will approach zero, so I added a minimum radius.
And, if the camera gets very high, you can see the shape of the quad, so I added a maximum.

staticconstantautomin_fade_distance=grid_size*0.05f;staticconstantautomax_fade_distance=grid_size*0.5f;...// In the fragment shaderautofade_distance=abs(frag.camera_pos.y)*height_to_fade_distance_ratio;{fade_distance=max(fade_distance,min_fade_distance);fade_distance=min(fade_distance,max_fade_distance);}opacity_falloff=smoothstep(1.0,0.0,distance_to_camera/fade_distance);
Enter fullscreen modeExit fullscreen mode

Et voilà!
Final Grid


2. Make it "infinite"

We're almost done, but at the moment you can (eventually) move outside of the plane.
Thankfully, it has a very easy solution, just move the planeand the UVs with the camera. Think of it as a moving treadmill.
Treadmill cat

In the vertex shader:

autoworld_pos=positions[id];world_pos.xyz*=grid_size;world_pos.xz+=vert_in.camera_pos.xz;}
Enter fullscreen modeExit fullscreen mode

An important detail is to ignore the Y component of the camera position. We only want the grid to movehorizontally, it shouldn't change when you move vertically.


3. Conclusions and future work

Even though there're objectively better approaches, this solution has scratched my brain itch (for now) and provides a good enough gizmo for my purposes.

The process of reinventing the wheel was a bit frustrating at times, but I'm glad I did. I wouldn't be happy having a copy-pasted piece of code that I don't understand and thus can't debug.
I learn best by doing so it was a great exercise.

Also, the process of writing this article forced me to dissect the code to be able to provide step by step examples. That uncovered a series of bugs and misunderstandings in my renderer and the shader itself. As a result, the version explained here is significantly different from the first one I wrote.

In terms of future work, there's still a bunch of stuff to be done:

  • Fix the fade to black in the distance
  • Actually filter the grid instead of using that hacky fade
  • Colour the X/Z axis
  • Add a Y axis at 0,0?

However, at the time of writing this, neither sound very fun, and I have a loong list of other features that do.
At the end of the day, this is a toy project and its sole purpose is to have fun while I experiment and learn.
This one doesn't spark joy
So I consider it done (for now).


📚 References

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Being able to create almost anything with what essentially is just an arbitrary arrangement of words is the closer I’ll get to real world magic, and that is why I love programming.
  • Location
    London, UK
  • Education
    Telecommunications Engineering Bachelor and Computer Graphics Master
  • Work
    Graphics programmer
  • Joined

More fromJavier Salcedo

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp