Extract Camera View Vector for Three.js Rim Lighting

This article explains how to extract the camera viewing direction vector in a Three.js custom shader to create a rim lighting (Fresnel) effect. You will learn how to leverage Three.js’s built-in matrices to calculate the view direction in view space, write the vertex and fragment shader GLSL code, and implement the final glowing rim effect on a 3D mesh.

To implement rim lighting, you need to calculate the angle between the surface normal of your 3D model and the direction pointing toward the camera (the viewing direction). When these two vectors are perpendicular (at the edges of the object from the camera’s perspective), the rim light is at its brightest.

While you can pass the camera’s world position using uniforms, the most efficient way to achieve this in Three.js is to perform the calculations in view space (camera space). In view space, the camera is always positioned at the origin (0, 0, 0).

1. The Vertex Shader

In the vertex shader, you transform both the vertex normals and the vertex positions into view space. Because the camera is at (0, 0, 0) in view space, the vector from the vertex to the camera is simply the negative of the vertex’s view-space position.

varying vec3 vNormal;
varying vec3 vViewPosition;

void main() {
    // Transform normal to view space and pass to fragment shader
    vNormal = normalize(normalMatrix * normal);

    // Transform position to view space
    vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
    
    // The vector from the surface point to the camera
    vViewPosition = -mvPosition.xyz;

    gl_Position = projectionMatrix * mvPosition;
}

2. The Fragment Shader

In the fragment shader, you normalize the interpolated vectors and calculate the dot product between the surface normal and the view direction vector.

A dot product of 1.0 means the surface is directly facing the camera, while 0.0 means the surface is perpendicular to the camera (the edges). By subtracting this result from 1.0, you isolate the edges of the geometry.

varying vec3 vNormal;
varying vec3 vViewPosition;

uniform vec3 uRimColor;
uniform float uRimPower;

void main() {
    // Re-normalize the interpolated vectors
    vec3 normal = normalize(vNormal);
    vec3 viewDir = normalize(vViewPosition);

    // Calculate the rim lighting intensity (Fresnel effect)
    float rim = 1.0 - max(dot(normal, viewDir), 0.0);

    // Raise to a power to sharpen the edge glow
    rim = pow(rim, uRimPower);

    // Output the final glowing color
    gl_FragColor = vec4(uRimColor * rim, 1.0);
}

3. Setting Up the Three.js Material

To use these shaders, instantiate a THREE.ShaderMaterial and declare the uniforms for the rim color and the power (which controls the thickness and sharpness of the glow).

const rimMaterial = new THREE.ShaderMaterial({
    vertexShader: vertexShaderSource,
    fragmentShader: fragmentShaderSource,
    uniforms: {
        uRimColor: { value: new THREE.Color(0x00ffcc) },
        uRimPower: { value: 3.0 } // Higher values make the rim thinner
    },
    transparent: true,
    blending: THREE.AdditiveBlending
});

Using view space eliminates the need to manually update a cameraPosition uniform in your JavaScript render loop, as Three.js automatically manages the modelViewMatrix and normalMatrix for you.