Skip to content

08 Diffuse Lighting

Karn Kaul edited this page Oct 1, 2022 · 1 revision

Introduction

Modelling lighting is tricky and complicated, and largely impossible to do accurately in real-time, but approximations and simplifications can produce convincing results. The simplest kind of emissivity to model is diffuse reflection: light from incident rays is scattered at many angles. Lambert's cosine law states that the radiant intensity observed is directly proportional to the cosine of the angle between the direction of incident light and the surface normal. This implies that smaller angles will reflect more light, as should be intuitive.

Lights can be of various kinds: point light, spotlight, directional light, etc. The simplest one to tackle is directional light, since it only needs a direction to be specified, and has the same intensity in that direction across the entire scene. The "cosine of the angle" is then just the dot product between the negative of the light's direction and the surface/collision normal.

      * [light]
       \
        \
 [ray]   \
========> ( [sphere]
      <---  [normal]

Code

Model a directional light. The resulting intensity is clamped to 0.15 to provide some ambient lighting throughout the object being lit (and to prevent values where the cosine is less than 0 from being "reverse" lit):

light.hpp

struct DirLight {
  fvec3 intensity{1.0f};
  nvec3 direction{};

  fvec3 diffuse(nvec3 const normal) const {
    auto const coeff = dot(normal.vec(), -direction.vec());
    return std::max(coeff, 0.15f) * intensity;
  }
};

Add a cyan dir light to the scene, pointing left-down-forwards

main.cpp

auto const light = DirLight{.intensity = {0.0f, 1.0f, 1.0f}, .direction = fvec3{-1.0f}};

And replace the normal colour with the light's diffuse output:

image[{row, col}] = Rgb::from_f32(light.diffuse(hit.normal));

diffuse_0

Multiple Lights

Adding more lights is quite simple: each one's contribution is summed up:

light.hpp

// static member function of DirLight
static fvec3 combine(std::span<DirLight const> lights, nvec3 const normal) {
  auto ret = fvec3{};
  for (auto const& light : lights) { ret += light.diffuse(normal); }
  return ret;
}

When combining multiple operations on colours, the resulting values can exceed the expected 1.0 maximum. There are a few ways to handle it, for now we shall simply clamp each channel to 1.0:

main.cpp

namespace {
constexpr fvec3 clamp(fvec3 in, float lo = 0.0f, float hi = 1.0f) {
	in.x() = std::clamp(in.x(), lo, hi);
	in.y() = std::clamp(in.y(), lo, hi);
	in.z() = std::clamp(in.z(), lo, hi);
	return in;
}
} // namespace

Changing light to be an array of lights, adding a half-intense red light facing straight ahead:

DirLight const lights[] = {
  DirLight{.intensity = {0.0f, 1.0f, 1.0f}, .direction = fvec3{-1.0f}},
  DirLight{.intensity = {0.5f, 0.0f, 0.0f}, .direction = fvec3{0.0f, 0.0f, -1.0f}},
};

// ...

image[{row, col}] = Rgb::from_f32(clamp(DirLight::combine(lights, hit.normal)));

diffuse_1

Clone this wiki locally