Cartoon Animated Eyes
Background: I needed a shader for some cartoon eyes so with some AI help I made this. Note: I’m a designer not a shader guru. Has blinking animation and some basic poses which you can change as needed. I’m sure you could build this as images/clipping masks, but I wanted something easy to adjust on the fly.
To use: Add the shader as a material applied to a mesh instance. Adjust the parameters as you like. If you find any bugs or improvements comment and tell us!
Version used: Godot V4.4.
If you want more anime style you can check out: https://www.reddit.com/r/godot/comments/17mepza/heres_a_shader_i_made_for_controlling_eyes/ . This one also has options for irises which you may want.
If you use it for something let me know, I’d love to see what you did with it!
Shader code
shader_type spatial;
render_mode cull_disabled, depth_draw_opaque;
group_uniforms Eye_Shader_Properties;
// Look direction (base values when not animated)
uniform float look_right : hint_range(-0.5, 1.5, 0.01) = 0.5;
uniform float look_up : hint_range(0.0, 1.0, 0.01) = 0.5;
// Basic eye settings
uniform float eye_separation : hint_range(0.1, 0.8, 0.01) = 0.41;
uniform float pupil_size : hint_range(0.01, 0.1, 0.01) = 0.04;
uniform float sclera_size : hint_range(0.1, 0.3, 0.01) = 0.3;
// Eyelid shape controls
uniform float eyelid_top : hint_range(0.0, 1.0, 0.01) = 0.3;
uniform float eyelid_bottom : hint_range(0.0, 1.0, 0.01) = 0.3;
uniform float top_eyelid_curve : hint_range(-100.0, 100.0, 0.01) = 50.0;
uniform float bottom_eyelid_curve : hint_range(-100.0, 100.0, 0.01) = 50.0;
uniform float curve_steepness : hint_range(0.01, 0.5) = 0.05;
uniform float eyelid_tilt : hint_range(-0.5, 0.5, 0.01) = 0.0;
uniform float eye_width : hint_range(0.5, 2.0, 0.01) = 1.0;
// Colors
uniform vec3 pupil_color : source_color = vec3(0.0, 0.0, 0.0);
uniform vec3 sclera_color : source_color = vec3(0.3, 0.6, 1.0);
// Eyelids
uniform bool show_eyelids = true;
uniform vec3 eyelid_color : source_color = vec3(1.0, 0.5, 0.5);
// Circular Warp
uniform bool circular_warp = true;
uniform float circular_warp_amount : hint_range(0.1, 5.0) = 1.0;
group_uniforms Animation_Settings;
// Master animation control
uniform bool enable_animation = true;
uniform float animation_speed : hint_range(0.1, 5.0) = 0.5;
// Blinking animation
uniform bool enable_blinking = true;
uniform float blink_frequency : hint_range(0.1, 10.0) = 0.8;
uniform float blink_speed : hint_range(0.1, 10.0) = 5.0;
uniform float blink_closure_amount : hint_range(0.0, 1.0) = 0.6;
// Idle eye movement
uniform bool enable_idle_movement = true;
uniform float idle_movement_speed : hint_range(0.1, 3.0) = 1.0;
uniform float idle_movement_intensity : hint_range(0.0, 1.0) = 0.5;
uniform float idle_pause_duration : hint_range(0.5, 5.0) = 0.5;
group_uniforms Pose_System;
// Pose selection and transition
uniform int current_pose : hint_enum("Neutral", "Alert", "Suspicious", "Sleepy", "Angry", "Happy", "Surprised") = 0;
uniform int transition_from_pose : hint_enum("Neutral", "Alert", "Suspicious", "Sleepy", "Angry", "Happy", "Surprised") = 0;
uniform float pose_transition_progress : hint_range(0.0, 1.0) = 1.0;
uniform float pose_transition_speed : hint_range(0.1, 10.0) = 2.0;
// Pose override - when enabled, ignores current_pose and uses manual values
uniform bool manual_pose_override = false;
group_uniforms;
// Pose data structure
struct EyePose {
float look_right;
float look_up;
float eyelid_top;
float eyelid_bottom;
float pupil_size_multiplier;
float eyelid_tilt;
};
// Get pose data based on pose index
EyePose get_pose_data(int pose_index) {
EyePose pose;
if (pose_index == 0) {
// Neutral
pose.look_right = 0.5;
pose.look_up = 0.5;
pose.eyelid_top = 0.3;
pose.eyelid_bottom = 0.3;
pose.pupil_size_multiplier = 1.0;
pose.eyelid_tilt = 0.0;
} else if (pose_index == 1) {
// Alert
pose.look_right = 0.5;
pose.look_up = 0.4;
pose.eyelid_top = 0.15;
pose.eyelid_bottom = 0.15;
pose.pupil_size_multiplier = 1.2;
pose.eyelid_tilt = 0.0;
} else if (pose_index == 2) {
// Suspicious
pose.look_right = 0.3;
pose.look_up = 0.45;
pose.eyelid_top = 0.6;
pose.eyelid_bottom = 0.4;
pose.pupil_size_multiplier = 0.8;
pose.eyelid_tilt = -0.2;
} else if (pose_index == 3) {
// Sleepy
pose.look_right = 0.5;
pose.look_up = 0.6;
pose.eyelid_top = 0.7;
pose.eyelid_bottom = 0.5;
pose.pupil_size_multiplier = 0.9;
pose.eyelid_tilt = 0.0;
} else if (pose_index == 4) {
// Angry
pose.look_right = 0.5;
pose.look_up = 0.4;
pose.eyelid_top = 0.5;
pose.eyelid_bottom = 0.2;
pose.pupil_size_multiplier = 0.7;
pose.eyelid_tilt = 0.15;
} else if (pose_index == 5) {
// Happy
pose.look_right = 0.5;
pose.look_up = 0.45;
pose.eyelid_top = 0.25;
pose.eyelid_bottom = 0.6;
pose.pupil_size_multiplier = 1.1;
pose.eyelid_tilt = 0.0;
} else if (pose_index == 6) {
// Surprised
pose.look_right = 0.5;
pose.look_up = 0.3;
pose.eyelid_top = 0.0;
pose.eyelid_bottom = 0.0;
pose.pupil_size_multiplier = 1.5;
pose.eyelid_tilt = 0.0;
} else {
// Fallback to neutral
pose.look_right = 0.5;
pose.look_up = 0.5;
pose.eyelid_top = 0.3;
pose.eyelid_bottom = 0.3;
pose.pupil_size_multiplier = 1.0;
pose.eyelid_tilt = 0.0;
}
return pose;
}
// Smooth interpolation between poses
EyePose interpolate_poses(EyePose from_pose, EyePose to_pose, float t) {
EyePose result;
float smooth_t = smoothstep(0.0, 1.0, t);
result.look_right = mix(from_pose.look_right, to_pose.look_right, smooth_t);
result.look_up = mix(from_pose.look_up, to_pose.look_up, smooth_t);
result.eyelid_top = mix(from_pose.eyelid_top, to_pose.eyelid_top, smooth_t);
result.eyelid_bottom = mix(from_pose.eyelid_bottom, to_pose.eyelid_bottom, smooth_t);
result.pupil_size_multiplier = mix(from_pose.pupil_size_multiplier, to_pose.pupil_size_multiplier, smooth_t);
result.eyelid_tilt = mix(from_pose.eyelid_tilt, to_pose.eyelid_tilt, smooth_t);
return result;
}
// Pose transition system that remembers previous pose
EyePose get_current_animated_pose(float time) {
if (manual_pose_override) {
// Use manual uniform values
EyePose manual_pose;
manual_pose.look_right = look_right;
manual_pose.look_up = look_up;
manual_pose.eyelid_top = eyelid_top;
manual_pose.eyelid_bottom = eyelid_bottom;
manual_pose.pupil_size_multiplier = 1.0;
manual_pose.eyelid_tilt = eyelid_tilt;
return manual_pose;
}
// Get poses for transition
EyePose from_pose = get_pose_data(transition_from_pose);
EyePose to_pose = get_pose_data(current_pose);
// Use the transition progress to blend between poses
float transition_t = clamp(pose_transition_progress, 0.0, 1.0);
// Interpolate between the from and to poses
return interpolate_poses(from_pose, to_pose, transition_t);
}
// Blinking animation
float get_blink_factor(float time) {
if (!enable_blinking) return 1.0;
float blink_cycle = time * blink_frequency;
float blink_phase = mod(blink_cycle, 1.0);
// Make blink shorter and snappier
float blink_duration = 0.08; // Shorter blink duration (was 0.15)
if (blink_phase < blink_duration) {
// We're in a blink
float blink_progress = blink_phase / blink_duration;
// Create faster blink curve - close and open quickly
float blink_curve = sin(blink_progress * 3.14159);
// Apply blink speed more aggressively
float speed_factor = clamp(blink_speed, 0.5, 10.0);
blink_curve = pow(blink_curve, 2.0 / speed_factor);
return 1.0 - blink_curve; // 1 = open, 0 = closed
}
return 1.0; // Eyes open
}
// Idle eye movement - neutral -> random direction -> pause -> back to neutral -> repeat
vec2 get_idle_eye_movement(float time) {
if (!enable_idle_movement) return vec2(0.0);
float idle_time = time * idle_movement_speed;
// Full cycle: move out (0.3s) -> pause (adjustable) -> move back (0.3s) -> brief pause (0.2s)
float move_out_duration = 0.3;
float pause_duration = idle_pause_duration;
float move_back_duration = 0.3;
float neutral_pause = 0.2;
float full_cycle_time = move_out_duration + pause_duration + move_back_duration + neutral_pause;
float phase = mod(idle_time, full_cycle_time);
float cycle_number = floor(idle_time / full_cycle_time);
// Generate random target position for this cycle
float random_seed = cycle_number * 12.9898;
float random_factor = fract(sin(random_seed) * 43758.5453);
float target_position = (random_factor - 0.5) * 2.0 * idle_movement_intensity; // -intensity to +intensity
vec2 idle_offset = vec2(0.0);
if (phase < move_out_duration) {
// Phase 1: Move from neutral to target position
float progress = phase / move_out_duration;
float smooth_progress = smoothstep(0.0, 1.0, progress);
idle_offset.x = mix(0.0, target_position, smooth_progress);
} else if (phase < move_out_duration + pause_duration) {
// Phase 2: Pause at target position
idle_offset.x = target_position;
} else if (phase < move_out_duration + pause_duration + move_back_duration) {
// Phase 3: Move back from target to neutral
float return_phase = phase - (move_out_duration + pause_duration);
float progress = return_phase / move_back_duration;
float smooth_progress = smoothstep(0.0, 1.0, progress);
idle_offset.x = mix(target_position, 0.0, smooth_progress);
} else {
// Phase 4: Brief pause at neutral before next cycle
idle_offset.x = 0.0;
}
// No vertical movement
idle_offset.y = 0.0;
return idle_offset;
}
vec2 rotate(vec2 uv, vec2 pivot, float angle) {
mat2 rotation = mat2(vec2(sin(angle), -cos(angle)), vec2(cos(angle), sin(angle)));
uv -= pivot;
uv = uv * rotation;
uv += pivot;
return uv;
}
vec4 generate_eye(vec2 uv, vec2 eye_center_base, vec2 look_direction, float animated_pupil_size) {
vec2 eye_center = eye_center_base;
// Separate pupil center - this moves with look direction
vec2 pupil_center = eye_center_base;
pupil_center.x += (look_direction.x - 0.5) * 0.2;
pupil_center.y += (look_direction.y - 0.5) * 0.2;
// Distances from fixed eye center and moving pupil center
float eye_dist = distance(uv, eye_center);
float pupil_dist = distance(uv, pupil_center);
// Start transparent
vec4 eye_color = vec4(0.0, 0.0, 0.0, 0.0);
// Add circular iris - stays centered
if (eye_dist < sclera_size) {
eye_color = vec4(sclera_color, 1.0);
}
// Add circular pupil - moves with look direction
if (pupil_dist < animated_pupil_size) {
eye_color = vec4(pupil_color, 1.0);
}
return eye_color;
}
float generate_eyelid_mask(vec2 uv, vec2 eye_center, float animated_eyelid_top, float animated_eyelid_bottom, float animated_eyelid_tilt, float blink_factor) {
// Transform UV relative to eye center
vec2 local_uv = (uv - eye_center);
// Apply tilt rotation
local_uv = rotate(local_uv, vec2(0.0), animated_eyelid_tilt);
float x_pos = local_uv.y; // Horizontal position (-0.5 to 0.5)
float y_pos = local_uv.x; // Vertical position (-0.5 to 0.5)
// Apply blink factor to eyelid positions with adjustable closure amount
float blink_top_closure = (1.0 - blink_factor) * blink_closure_amount * 0.5; // Top eyelid moves down
float blink_bottom_closure = (1.0 - blink_factor) * blink_closure_amount * 0.5; // Bottom eyelid moves up
// Separate the base eyelid position from blink movement
float effective_top = animated_eyelid_top + blink_top_closure;
float effective_bottom = animated_eyelid_bottom + blink_bottom_closure;
// Base position + curve based on horizontal position
float top_curve = top_eyelid_curve * x_pos * x_pos;
float top_boundary = 0.2 - (effective_top * 0.4) - top_curve * curve_steepness;
// Base position + curve based on horizontal position
float bottom_curve = bottom_eyelid_curve * x_pos * x_pos;
float bottom_boundary = -0.2 + (effective_bottom * 0.4) + bottom_curve * curve_steepness;
// Check if pixel is clipped by eyelids
bool clipped_by_top = y_pos > top_boundary;
bool clipped_by_bottom = y_pos < bottom_boundary;
// Horizontal bounds for eye shape (not affected by blinking)
float max_x_width = 0.25 * eye_width;
bool outside_horizontal = abs(x_pos) > max_x_width;
// Return mask value
if (clipped_by_top || clipped_by_bottom || outside_horizontal) {
return 0.0; // Covered by eyelid
}
return 1.0; // Visible eye area
}
void fragment() {
vec2 uv = UV;
// Get animation time
float anim_time = TIME * animation_speed;
// Get current pose
EyePose current_animated_pose = get_current_animated_pose(anim_time);
// Get animation effects
float blink_factor = get_blink_factor(anim_time);
vec2 idle_movement = get_idle_eye_movement(anim_time);
// Apply idle movement to look direction
vec2 final_look_direction = vec2(current_animated_pose.look_right, current_animated_pose.look_up) + idle_movement;
// Calculate animated pupil size
float animated_pupil_size = pupil_size * current_animated_pose.pupil_size_multiplier;
// Calculate eye positions
float half_separation = eye_separation * 0.5;
vec2 left_eye_center = vec2(0.5 - half_separation, 0.5);
vec2 right_eye_center = vec2(0.5 + half_separation, 0.5);
// Generate both circular eyes
vec4 left_eye = generate_eye(uv, left_eye_center, final_look_direction, animated_pupil_size);
vec4 right_eye = generate_eye(uv, right_eye_center, final_look_direction, animated_pupil_size);
// Combine eyes
vec4 combined_eyes = max(left_eye, right_eye);
// Generate eyelid masks with current pose and blink animation
float left_eyelid_mask = generate_eyelid_mask(uv, left_eye_center,
current_animated_pose.eyelid_top, current_animated_pose.eyelid_bottom,
current_animated_pose.eyelid_tilt, blink_factor);
float right_eyelid_mask = generate_eyelid_mask(uv, right_eye_center,
current_animated_pose.eyelid_top, current_animated_pose.eyelid_bottom,
current_animated_pose.eyelid_tilt, blink_factor);
float total_eyelid_mask = max(left_eyelid_mask, right_eyelid_mask);
if (show_eyelids) {
// DEBUG MODE: Show eyelids as colored areas
if (total_eyelid_mask < 0.5) {
// Eyelid areas in pink
ALBEDO = eyelid_color;
ALPHA = 1.0;
} else {
// Eye areas
ALBEDO = combined_eyes.rgb;
ALPHA = combined_eyes.a;
}
} else {
// NORMAL MODE: Apply eyelid masking
combined_eyes.a *= total_eyelid_mask;
ALBEDO = combined_eyes.rgb;
ALPHA = combined_eyes.a;
}
}
void vertex() {
if (circular_warp) {
// === SPHERE DISTORTION IN VERTEX SHADER ===
float distance = VERTEX.x; // How far along to bend
// Calculate bend angle
float angle = distance * circular_warp_amount;
// Apply cylindrical bending
VERTEX.x = sin(angle) * (1.0 / circular_warp_amount);
VERTEX.z = -cos(angle) * (1.0 / circular_warp_amount) + (1.0 / circular_warp_amount);
}
}


This is fricking cute hahaha, nice job pal