Handpainted Light Shader: Cross-Hatching in Godot showed how to use textures in light shaders to apply hand-painted shadows. We can use a similar technique for halftone shading, where shades of darkness are represented by black dots of varying size:

image.png

This effect reads from a texture and wraps it in a step function to get dots of varying size, much like how we got a line of varying thickness in Godot Shader for Beginners: Lightning Strike Effect using step and smoothstep. The dot texture looks like this:

dot.png

We read from this texture and apply a step function to get a dot based on the diffuse value, calculated as discussed in Understanding Godot Light Shaders and Light Calculations by Implementing a Toon Light Shader:

void light() {
	float diffuse_value = dot(LIGHT, NORMAL);

	float light_value = step(texture(dot_texture, dot_uv).r,
		diffuse_value);

	DIFFUSE_LIGHT += light_value
}

We read from the texture with the UV coordinates dot_uv. These are calculated in the fragment shader, since we need some variables which we don’t have access to in the light function. The coordinates are based on SCREEN_UV, which is independent of the object’s UV coordinates: it is always (0, 0) if we’re on the upper left edge of the screen and (1, 1) at the lower right edge, since we always want the dots to be in a perfect grid and remain stationary, regardless of the geometry they’re on. Additionally, we want the dots to be perfectly round. Therefore, we must take the viewport’s aspect ratio into account. This is how dot_uv is calculated and passed over:

varying vec2 dot_uv;

void fragment() {
	dot_uv = SCREEN_UV * 50.0;

	float aspect = VIEWPORT_SIZE.x / VIEWPORT_SIZE.y;
	dot_uv.x *= aspect;
}

Of course, this shader (alone) does not accurately replicate halftone printing - if that’s the goal, a more elaborate post-processing shader is needed. The general approach would be the same; however, light shaders have the advantage of keeping full artistic freedom over the rest of the coloring logic.