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);
    }
}
Tags
godot 4 cartoon animated eyes
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from popcornstudios

Related shaders

guest

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
tqef
4 months ago

This is fricking cute hahaha, nice job pal