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.