Understanding Light Vectors
Every light calculation starts with direction. The vector from a surface point to the light source tells your shader everything about the angle of incidence. This single piece of information — combined with the surface normal — determines how bright that pixel should be.
For point lights, you’re calculating this vector every frame for every pixel. That’s computationally expensive if done naively. The trick is normalizing vectors and using dot products efficiently. A dot product between the light vector and surface normal gives you a value between -1 and 1, where 1 means the light hits dead-on and -1 means it’s hitting from behind.
Key insight: The dot product of normalized vectors is your primary lighting calculation. It’s fast, elegant, and works for all light types.
Point Lights vs Directional Lights
A point light sits at a specific location in your scene — think of a lamp, campfire, or explosion. Light rays emanate outward in all directions. Your shader needs to calculate the distance from the surface to the light source and apply attenuation. The farther away, the dimmer it gets. This is why a torch looks bright nearby but doesn’t illuminate the entire level.
Directional lights, by contrast, come from infinitely far away — like the sun. There’s no distance calculation because you assume all light rays are parallel. This makes directional lights computationally cheap. Most scenes use one or two directional lights for global illumination and then layer point lights for local effects. That’s your basic strategy right there.
- Point Light: Position-based, distance matters, attenuation required
- Directional Light: Direction-based, no distance, constant intensity
Attenuation and Distance
Point light brightness falls off with distance. The most common formula is inverse square law — intensity decreases by the square of the distance. If you double the distance, the light becomes one-quarter as bright. This matches real-world physics and looks natural.
In practice, you’ll calculate distance from the surface to the light, divide the light’s intensity by that distance squared, and clamp the result to prevent negative values. Some developers add a small epsilon value to avoid division by zero when surfaces touch the light source directly. Others use a maximum light radius to prevent distant pixels from being affected.
Performance matters here. You might optimize by using simplified attenuation curves or even pre-calculated lookup tables. We’ve seen real game projects use a combination of squared distance and a custom falloff function that feels right to artists even if it’s not physically accurate.
Specular Highlights and Reflections
Diffuse lighting shows you the basic shape of an object. Specular highlights add polish — that bright spot you see on a wet surface or polished metal. It’s created when light reflects directly toward the camera. Your shader calculates the reflection vector, compares it to the camera direction, and if they’re aligned, you get a bright pixel.
The Phong reflection model is a classic approach. You calculate the reflection of the incoming light across the surface normal, then compare it to the view direction using a dot product. Raise the result to a power (called the shininess or specular exponent) to control how sharp or soft the highlight appears. Higher powers create tight, sharp highlights on shiny surfaces. Lower powers spread the highlight across a wider area on rougher materials.
Modern game engines often use physically-based rendering (PBR) instead, which uses roughness and metallic values to control how light interacts. The math is more complex but produces consistent, realistic results across different lighting conditions.
Combining Multiple Lights
Here’s where it gets interesting. You rarely use just one light. Real scenes need depth. You’ll typically have a directional light for the main fill, then layer point lights for local effects. A torch indoors, an explosion nearby, a neon sign reflecting off wet pavement — each is a separate light contribution.
The approach is straightforward: calculate the contribution from each light independently, then add them together. But this gets expensive fast. If you have 32 point lights in your scene and you’re rendering at 1440p, that’s a lot of calculations. Game engines handle this through techniques like deferred rendering, light culling, or clustered forward rendering. You group nearby lights and only process the relevant ones for each pixel. It’s an optimization challenge that separates polished engines from prototypes.