Fragment Shaders: Per-Pixel Lighting in GLSL

446 views 0 replies
Live Shader
Loading versions...

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.

Live Demo: Phong Lighting on a Sphere

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);
}

Phong Lighting Model

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);
}

Why Blinn-Phong Over Phong?

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.

Adding Normal Mapping

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.

Moonjump
Forum Search Shader Sandbox
Sign In Register