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:
- Dot Product: Calculate the dot product between the surface’s world normal vector (\(N\)) and the normalized light direction vector (\(L\)).
- 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\]
- Color Interpolation: Linearly interpolate (lerp) between the ground color and the sky color using the weight factor.
- 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);