Anime Style Eyes (Procedural Eye Shader)
This is a quite simple, but IMHO nice, procedural anime style eye shader. Creating an eye this way instead of using a simple texture has some advantages. So I wanted to explore these advantages. My shader has a specuar offset with different view angles, a very simple specular jitter animation, can track a target using a very simple script (for more details, look at the demo project), and I can play with the pupil size for dramatic effects.
For more details, please take a look to the sample/demo project on my github and/or watch my youtube video for a detailed explanation of this shader (its in spanish).
As usual, hope you’ll like it 😋
Shader code
shader_type spatial;
render_mode ambient_light_disabled, specular_disabled; // unshaded;
// this value should be controled with your script
uniform vec3 g_eye_direction = vec3(0.0, 0.0, 1.0); // in world coordinates
uniform vec2 g_eye_movement_limits = vec2(0.2, 0.2); // in UV space
uniform vec2 g_eye_scale = vec2(1.2, 1.0);
uniform vec3 g_iris_shadow = vec3(0.0, 0.25, 1.5); // xy = position offset, z = scale
uniform float g_outline_radius : hint_range(0.0, 1.0, 0.001) = 0.01;
uniform float g_radius = 0.2;
uniform float g_pupil_radius = 0.5; // relative size
uniform float g_specular_outline_radius : hint_range(0.0, 1.0, 0.001) = 0.01;
uniform vec2 g_specular_view_offset_mult = vec2(0.05, 0.025);
uniform vec3 g_specular_a = vec3(0.35, 0.35, 0.3); // xy = position, z = size (all relative to the eye
uniform vec3 g_specular_b = vec3(-0.35, -0.35, 0.15); // xy = position, z = size (all relative to the eye
uniform vec2 g_specular_jitter = vec2(0.05, 25.0); // size and speed
uniform vec3 g_outline_color : source_color = vec3(0.0, 0.0, 0.0);
uniform vec3 g_pupil_color : source_color = vec3(0.0, 0.0, 0.0);
uniform vec3 g_iris_color_a : source_color = vec3(0.3, 1.0, 0.3);
uniform vec3 g_iris_color_b : source_color = vec3(0.5, 0.5, 1.0);
uniform vec3 g_iris_shadow_color : source_color = vec3(0.7, 0.7, 0.7); // shadow
uniform vec3 g_specular_color : source_color = vec3(0.95, 0.975, 1.0);
uniform vec3 g_background_color : source_color = vec3(1.0, 1.0, 1.0);
// returns 0 if position is inside the circle, otherwise returns 1
float circle(vec2 position, float radius)
{
return step(radius, length(position - vec2(0.5)));
}
// fills a circle
vec3 fill_circle(vec2 position, vec3 outside_color, vec3 inside_color, float radius)
{
float c = circle(position, radius);
return mix(inside_color, outside_color, c);
}
// strokes a circle
vec3 stroke_circle(vec2 position, vec3 outside_color, vec3 inside_color, float circle_radius,
float stroke_size)
{
float outside = circle(position, circle_radius);
float inside = circle(position, circle_radius - stroke_size);
return mix(outside_color, inside_color, inside - outside);
}
// fills a circle but also cuts it with anothed circle mask
vec3 fill_masked_circle(
vec2 mask_position, float mask_radius, vec3 mask_color,
vec2 position, float radius, vec3 outside_color, vec3 inside_color)
{
vec3 in_out_color = fill_circle(position, outside_color, inside_color, radius);
return fill_circle(mask_position, mask_color, in_out_color, mask_radius);
}
// draws a circle with stroke and fill colors
vec3 draw_circle(vec2 position, vec3 outside_color, vec3 stroke_color, vec3 fill_color,
float circle_radius, float stroke_size)
{
float outside_outline_val = circle(position, circle_radius);
vec3 outside_outline_color = mix(stroke_color, outside_color, outside_outline_val);
float inside_color_val = circle(position, circle_radius - stroke_size);
return mix(fill_color, outside_outline_color, inside_color_val);
}
void fragment() {
// this is for the specular jitter
float spc_jitter = 1.0 + (g_specular_jitter.x * sin(TIME * g_specular_jitter.y));
// compute the specular dynamic offset
vec2 specular_offset = NORMAL.xy * g_specular_view_offset_mult;
// compute local eye direction
vec3 local_eye_direction = mat3(inverse(MODEL_MATRIX)) * normalize(g_eye_direction);
// limit eye movement
vec2 eye_position = (local_eye_direction.xy * g_eye_movement_limits);
// convert from local space to uv space
eye_position.y = -eye_position.y;
eye_position += 0.5;
// compute final uv with horizontal and vertical scaling
vec2 uv = ((UV - eye_position) * g_eye_scale) + vec2(0.5);
// specular dots must be 100% circles and not ovals
vec2 uv_specular = ((UV - eye_position) * g_eye_scale.xx) + vec2(0.5);
// compute iris color (gradient)
vec3 iris_color = mix(g_iris_color_a, g_iris_color_b, smoothstep(-g_radius, g_radius, uv.y - 0.5));
// set initial color
vec3 current_color = g_background_color;
// draw iris + shadow
current_color = fill_masked_circle(uv, g_radius, current_color, uv - g_iris_shadow.xy, g_radius * g_iris_shadow.z, iris_color * g_iris_shadow_color, iris_color);
// draw pupil
current_color = fill_circle(uv, current_color, g_pupil_color, g_radius * g_pupil_radius);
// draw specular dots
current_color = draw_circle(uv_specular + specular_offset + g_specular_a.xy * g_radius, current_color, g_outline_color, g_specular_color, g_specular_a.z * g_radius * spc_jitter, g_specular_outline_radius * g_radius);
current_color = draw_circle(uv_specular + specular_offset + g_specular_b.xy * g_radius, current_color, g_outline_color, g_specular_color, g_specular_b.z * g_radius * spc_jitter, g_specular_outline_radius * g_radius);
// draw outline
current_color = stroke_circle(uv, current_color, g_outline_color, g_radius, g_outline_radius);
// draw outside (to avoid specular dots outside the eye)
current_color = fill_circle(uv, g_background_color, current_color, g_radius);
// set final color
ALBEDO = current_color;
}

