Calculate Three.js HemisphereLight Color on a Mesh

This article explains how to programmatically calculate the exact color contribution of a HemisphereLight on a specific mesh surface in Three.js. You will learn the mathematical formula Three.js uses internally and how to implement a JavaScript function to calculate this value for any given point or face on a 3D mesh.

The Mathematical Formula

A HemisphereLight creates a smooth gradient between a sky color (shining from above) and a ground color (shining from below). The light’s influence depends entirely on the direction of the surface normal relative to the light’s direction vector.

Three.js calculates the light intensity at a vertex or fragment using the following steps:

  1. Dot Product: Calculate the dot product between the surface’s world normal vector (\(N\)) and the normalized light direction vector (\(L\)).
  2. Weight Factor: Map the resulting dot product (which ranges from -1.0 to 1.0) to a 0.0 to 1.0 range using the formula: \[\text{weight} = 0.5 \times (N \cdot L) + 0.5\]
  3. Color Interpolation: Linearly interpolate (lerp) between the ground color and the sky color using the weight factor.
  4. Intensity Scaling: Multiply the interpolated color by the light’s intensity.

Implementation in JavaScript

To calculate this value on a specific mesh surface, you must transform the surface normal into world space and apply the interpolation formula.

Here is the complete function to calculate the light color acting on a specific face normal of a mesh:

import * as THREE from 'three';

/**
 * Calculates the HemisphereLight color acting on a specific mesh surface.
 * @param {THREE.Mesh} mesh - The target mesh.
 * @param {THREE.Vector3} localNormal - The local normal vector of the surface face.
 * @param {THREE.HemisphereLight} light - The HemisphereLight source.
 * @returns {THREE.Color} The calculated light color acting on the surface.
 */
function calculateHemiColorOnSurface(mesh, localNormal, light) {
    // 1. Get the light's direction in world space
    const lightDir = new THREE.Vector3();
    lightDir.copy(light.position).normalize();

    // 2. Transform the local surface normal to world space
    const worldNormal = localNormal.clone();
    const normalMatrix = new THREE.Matrix3().getNormalMatrix(mesh.matrixWorld);
    worldNormal.applyMatrix3(normalMatrix).normalize();

    // 3. Calculate the dot product between the world normal and light direction
    const dot = worldNormal.dot(lightDir);

    // 4. Map the dot product from [-1, 1] to [0, 1]
    const weight = 0.5 * dot + 0.5;

    // 5. Interpolate between groundColor and skyColor
    const finalColor = new THREE.Color();
    finalColor.lerpColors(light.groundColor, light.color, weight);

    // 6. Scale by the light's intensity
    finalColor.multiplyScalar(light.intensity);

    return finalColor;
}

How to Use the Function

To use this function, pass your mesh, the local normal of the face you want to query, and your active HemisphereLight instance:

// Example: Calculate light on the top face of a cube
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial();
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1.5);
scene.add(hemiLight);

// The local normal pointing straight up (top face of the cube)
const topFaceNormal = new THREE.Vector3(0, 1, 0);

// Ensure world matrices are up to date before calculation
mesh.updateMatrixWorld();
hemiLight.updateMatrixWorld();

const lightContribution = calculateHemiColorOnSurface(mesh, topFaceNormal, hemiLight);
console.log(`RGB Contribution:`, lightContribution);