Back

Software

GLSL Grids

Roy Varon Weinryb

varonroy@gmail.com
Introduction
Commonly, I find myself needing a background grid when working with OpenGL. Since grids are made of patterns that can be defined with simple equations, they are perfect for rendering with a fragment shader.
Checkerboard
The input to the shader is a rectangle, with uv coordinates ranging from \((0, 0)\) at to bottom left to \((1, 1)\) at to top left.
Then coordinates are transformed such that the corners of the camera correspond to the corners of the viewport. These are the 'world' coordinates.
Using the world coordinates and the location of the grid, a mask can be created - a variable whose value is \(1\) if the point in the world coordinates is inside the grid and \(0\) otherwise.
Then, two operations are performed: the coordinates are translated such that \((0, 0)\) is at the bottom left corner or the grid, and then modded with twice the size of the grid's cell size.
Each of the modded sections are then tested where \(x \lt 0.5\) and \(y \lt 0.5\).
Based on the above, sections can be tested for being on the diagonal and on the off-diagonal.
The result:
Your browser does not seem to support HTML canvas.
The fragment shader:
precision highp float;

// varying
varying vec2 uv;
varying vec2 screenSize;

// colors
vec3 bgColor = vec3(0.2);
vec3 gridColor1 = vec3(0.9);
vec3 gridColor2 = vec3(0.7, 0.5, 0.5);

// camera
vec2 cameraPos = vec2(0.0);
float cameraWidth = 2.0;
vec2 cameraSize = vec2(0.0);

// grid
vec2 gridCenter = vec2(0.3);
vec2 gridSize = vec2(1.0);
vec2 gridCellSize = vec2(0.2);

void main(void) {
	// match the camera's aspect ratio to the canvas's
	float cameraHeight = cameraWidth * screenSize.y / screenSize.x;
	cameraSize = vec2(cameraWidth, cameraHeight);

	// transform the uv coordinates to the to the camera's coordinates
	// (cx - cw / 2, cy - ch / 2) to (cx + cw / 2, cy + ch / 2)
	// now, each point on the screen will the the 'world' coordinate
	vec2 p = (uv - 0.5) * cameraSize + cameraPos;

	// check if the point is inside the bounds of the grid from each direction
	vec2 inGridMin = min(vec2(0.0), p - gridCenter - gridSize / 2.0);
	vec2 inGridMax = max(vec2(0.0), p - gridCenter + gridSize / 2.0);

	// check if the point is inside the grid or not
	float inGrid =
		sign(inGridMin.x) * sign(inGridMin.y) *
		sign(inGridMax.x) * sign(inGridMax.y);

	// get the relative position to the corner of the grid
	vec2 pRel = p - (gridCenter - gridSize / 2.0);

	// modulate the relative position with twice the cell size and normalize it
	vec2 pMod = mod(pRel, 2.0 * gridCellSize) / (2.0 * gridCellSize);

	// check when each of the componenents of pMod are smaller than 0.5
	vec2 isLower = 1.0 - step(0.5, pMod);

	// check if the point is on the diagonal
	float isOnDiagonal =
		(isLower.x * isLower.y) +
		(1.0 - isLower.x) * (1.0 - isLower.y);

	// map the diagonal to the fist color,
	// and the corss diagonal to the second one 
	vec3 gridColor = 
		gridColor1 * isOnDiagonal + 
		gridColor2 * (1.0 - isOnDiagonal);

	// calculate the final color
	vec3 col = gridColor * inGrid + bgColor * (1.0 - inGrid);

	// output to screen
	gl_FragColor = vec4(col, 1.0);
}
Line Grid
This shader is very similar to the previous one. In this shader, the coordinates are modded with the cell size (instead of twice the cell size). The goal is to fill a small area near the borders of the cells while keeping their insides transparent.
Checking if a point is near the borders reduces to constructing the following function: \[ f(x) = \begin{cases} 1\ \ \text{if}\ (x \lt l)\ \text{or}\ (x \gt 1.0 - l) \\ 0\ \ \text{otherwise} \\ \end{cases} \] Where \(x \in [0, 1]\). This function is symmetric around \(0.5\), so it can be simplified to: \[ f(x) = \begin{cases} 1\ \ \text{if}\ (-|x - 0.5| + 0.5 \lt l)\ \\ 0\ \ \text{otherwise} \\ \end{cases} \] Which can be neatly expressed with glsl's step function.
The Result:
Your browser does not seem to support HTML canvas.
The fragment shader:
precision highp float;

// varying
varying vec2 uv;
varying vec2 screenSize;

// colors
vec3 bgColor = vec3(0.2);
vec3 gridColor = vec3(0.85);

// camera
vec2 cameraPos = vec2(0.0);
float cameraWidth = 2.0;
vec2 cameraSize = vec2(0.0);

// grid
vec2 gridCenter = vec2(0.3);
vec2 gridSize = vec2(1.0);
vec2 gridCellSize = vec2(0.2);
vec2 gridCellThickness = vec2(0.08);

