Dynamic crosshairs with shaders

Hahaha crosshair go brrrrrrr

Posted by 2nafish117 on March 05, 2021 · 8 mins read

Shaders are cool, dynamic crosshairs too. So how about we use some shader stuff to make dynamic crosshairs like the one below.

Health on the left, ammo on the right.

If you can't see the above shader try using a desktop browser.
I'll be using plain old glsl in my code snippets here, feel free to port it to any other shading language out there. If you want to try it out right in your browser check out book of shaders editor. The Book of Shaders is also an excellent resource if you want to learn more about shaders. Shout out to glslCanvas.js. It is what is being used to render the shaders on my blog.

The Idea

The basic idea of creating shapes in a fragment shader is to make a mask using shader functions that fills the crosshair shape you want, and then animate this mask using time or another uniform.

You can create multiple simple masks and then put them together to make composite masks. You can sometimes get away with texture masks instead of creating them by shader code. But the beauty of anything made with a shader are that they can be scaled arbitrarily without loss in quality. Finally this mask can be multiplied with a color to get your final crosshair.

The Code

Here are a few simple masks that can be used to make more complex masks and how they look.

// circle mask
float mask_circle(vec2 st, vec2 center, float radius) {
	return 1.0 - smoothstep(radius - SMOOTH_AMOUNT, 
	radius + SMOOTH_AMOUNT, 
	length(st - center));
}

// elliptical mask
float mask_ellipse(vec2 st, vec2 center, float r1, float r2) {
	st -= center;
	float d = st.x * st.x / (r1 * r1) + st.y * st.y / (r2 * r2);
	return 1.0 - smoothstep(1.0 - 10.0 * SMOOTH_AMOUNT, 
		1.0 + 10.0 * SMOOTH_AMOUNT, d);
}

// ring mask
float mask_ring(vec2 st, vec2 center, float in, float out) {
	return (1.0 - mask_circle(st, center, in)) * 
		mask_circle(st, center, out);
}

// plus mask
float mask_plus(vec2 st, vec2 center, float width) {
	return 1.0 - min(
		smoothstep(width - SMOOTH_AMOUNT, 
			width + SMOOTH_AMOUNT, abs(st.x)), 
		smoothstep(width - SMOOTH_AMOUNT, 
			width + SMOOTH_AMOUNT, abs(st.y))
	);
}

// square mask
float mask_square(vec2 st, vec2 center, float width) {
	return 1.0 - max(
		smoothstep(width - SMOOTH_AMOUNT, 
			width + SMOOTH_AMOUNT, abs(st.x)), 
		smoothstep(width - SMOOTH_AMOUNT, 
			width + SMOOTH_AMOUNT, abs(st.y))
	);
}

Circular mask.

Elliptical mask.

Ring mask.

Plus mask.

Square mask.

Polygon masks.

You can see the extensive use of simple shader functions like smoothstep, min, max, and abs. The mask_plus works by doing a union of a vertical and horizontal strip to form a plus. The mask_square works in the exact opposite way, it uses the intersection of the vertical and horizontal strip to form a square. The mask_ring works by using two mask_circle calls and cutting out the inner circle from the outer circle. You can see now how you can combine multiple masks, this can be generalised similar to set theory!!?

// Mask Arithmetic? WTF
mask = 1.0 - mask   // inverse of mask

// union and intersection using max, min
mask = max(mask1, mask2) // mask1 union mask2
mask = min(mask1, mask2) // mask1 intersection mask2

// union and intersection using addition, multiplication
mask = clamp(0.0, 1.0, mask1 + mask2) // mask1 union mask2
mask = mask1 * mask2 				// mask1 intersection mask2

// here intersection can be any of the methods shown above
mask = mask1 intersection (1.0 - mask2) // mask1 set-subtract mask2

Who knows, maybe more of the boolean algebra works on these? Maybe de-morgans laws. An idea for another post perhaps.

Another trick to move your masks is transforming the uvs before passing them into the masks. You can do stuff like translating, rotating and scaling, the masks.

Translated mask

Scaled mask

Rotated mask

These are the glsl functions that transform the uv space.

// transform functions go BRRRRR
vec2 translate(vec2 uv, vec2 t) {
	return uv + t;
}

vec2 scale(vec2 uv, vec2 s) {
	return uv / s;
}

// rotate about mid
vec2 rotate(vec2 uv, float rotation, vec2 mid) {
	return vec2(
		cos(rotation) * (uv.x - mid.x) + 
		sin(rotation) * (uv.y - mid.y) + mid.x,
		cos(rotation) * (uv.y - mid.y) - 
		sin(rotation) * (uv.x - mid.x) + mid.y
	);
}

Here are a few more crosshairs and fun shaders to look at.

Reticule inspired from halo's needler.

Halo 5's boltshot reticule.

A very unoriginal crosshair.

Doom ballista reticule.

TLDR: masks and transformations, Half life, Needler, Halo 5's boltshot reticule, Regular crosshair