The typical specular highlight represents a glossy surface where there’s a particular spot that much of the light reflects off of. It will shift slightly depending on how you look at it, but in general the path from your eye to the spot is even and concentrated.
Anisotropy is a concept where different viewing angles have a drastic effect on how it’s viewed. For a specular highlight, this can turn the specular spot into a specular band that wraps around the object. In our case, we use it to make a shader that gives a hair-like highlight typical of cel shaded hair.
In anisotropic highlighting, as the camera goes up and over or under the object, the highlight follows suit to the poles vertically.
To achieve that hair-like effect, we sample from some noise textures and compose the data together to split up ‘strands’ and ‘clumps’ of hair.
A note on hair and UVs: Because we’re using the model’s UVs to sample the noise, to get a proper hair effect, the UVs of the model should be laid out in a nice grid with the top of the hair on top and the bottom at the bottom. For the purposes of this tutorial, Godot’s sphere has that grid pattern so you can use that.
Since the angle of the camera affects the highlight, we need a point of reference. We calculate this in the vertex shader as a vector that points down multiplied by the camera’s matrix. We pass it along to the fragment shader in a varying variable.
varying vec3 down_camera_angle; void vertex() { down_camera_angle = (vec4(0, -1, 0, 1) * CAMERA_MATRIX).xyz; }
Write in some uniforms for noise: high frequency for sharp strands, low frequency for clumps, and spottiness to split up hair clumps and leave gaps.
uniform sampler2D high_frequency_anisotropy_noise : hint_black; uniform sampler2D low_frequency_anisotropy_noise : hint_black; uniform sampler2D spottiness_anisotropy_noise : hint_black;
Create new NoiseTextures
for each parameter, and apply a new OpenSimplexNoise
to each NoiseTexture’s Noise parameter. Low frequency noise should have a high Period so it looks softer, and high frequency/spottiness should have low Period so it looks sharper.
We need to control the highlight, so we introduce some uniforms and constants. Since this is a form of specular, we can re-use specular’s softness and color. The new ones are:
uniform float anisotropy_specular_strength : hint_range(0, 1) = 0.0; uniform float anisotropy_specular_contrast : hint_range(0, 12) = 5.0; uniform float anisotropy_specular_brightness : hint_range(0, 2) = 0.85; uniform float anisotropy_in_shadow_strength : hint_range(0, 1) = 0.1;
Whether you put it before or after the current specular code is up to you; you shouldn’t have both on at the same time anyway.
The goal is to create a band of black and white that gets distorted by the noise. Outside of that should be black, and the middle should be brightest. This can be achieved with two smoothstep
calls: one for the top half, and one for the bottom half. We use the vertical UV so that the band follows the vertical contour of the shape.
float anisotropy_uv = UV.y; float lower_sample = smoothstep(0.4, 0.5, anisotropy_uv); float higher_sample = 1.0 - smoothstep(0.5, 0.6, anisotropy_uv); float anisotropy_sample = lower_sample * higher_sample;
We can change those magic numbers with constants, as before:
float lower_sample = smoothstep(ANISOTROPY_BAND_MIN, ANISOTROPY_BAND_MIDDLE, anisotropy_uv); float higher_sample = 1.0 - smoothstep(ANISOTROPY_BAND_MIDDLE, ANISOTROPY_BAND_MAX, anisotropy_uv);
The UVs of the sphere are a grid top to bottom, so every UV is a percentage of how high they are along the model vertically.
We can offset it by how it’s viewed by the camera. At full strength it’s far too strong, but if we take a third or so, we get a pleasing ring effect.
float anisotropy_uv = UV.y - down_camera_angle.z * 0.33;
If we offset that UV even further by our low and high frequency noises, sampled vertically to give a banding effect, we can start to get some nice hair-like distortions going.
We subtract 0.5
from the UV and divide the result by 5
to reduce the effect’s intensity. We only want to subtly shift the band around, not turn it into thin shards.
float high_anisotropy_noise_value = (texture(high_frequency_anisotropy_noise, vec2(UV.x, 0)).r - 0.5) * 0.2; float low_anisotropy_noise_value = (texture(low_frequency_anisotropy_noise, vec2(UV.x, 0)).r - 0.5) * 0.2; float anisotropy_uv = UV.y - down_camera_angle.z * 0.33 + high_anisotropy_noise_value + low_anisotropy_noise_value;
Finally, we sample from the spottiness noise, and multiply the sample by it to create gaps.
float spottiness_anisotropy_noise_value = (texture(spottiness_anisotropy_noise, vec2(UV.x, 0)).r - 0.5); anisotropy_sample *= spottiness_anisotropy_noise_value;
On its own, that makes the result too weak so let’s control the brightness and contrast. Brightness is a value added to the sample, and contrast is a multiplier.
float spottiness_anisotropy_noise_value = (texture(spottiness_anisotropy_noise, vec2(UV.x, 0)).r - 0.5) * anisotropy_specular_contrast + anisotropy_specular_brightness; anisotropy_sample *= spottiness_anisotropy_noise_value;
To control how sharp and soft it is, we make a sharp and a soft version with smoothstep
, like with the specular.
float sharp_anisotropy_value = smoothstep(ANISOTROPY_SHARPNESS_MIN, ANISOTROPY_SHARPNESS_MAX, anisotropy_sample); float soft_anisotropy_value = smoothstep(ANISOTROPY_SOFTNESS_MIN, ANISOTROPY_SOFTNESS_MAX, anisotropy_sample);
We must naturally set those constants to decent values.
const float ANISOTROPY_SHARPNESS_MIN = 0.34; const float ANISOTROPY_SHARPNESS_MAX = 0.35; const float ANISOTROPY_SOFTNESS_MIN = 0.06; const float ANISOTROPY_SOFTNESS_MAX = 0.36;
And use the specular_softness
as the factor to mix the two together.
float anisotropy_value = mix(sharp_anisotropy_value, soft_anisotropy_value, specular_softness);
We create a final color with the specular_color
uniform with it, and add it to the outbound color. The anisotropy strength controls how much of it should show up.
vec3 anisotropy_color = specular_color.rgb * anisotropy_value; out_color = out_color + (anisotropy_color * anisotropy_specular_strength);
You could stop here, but the light has no effect on the highlight yet. We fix that by creating a ‘hotspot’ where the specular highlight should be. Any of the anisotropic highlight outside of that can be turned off or made weaker.
The code is close to the one we used with the specular size:
float anisotropy_specular_hotspot = smoothstep( ANISOTROPY_HOTSPOT_MIN, ANISOTROPY_HOTSPOT_MAX, specular * anisotropy_specular_width);
With a softened pair of constants.
const float ANISOTROPY_HOTSPOT_MIN = 0.083; const float ANISOTROPY_HOTSPOT_MAX = 0.64;
If we multiply this hotspot by our anisotropy sample, we get a specular that responds to the light.
anisotropy_sample *= spottiness_anisotropy_noise_value * anisotropy_specular_hotspot;
But we’ve lost all highlights inside of shadows. Depending on your style, that may be fine, but you could bring a faded out version. We take the maximum of the hotspot and the anisotropy_in_shadow_strength
uniform as the hotspot multiplier instead.
anisotropy_sample *= spottiness_anisotropy_noise_value * max(anisotropy_specular_hotspot, anisotropy_in_shadow_strength);
shader_type spatial; render_mode unshaded; //Specular constants const float SPECULAR_SOFT_MIN = 0.0; const float SPECULAR_SOFT_MAX = 0.64; const float SPECULAR_HARD_MIN = 0.17; const float SPECULAR_HARD_MAX = 0.18; //Outline constants const float OUTLINE_MIN = 0.45; const float OUTLINE_MAX = 0.47; //Anisotropic constants const float ANISOTROPY_SHARPNESS_MIN = 0.34; const float ANISOTROPY_SHARPNESS_MAX = 0.35; const float ANISOTROPY_SOFTNESS_MIN = 0.06; const float ANISOTROPY_SOFTNESS_MAX = 0.364; const float ANISOTROPY_HOTSPOT_MIN = 0.083; const float ANISOTROPY_HOTSPOT_MAX = 0.637; const float ANISOTROPY_BAND_MIN = 0.42; const float ANISOTROPY_BAND_MIDDLE = 0.5; const float ANISOTROPY_BAND_MAX = 0.58; //Data textures uniform sampler2D light_data : hint_black; uniform sampler2D specular_data : hint_black; uniform sampler2D key_light_ramp : hint_black; uniform sampler2D fill_light_ramp : hint_black; uniform sampler2D kick_light_ramp : hint_black; uniform sampler2D metalness_texture : hint_black_albedo; uniform sampler2D high_frequency_anisotropy_noise : hint_black; uniform sampler2D low_frequency_anisotropy_noise : hint_black; uniform sampler2D spottiness_anisotropy_noise : hint_black; //Outline uniform vec4 outline_color : hint_color = vec4(0, 0, 0, 1.0); uniform float outline_size : hint_range(0, 1) = 0.5; //Metalness uniform vec4 dark_metalness_color : hint_color = vec4(0, 0, 0, 1); uniform vec4 light_metalness_color : hint_color = vec4(1, 1, 1, 1); uniform float metalness_contrast_factor : hint_range(0, 5) = 1.0; uniform float metalness : hint_range(0, 1) = 0.0; //Specular uniform float specular_softness : hint_range(0, 1); uniform float specular_size : hint_range(0, 4); uniform vec4 specular_color : hint_color; //Light colors uniform vec4 key_light_color : hint_color; uniform vec4 shadow_color : hint_color; uniform vec4 fill_light_color : hint_color; uniform vec4 kick_light_color : hint_color; //Anisotropic highlight uniform float anisotropy_specular_width = 10.0; uniform float anisotropy_specular_strength : hint_range(0, 1) = 0.0; uniform float anisotropy_specular_contrast : hint_range(0, 12) = 5.0; uniform float anisotropy_specular_brightness : hint_range(0, 2) = 0.85; uniform float anisotropy_in_shadow_strength : hint_range(0, 1) = 0.1; varying vec3 down_camera_angle; void vertex() { down_camera_angle = (vec4(0, -1, 0, 1) * CAMERA_MATRIX).xyz; } void fragment() { //Data vec3 diffuse = texture(light_data, SCREEN_UV).rgb; //Key light float key_light_value = texture(key_light_ramp, vec2(diffuse.r, 0)).r; vec3 out_color = key_light_value * key_light_color.rgb; out_color = max(out_color, shadow_color.rgb); //Fill light float fill_light_value = texture(fill_light_ramp, vec2(diffuse.g, 0)).r; out_color += fill_light_value * fill_light_color.rgb; //Kick light float kick_light_value = texture(kick_light_ramp, vec2(diffuse.b, 0)).r; out_color += kick_light_value * kick_light_color.rgb; //Metalness vec2 metalness_uv = (NORMAL.xy * vec2(0.5, -0.5) + vec2(0.5, 0.5)); vec3 metalness_value = texture(metalness_texture, metalness_uv).rgb; metalness_value = clamp(pow(metalness_value, vec3(metalness_contrast_factor)), 0, 1); metalness_value = clamp(metalness_value, dark_metalness_color.rgb, light_metalness_color.rgb); out_color = mix(out_color, metalness_value, metalness); //Specular float specular = texture(specular_data, SCREEN_UV).r; float soft_specular = smoothstep(SPECULAR_SOFT_MIN, SPECULAR_SOFT_MAX, specular * specular_size); float hard_specular = smoothstep(SPECULAR_HARD_MIN, SPECULAR_HARD_MAX, specular * specular_size); vec3 specular_out = mix(hard_specular, soft_specular, specular_softness) * specular_color.rgb; out_color += specular_out; //Anisotropic highlight float anisotropy_angle = down_camera_angle.z * 0.33; float high_anisotropy_noise_value = (texture(high_frequency_anisotropy_noise, vec2(UV.x, 0)).r - 0.5) * 0.2; float low_anisotropy_noise_value = (texture(low_frequency_anisotropy_noise, vec2(UV.x, 0)).r - 0.5) * 0.2; float anisotropy_specular_hotspot = smoothstep( ANISOTROPY_HOTSPOT_MIN, ANISOTROPY_HOTSPOT_MAX, specular * anisotropy_specular_width); float spottiness_anisotropy_noise_value = (texture(spottiness_anisotropy_noise, vec2(UV.x, 0)).r - 0.5) * anisotropy_specular_contrast + anisotropy_specular_brightness; float anisotropy_uv = UV.y + high_anisotropy_noise_value + low_anisotropy_noise_value - anisotropy_angle; float lower_sample = smoothstep(ANISOTROPY_BAND_MIN, ANISOTROPY_BAND_MIDDLE, anisotropy_uv); float higher_sample = 1.0 - smoothstep(ANISOTROPY_BAND_MIDDLE, ANISOTROPY_BAND_MAX, anisotropy_uv); float anisotropy_sample = lower_sample * higher_sample * spottiness_anisotropy_noise_value * max(anisotropy_specular_hotspot, anisotropy_in_shadow_strength); float sharp_anisotropy_value = smoothstep(ANISOTROPY_SHARPNESS_MIN, ANISOTROPY_SHARPNESS_MAX, anisotropy_sample); float soft_anisotropy_value = smoothstep(ANISOTROPY_SOFTNESS_MIN, ANISOTROPY_SOFTNESS_MAX, anisotropy_sample); float anisotropy_value = mix(sharp_anisotropy_value, soft_anisotropy_value, specular_softness); vec3 anisotropy_color = specular_color.rgb * anisotropy_value; out_color = out_color + (anisotropy_color * anisotropy_specular_strength); //Outline float outline_factor = outline_size * (1.0 - diffuse.r); float rim_value = pow(dot(NORMAL, VIEW), outline_factor); float outline_amount = smoothstep(OUTLINE_MIN, OUTLINE_MAX, rim_value); out_color = mix(outline_color.rgb, out_color, outline_amount); ALBEDO = vec3(out_color); }