void main(void) {
	// match the camera's aspect ratio to the canvas's
	float cameraHeight = cameraWidth * screenSize.y / screenSize.x;
	cameraSize = vec2(cameraWidth, cameraHeight);

	// transform the uv coordinates to the to the camera's coordinates
	// (cx - cw / 2, cy - ch / 2) to (cx + cw / 2, cy + ch / 2)
	// now, each point on the screen will the the 'world' coordinate
	vec2 p = (uv - 0.5) * cameraSize + cameraPos;

	// check if the point is inside the bounds of the grid from each direction
	vec2 inGridMin = min(vec2(0.0), p - gridCenter - gridSize / 2.0);
	vec2 inGridMax = max(vec2(0.0), p - gridCenter + gridSize / 2.0);

	// check if the point is inside the grid or not
	float inGrid =
		sign(inGridMin.x) * sign(inGridMin.y) *
		sign(inGridMax.x) * sign(inGridMax.y);

	// get the relative position to the corner of the grid
	vec2 pRel = p - (gridCenter - gridSize / 2.0);

	// modulate the relative position with twice the cell size and normalize it
	vec2 pMod = mod(pRel, gridCellSize) / (gridCellSize);

	// checks if the point is near the border of the cell
	vec2 nearBorderVec = 1.0 - step(gridCellThickness, -abs(pMod - 0.5) + 0.5);
	float nearBorder = clamp(nearBorderVec.x + nearBorderVec.y, 0.0, 1.0);

	float fill = inGrid * nearBorder;

	// output to screen
	gl_FragColor =
		(1.0 - fill) * vec4(vec3(bgColor), 1.0) +
		fill * vec4(vec3(gridColor), 1.0);
}
Hex Grid
Using the radius of the hexagon, its width and height can be calculated:
The next step is to draw a single hexagon. This will be a function that will return \(1\) for points inside the hexagon and \(0\) for points outside. The hexagon will be assumed to be centered at \((0, 0)\) with radius \(r\). Using symmetry, the function can be simplified to test only positive points.
\[ h(x, y) = \begin{cases} 1\ \ \text{if}\ (x \lt w)\ \text{and}\ (y \lt r-\frac{r}{2w}x) \\ 0\ \ \text{otherwise} \\ \end{cases} \]
Two hexagons can be put together by translating the coordinates by the width. Each hex can be multiplied by a different color. \[ h(x, y)\vec{c_1} + h(x - 2w)\vec{c_2} \] Then, the two hexagons can be made into a row with the \(\text{mod}\) function. When repeating shapes using \(\text{mod}\) it is important to place the entire position in the positive quadrant otherwise the shape will be cut. So the hexes are shifted to the right by \(w\) and to the top by \(r\).
Then, the top operations can be called again but with a shift of half a hexagon and different colors.
Then, each row is modulated in the \(y\) direction by \((y \mod 3r)\). \(3r\) is the distance between two hex rows.
Finally each of the rows need to modulated separately. The reason for that is that only rectangular regions can be modulated and two hexagonal rows are not rectangular. Modulating the two rows together would end up with missing regions.
Your browser does not seem to support HTML canvas.
Here is the result with separate modulation:
Your browser does not seem to support HTML canvas.
The fragment shader:
precision highp float;

// constants
float COS_30 = sqrt(3.0) / 2.0;

// varying
varying vec2 uv;
varying vec2 screenSize;

// colors
vec3 bgColor = vec3(0.2);
vec3 gridColor1 = vec3(0.8, 0.5, 0.5);
vec3 gridColor2 = vec3(0.8, 0.8, 0.5);
vec3 gridColor3 = vec3(0.8, 0.5, 0.8);
vec3 gridColor4 = vec3(0.8, 0.8, 0.8);

// camera
vec2 cameraPos = vec2(0.0);
float cameraWidth = 2.0;
vec2 cameraSize = vec2(0.0);

// grid
vec2 gridCenter = vec2(0.0);
vec2 gridSize = vec2(1.0);
float hexR = 0.1;
float hexW = COS_30 * hexR;

float inHex(vec2 p) {
	p = abs(p);

	float targetHeight = hexR + (hexR / 2.0 - hexR) / hexW * p.x;

	float underLine = 1.0 - step(targetHeight, p.y);

	return underLine * (1.0 - step(hexW, p.x));
}


void main(void) {
	// match the camera's aspect ratio to the canvas's
	float cameraHeight = cameraWidth * screenSize.y / screenSize.x;
	cameraSize = vec2(cameraWidth, cameraHeight);

	// transform the uv coordinates to the to the camera's coordinates
	// (cx - cw / 2, cy - ch / 2) to (cx + cw / 2, cy + ch / 2)
	// now, each point on the screen will the the 'world' coordinate
	vec2 p = (uv - 0.5) * cameraSize + cameraPos;

	// check if the point is inside the bounds of the grid from each direction
	vec2 inGridMin = min(vec2(0.0), p - gridCenter - gridSize / 2.0);
	vec2 inGridMax = max(vec2(0.0), p - gridCenter + gridSize / 2.0);

	// check if the point is inside the grid or not
	float inGrid =
		sign(inGridMin.x) * sign(inGridMin.y) *
		sign(inGridMax.x) * sign(inGridMax.y);

	// get the relative position to the corner of the grid
	vec2 pRel = p - (gridCenter - gridSize / 2.0);

	vec3 col = vec3(0.0);

	// gird of colors 1 and 2
	vec2 p1 = pRel;
	p1.x = mod(p1.x, hexW * 4.0);
	p1.y = mod(p1.y, hexR * 3.0);
	col +=
		gridColor1 * inHex(p1 - vec2(hexW * 1.0, hexR)) +
		gridColor2 * inHex(p1 - vec2(hexW * 3.0, hexR));
	
	// gird of colors 3 and 4
	vec2 p2 = pRel - vec2(hexW, hexR * 1.5);
	p2.x = mod(p2.x, hexW * 4.0);
	p2.y = mod(p2.y, hexR * 3.0);
	col +=
		gridColor3 * inHex(p2 - vec2(hexW * 1.0, hexR)) +
		gridColor4 * inHex(p2 - vec2(hexW * 3.0, hexR));

	// output to screen
	gl_FragColor = vec4(col * inGrid + bgColor * (1.0 - inGrid), 1.0);
}