Skip to content

09 Scene

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

Introduction

We now have the components to architect a basic scene. Currently the only adjustable parameters are the number of lights and their intensities (colours), so we start by adding a Material, which encapsulates a renderable object's properties with respect to its interactions with light. A Renderable is thus any "hittable" object plus its material properties. A scene is then a collection of renderables and lights (and the background / "skybox").

Code

Material

For now we just incorporate its albedo (diffuse colour):

material.hpp

struct Material {
  fvec3 albedo{1.0f};
};

Renderable

For Renderable, we shall use external polymorphism (type erasure) to accommodate any type that fits the required constraints - being hittable. Starting with a concept to encapsulate that constraint:

renderable.hpp

template <typename T>
concept Hittable = requires(T const& t, Ray const& ray) {
  Hit{}(ray, t);
};

class Renderable erases an incoming Hittable into its own internal types:

class Renderable {
 private:
  struct Base {
    virtual ~Base() = default;
    virtual bool hit(Hit& out, Ray const& ray) const = 0;
  };

  template <Hittable T>
  struct Model final : Base {
    T t;
    Model(T&& t) : t{std::move(t)} {}
    bool hit(Hit& out, Ray const& ray) const override final { return out(ray, t); }
  };

  std::unique_ptr<Base> m_model{};
};

Its constructor takes any Hittable:

template <Hittable T>
Renderable(T t, Material material = {}) : 
  material{material}, m_model{std::make_unique<Model<T>>(std::move(t))} {}

Its interface offers a Material instance and exposes Base::hit:

bool hit(Hit& out, Ray const& ray) const { return m_model->hit(out, ray); }

Material material{};

Scene

A scene is just a collection of lights, renderables, and the background gradient. With all that data, it can compute the final colour for any incoming ray.

scene.hpp

struct Scene {
  std::vector<DirLight> dir_lights{};
  std::vector<Renderable> renderables{};
  struct {
    fvec3 top{Rgb::from_hex(0x002277).to_f32()};
    fvec3 bottom{Rgb::from_hex(0xffffff).to_f32()};
  } background{};

  fvec3 raycast(Ray const& ray) const;
};

To find the final colour, the scene would need to iterate over all the renderables to find the one closest to the ray (if any, otherwise lerp the background). In order to aid this search, we upgrade the Hit struct to also store the resulting t (which when plugged into ray.at() will yield the same collision point).

hit.hpp

struct Hit {
  fvec3 point{};
  nvec3 normal{};
  float t{};

  bool operator()(Ray const& ray, Sphere const& sphere);
};

hit.cpp

t = smallest_positive_root(roots);
// ...

Back in Scene::colour, we can now find the renderable closest to the ray:

scene.cpp

fvec3 Scene::raycast(Ray const& ray) const {
  struct {
    Renderable const* renderable{};
    Hit hit{};
  } nearest{};
  auto hit = Hit{};
  for (auto const& renderable : renderables) {
    if (renderable.hit(hit, ray) && 
      (!nearest.renderable || hit.t < nearest.hit.t)) { nearest = {&renderable, hit}; }
  }
  // TODO ...
}

If no renderable was hit along the ray, simply return the lerped background colour (feel free to use 1.0 - ray.direction.y and lerp(top, bottom) instead):

if (!nearest.renderable) {
  auto const t = 0.5f * (ray.direction.vec().y() + 1.0f);
  return lerp(background.bottom, background.top, t);
}

Otherwise, combine the light intensities along the hit normal, and multiply it with the material's albedo:

return DirLight::combine(dir_lights, nearest.hit.normal)
   * nearest.renderable->material.albedo;

Multiple spheres

Add a scene and move the sphere and lights into it:

main.cpp

auto scene = Scene{};
scene.renderables.push_back(Sphere{.centre = {0.0f, 0.0f, -5.0f}, .radius = 1.0f});
scene.dir_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}},
};

The image colour is then simply a raycast into the scene:

image[{row, col}] = Rgb::from_f32(clamp(scene.raycast(ray)));

Verify the output, then modify the scene lighting a bit, and add another sphere with a custom material:

scene.renderables.push_back(Sphere{.centre = {0.0f, 0.0f, -5.0f}, .radius = 1.0f});
scene.renderables.push_back({
  Sphere{.centre = {0.5f, -2.0f, -10.0f}, .radius = 5.0f},
  Material{.albedo = {0.2f, 0.8f, 0.7f}},
});
scene.dir_lights = {
  DirLight{.intensity = {1.0f, 1.0f, 1.0f}, .direction = fvec3{-1.0f}},
  DirLight{.intensity = {0.5f, 0.3f, 0.3f}, .direction = fvec3{0.0f, 0.0f, -1.0f}},
};

scene

Housekeeping

hit.cpp

Fixed typo:

constexpr float smallest_positive_root(std::span<float const, 2> roots)
Clone this wiki locally