In Understanding Godot Light Shaders and Light Calculations by Implementing a Toon Light Shader, we discussed light shaders and how you can use functions like step() to switch from realistic to toon lighting. Here, we will use light shaders along with a hand-painted texture to create a cross-hatching effect. Cross-hatching is a (pencil) shading technique that looks like this:

Veronica_detail.jpg

Bright areas are left blank, whereas darker areas are shaded with parallel lines. The darker an area is, the more different directions of pencil strokes are added. A logical procedure for a shader could therefore look like this:

  1. Start with bright light.
  2. Check the diffuse value.
  3. If the diffuse value is high, subtract horizontal paint strokes from the light.
  4. If the diffuse value is low, subtract additional paint stroke directions.

In order to be able to subtract different paint stroke directions, we’ll have to find a way to create hand-painted textures resembling cross-hatching, and design them in a way that makes it possible to “deconstruct” it to produce different shades. In order words, we need a texture that contains different paint stroke directions which we can read separately. For this, we can use the individual channels of an RGB texture: red strokes can be horizontal, green strokes vertical, and blue strokes diagonal. You can hand-paint such a texture in any way you like (even use physical pen and paper, scan it, and pack it into separate color channels), just three things are important:

  • The background must be black, so that these areas are left white.
  • Make sure the texture tiles. You can offset it by 1/2 its width and height to check and fill potential gaps.
  • Use “Additive” blending in the paint strokes, so when e.g. red and green overlap, you get yellow.

The texture I came up with looks like this:

hatch.png

Now we just need a shader to read and apply this texture as we described above.

We start with bright light, i.e. a value of 1.0. The next step is to calculate the diffuse value. We do this as described in Understanding Godot Light Shaders and Light Calculations by Implementing a Toon Light Shader, by calculating the dot product between the surface normal and the light direction:

float diffuse_value = dot(LIGHT, NORMAL) * ATTENUATION;

Next, we check if this value is lower than a certain threshold and, if it is, subtract the first channel of paint strokes, which we read from the texture. Doing something if a value is higher/lower than a certain threshold should sound familiar from Godot Shader for Beginners: Lightning Strike Effect using step and smoothstep: it’s a typical use case for the step function - or, if we want to have a nice transition, smoothstep. Therefore, we can apply the first paint strokes like this:

float light_value = 1.0;
vec2 hatch_uv = UV * 13.0;

light_value -= smoothstep(0.9, 0.7,
	diffuse_value) * texture(cross_hatch_texture, hatch_uv).r;

Note that we start with a high value, 0.9, and follow with a low value, 0.7. This inverts the smoothstep, which is what we want: diffuse values greater than 0.9 should yield 0.0, whereas diffuse values smaller than 0.7 should give 1.0.

Similarly, we apply the other two channels using lower thresholds:

light_value -= smoothstep(0.6, 0.4,
	diffuse_value) * texture(cross_hatch_texture, hatch_uv).g;

light_value -= smoothstep(0.3, 0.1,
	diffuse_value) * texture(cross_hatch_texture, hatch_uv).b;

And lastly, make sure to clamp the result (we might get areas with negative values otherwise) and apply it:

light_value = clamp(light_value, 0.0, 1.0);
	
DIFFUSE_LIGHT += light_value;

This is how the resulting shader looks:

image.png

shader_type spatial;

uniform sampler2D cross_hatch_texture: filter_linear_mipmap_anisotropic;

void light() {
	float diffuse_value = dot(LIGHT, NORMAL) * ATTENUATION;
	
	float light_value = 1.0;
	vec2 hatch_uv = UV * 13.0;
	
	light_value -= smoothstep(0.9, 0.7,
		diffuse_value) * texture(cross_hatch_texture, hatch_uv).r;
	
	light_value -= smoothstep(0.6, 0.4,
		diffuse_value) * texture(cross_hatch_texture, hatch_uv).g;
	
	light_value -= smoothstep(0.3, 0.1,
		diffuse_value) * texture(cross_hatch_texture, hatch_uv).b;
	
	light_value = clamp(light_value, 0.0, 1.0);
	
	DIFFUSE_LIGHT += light_value;
}

Understanding this effect and how it combines hand-painting with data-driven shaders opens the door for a variety of interesting styles. Give it a try and experiment with the possibilities!

One example of a similar effect can be seen here: Pop-Art Light Shader: Halftone Shading in Godot