Refractive Procedural Eye

Configurable eye with corneal refraction. Works on any sphere mesh. You can use it out of the box as a toon eye shader or modify it to support more realistic effects.

Shader code
shader_type spatial;

/**
 * Eye forward axis. For the default Godot sphere mesh, Y is optimal
*/
uniform int eye_axis : hint_enum("X", "Y", "Z") = 1;
uniform bool flip = false;
uniform float iris_size : hint_range(0.001, 1.0) = 0.35;
uniform float pupil_dilation : hint_range(0.0, 1.0)  = 0.375;
uniform float cornea_bulge : hint_range(0.0, 0.999) = 0.75;
uniform float cornea_index_of_refraction : hint_range(1.0, 2.0) = 1.376;
uniform float roughness : hint_range(0.0, 1.0) = 0.05;

group_uniforms Colors;
uniform vec3 iris_color_1 : source_color = vec3(0.8, 0.4, 0.0);
uniform vec3 iris_color_2 : source_color = vec3(0.5, 0.2, 0.0);
uniform vec3 pupil_color : source_color = vec3(0.0);
uniform vec3 sclera_color : source_color = vec3(1.0);
group_uniforms;

const float cornea_blend_radius = 0.1;
const float iris_blend_radius = 0.05;
const float iris_edge_radius = 0.05;
const float iris_inset = 0.02;
const float pupil_blend_radius = 0.03;

// --- Vertex ---

// Sphere signed distance and gradient
vec4 sdg_sphere(vec3 p, vec4 sphere) {
	vec3 v = p - sphere.xyz;
	float l = length(v);
	float d = l - sphere.w;
	vec3 n = v / l;
	return vec4(n, d);
}

varying vec3 axis;
varying mat4 inv_modelview_matrix;
varying float sclera_radius;
varying float iris_radius;
varying float cornea_depth;

void vertex() {
	// Set eye forward axis
	axis = vec3(0.0);
	axis[eye_axis] = 1.0;
	axis *= flip ? 1.0 : -1.0;
	
	// Eye measures required by vertex and fragment
	sclera_radius = length(VERTEX);
	float iris_angle = iris_size * PI / 2.0;
	iris_radius = sin(iris_angle) * sclera_radius;
	cornea_depth = -cos(iris_angle) * sclera_radius;
	
	// Cornea bulge
	float cornea_radius = mix(sclera_radius, iris_radius, cornea_bulge);
	vec3 cornea_center = axis * (cornea_depth + sqrt(cornea_radius * cornea_radius - iris_radius * iris_radius));
	vec4 s = sdg_sphere(VERTEX, vec4(cornea_center, cornea_radius));
	if (s.w < 0.0)
		VERTEX += -s.xyz * s.w;
		
	// Simple normal blending
	// Godot normalizes NORMAL automatically so we don't need to do it here
	float normal_blend_radius = cornea_blend_radius * iris_size * sclera_radius;
	NORMAL = mix(NORMAL, s.xyz, smoothstep(0.0, -normal_blend_radius, s.w));
	
	// Inverse MODELVIEW_MATRIX for transforming view ray into local space
	inv_modelview_matrix = inverse(MODELVIEW_MATRIX);
}

// --- Fragment ---

// Ray-plane intersection
float intersect_plane(vec3 o, vec3 n, vec3 ro, vec3 rd) {
	return dot(o - ro, n) / dot(rd, n);
}

// https://godotshaders.com/snippet/polar-coordinates/
vec2 polar_coordinates(vec2 uv, vec2 center, float zoom, float repeat) {
	vec2 dir = uv - center;
	float radius = length(dir);
	float angle = atan(dir.y, dir.x) * 1.0 / TAU;
	return vec2(angle * repeat, radius * zoom);
}

// Filtered cosine
// https://iquilezles.org/articles/bandlimiting/
float fcos(float x) {
	float w = fwidth(x);
	return cos(x) * smoothstep(TAU, 0.0, w);
}

// Basic procedural eye color
// Can be modified or replaced with texture read for different effects
vec4 eye_color(vec2 uv) {
	vec4 color = vec4(1.0);
	float pupil_radius = pupil_dilation;
	
	// Iris pattern
	float s = fcos(uv.x * TAU * 35.0) + fcos(uv.x * TAU * 20.0);
	s = s * 0.25 + 0.5;
	float r = pupil_radius - 0.075;
	float remapped_y = (uv.y - r) / (1.0 - r);
	float fade = smoothstep(1.0, 0.6, remapped_y);
	float pattern = smoothstep(1.0, s, remapped_y) * fade;
	color.rgb = mix(iris_color_1, iris_color_2, clamp(remapped_y, 0.0, 1.0));
	color.rgb = mix(color.rgb * 0.25, color.rgb, pattern);
	
	// Pupil
	float pupil_mask = smoothstep(pupil_radius, pupil_radius - pupil_blend_radius, uv.y);
	color.rgb = mix(color.rgb, pupil_color, pupil_mask);
	
	return color;
}

void fragment() {
	// View ray in local space
	vec3 local_vertex = (inv_modelview_matrix * vec4(VERTEX, 1.0)).xyz;
	vec3 ray_origin = local_vertex;
	vec3 ray_direction = (inv_modelview_matrix * vec4(-VIEW, 0.0)).xyz;
	
	// Refract view ray through cornea
	vec3 local_normal = (inv_modelview_matrix * vec4(NORMAL, 0.0)).xyz;
	ray_direction = refract(ray_direction, local_normal, 1.0 / cornea_index_of_refraction);
	
	ray_direction = normalize(ray_direction);
	
	// View ray intersection with iris plane
	vec3 iris_normal = axis;
	vec3 iris_origin = iris_normal * (cornea_depth + iris_inset);
	float t = intersect_plane(iris_origin, iris_normal, ray_origin, ray_direction);
	vec3 iris_vertex = ray_origin + ray_direction * t;
	
	// Set iris UV depending on axis
	vec2 iris_uv;
	if (eye_axis == 0) 
		iris_uv = iris_vertex.zy;
	else if (eye_axis == 1) 
		iris_uv = iris_vertex.xz;
	else 
		iris_uv = iris_vertex.xy;
	
	// Apply eye color
	// UVs are adjusted to avoid filtering artifacts
	// https://bgolus.medium.com/rendering-a-sphere-on-a-quad-13c92025570c#8789
	iris_uv = polar_coordinates(iris_uv, vec2(0.0, 0.0), 1.0, 1.0);
	iris_uv.y /= iris_radius;
	vec2 fract_iris_uv = vec2(fract(iris_uv.x), iris_uv.y);
	float w0 = fwidth(iris_uv.x);
	float w1 = fwidth(fract_iris_uv.x);
	ALBEDO = w0 < w1 ? eye_color(iris_uv).rgb : eye_color(fract_iris_uv).rgb;
	
	// Mask eye color with sclera
	float sclera_blend_radius = cornea_blend_radius * iris_size * sclera_radius;
	float sclera_mask = smoothstep(cornea_depth + sclera_blend_radius, cornea_depth, (float(flip) * 2.0 - 1.0) * local_vertex[eye_axis]);
	ALBEDO = mix(sclera_color, ALBEDO, sclera_mask);
	
	// Surface properties
	ROUGHNESS = roughness;
}
Live Preview
Tags
Eye, optics, Procedural
The shader code and all code snippets in this post are under MIT license and can be used freely. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from tentabrobpy

Related shaders

guest

1 Comment
Oldest
Newest Most Voted
______
______
13 hours ago

Probably the coolest shader on this website