Fragment shaders (also called pixel shaders) run once per fragment and determine the final color output. This is where lighting, texturing, and most visual effects happen.
This standalone shader ray-traces a sphere and applies Blinn-Phong lighting with an animated light source. Watch how the specular highlight moves as the light orbits:
precision mediump float;
uniform vec2 iResolution;
uniform float iTime;
void main() {
vec2 uv = gl_FragCoord.xy / iResolution.xy;
vec3 ro = vec3(0.0, 0.0, -3.0);
vec3 rd = normalize(vec3((uv - 0.5) * vec2(iResolution.x/iResolution.y, 1.0), 1.0));
// Animated sphere
vec3 spherePos = vec3(sin(iTime * 0.7) * 0.5, cos(iTime * 0.5) * 0.3, 0.0);
float sphereR = 1.0;
vec3 oc = ro - spherePos;
float b = dot(oc, rd);
float c = dot(oc, oc) - sphereR * sphereR;
float h = b * b - c;
vec3 col = vec3(0.02, 0.02, 0.05); // background
if (h > 0.0) {
float t = -b - sqrt(h);
vec3 pos = ro + t * rd;
vec3 nor = normalize(pos - spherePos);
// Animated light
vec3 lightPos = vec3(2.0 * sin(iTime), 2.0, -2.0 + cos(iTime));
vec3 lightDir = normalize(lightPos - pos);
vec3 viewDir = normalize(ro - pos);
vec3 halfDir = normalize(lightDir + viewDir);
// Phong lighting
float ambient = 0.12;
float diff = max(dot(nor, lightDir), 0.0);
float spec = pow(max(dot(nor, halfDir), 0.0), 64.0);
vec3 albedo = vec3(0.2, 0.5, 1.0);
col = albedo * (ambient + diff * 0.8) + vec3(1.0) * spec * 0.6;
}
col = pow(col, vec3(0.4545)); // gamma correction
gl_FragColor = vec4(col, 1.0);
}
The classic Phong model combines three components: ambient, diffuse, and specular lighting. Each is calculated per-fragment using the interpolated normal and position from the vertex shader.
#version 330 core
in vec3 FragPos;
in vec3 FragNormal;
in vec2 TexCoord;
uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;
uniform sampler2D diffuseMap;
out vec4 FragColor;
void main()
{
vec3 albedo = texture(diffuseMap, TexCoord).rgb;
vec3 normal = normalize(FragNormal);
// Ambient
float ambientStrength = 0.15;
vec3 ambient = ambientStrength * lightColor * albedo;
// Diffuse
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuse = diff * lightColor * albedo;
// Specular (Blinn-Phong)
vec3 viewDir = normalize(viewPos - FragPos);
vec3 halfDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(normal, halfDir), 0.0), 64.0);
vec3 specular = spec * lightColor * 0.5;
vec3 result = ambient + diffuse + specular;
FragColor = vec4(result, 1.0);
}
The original Phong model uses the reflection vector, which is more expensive to compute and can produce artifacts at grazing angles. Blinn-Phong uses the half-vector between light and view directions — it is cheaper and often looks better for game rendering.
For extra surface detail without adding geometry, you can sample normals from a texture and transform them into world space using a TBN matrix:
// In vertex shader, compute TBN matrix vec3 T = normalize(normalMatrix * aTangent); vec3 N = normalize(normalMatrix * aNormal); vec3 B = cross(N, T); mat3 TBN = mat3(T, B, N); // In fragment shader, sample and transform normal vec3 normalMap = texture(normalTex, TexCoord).rgb * 2.0 - 1.0; vec3 normal = normalize(TBN * normalMap);
This gives the illusion of bumps, scratches, and fine detail while keeping polygon counts low — essential for real-time game rendering.