Inside the companion
First Light
Make your first CPU image, rasterize triangles, and trace an allocation budget.
Project files 15
Change the composition
The demo scene and validation of camera, geometry, lighting, and work limits.
#include "prism/scene.hpp"
#include <cmath>
#include <utility>
namespace {
prism::Error invalid(std::string message) {
return {prism::ErrorCode::invalid_scene, std::move(message)};
}
bool finite(double value, double bound = 1000.0) {
return std::isfinite(value) && std::abs(value) <= bound;
}
bool valid_vector(prism::Vec3 v) { return finite(v.x) && finite(v.y) && finite(v.z); }
double length_squared(prism::Vec3 v) { return v.x * v.x + v.y * v.y + v.z * v.z; }
bool valid_color(prism::Color c) {
return std::isfinite(c.r) && std::isfinite(c.g) && std::isfinite(c.b) &&
c.r >= 0 && c.r <= 1 && c.g >= 0 && c.g <= 1 && c.b >= 0 && c.b <= 1;
}
} // namespace
namespace prism {
Scene demo_scene() {
// Typed authoring keeps this graphics starter independent of file formats.
// The returned value owns its title and sphere storage.
return {1, "Parallax · Afternoon study", 960, 600,
{{5.2, 3.0, 7.5}, {0, 0.7, 0}, 38},
{0.76, 0.85, 0.96}, {-3, 6, 4}, 0, {0.82, 0.79, 0.73},
{{{-1.25, 1.05, 0}, 1.05, {0.93, 0.25, 0.10}},
{{1.05, 0.76, 0.25}, 0.76, {0.10, 0.37, 0.74}},
{{0.25, 0.42, 1.75}, 0.42, {0.29, 0.73, 0.59}}}};
}
std::expected<void, Error> validate_scene(const Scene& scene) {
if (scene.version != 1) return std::unexpected(invalid("Unsupported scene version; expected 1."));
if (scene.title.empty() || scene.title.size() > 80)
return std::unexpected(invalid("Title must contain 1–80 bytes."));
for (const char byte : scene.title) {
const auto c = static_cast<unsigned char>(byte);
if (c < 0x20 || c == 0x7f) return std::unexpected(invalid("Title contains a control character."));
}
if (scene.width == 0 || scene.height == 0 || scene.width > max_dimension || scene.height > max_dimension)
return std::unexpected(invalid("Image dimensions must each be in 1–2048."));
// Divide before multiplying: these checks remain correct on 32-bit size_t.
const auto width = static_cast<std::size_t>(scene.width);
const auto height = static_cast<std::size_t>(scene.height);
if (height > max_pixels / width)
return std::unexpected(invalid("Image exceeds the 2097152-pixel budget."));
const auto pixels = width * height;
if (scene.spheres.size() > max_spheres)
return std::unexpected(invalid("Scene exceeds the 32-sphere limit."));
// Shadow rays inspect every sphere too. Bound the primary intersection
// budget here; the renderer performs at most twice this sphere work.
if (!scene.spheres.empty() && pixels > max_intersection_work / scene.spheres.size())
return std::unexpected(invalid("Image and sphere count exceed the intersection budget."));
if (!valid_vector(scene.camera.position) || !valid_vector(scene.camera.look_at))
return std::unexpected(invalid("Camera coordinates must be finite and within ±1000."));
const Vec3 forward{scene.camera.look_at.x - scene.camera.position.x,
scene.camera.look_at.y - scene.camera.position.y,
scene.camera.look_at.z - scene.camera.position.z};
if (length_squared(forward) < 1e-12 || forward.x * forward.x + forward.z * forward.z < 1e-12)
return std::unexpected(invalid("Camera needs a distinct target and cannot point parallel to world up."));
if (!finite(scene.camera.vertical_fov_degrees, 120) || scene.camera.vertical_fov_degrees < 10)
return std::unexpected(invalid("Vertical field of view must be in 10–120 degrees."));
if (!valid_vector(scene.light_direction) || length_squared(scene.light_direction) < 1e-12)
return std::unexpected(invalid("Light direction must be finite, bounded, and nonzero."));
if (!valid_color(scene.background) || !valid_color(scene.ground_color) || !finite(scene.ground_y))
return std::unexpected(invalid("Ground and background values are outside their domains."));
for (const auto& sphere : scene.spheres) {
if (!valid_vector(sphere.center) || !finite(sphere.radius, 100) || sphere.radius < 0.001 || !valid_color(sphere.color))
return std::unexpected(invalid("Sphere needs bounded finite coordinates, radius in 0.001–100, and RGB in 0–1."));
}
return {};
}
} // namespace prism
This is the file included with the course. Copy and download give you its complete source.
Taking your work offline? Download all course companions
