-
Notifications
You must be signed in to change notification settings - Fork 0
09 Scene
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").
For now we just incorporate its albedo (diffuse colour):
material.hpp
struct Material {
fvec3 albedo{1.0f};
};
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{};
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;
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}},
};
hit.cpp
Fixed typo:
constexpr float smallest_positive_root(std::span<float const, 2> roots)