Inside the companion
First Light
Make your first CPU image, rasterize triangles, and trace an allocation budget.
Project files 15
Cover the pixel grid
Triangle coverage, interpolated color and depth, and the demo raster scene.
#include "prism/raster_lab.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <new>
namespace {
using prism::ScreenVertex;
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;
}
double edge(const ScreenVertex& a, const ScreenVertex& b, double x, double y) noexcept {
return (b.x - a.x) * (y - a.y) - (b.y - a.y) * (x - a.x);
}
std::uint8_t encode(double linear) {
return static_cast<std::uint8_t>(std::lround(std::sqrt(std::clamp(linear, 0.0, 1.0)) * 255.0));
}
prism::Error invalid(const char* message) { return {prism::ErrorCode::invalid_scene, message}; }
} // namespace
namespace prism {
RasterScene demo_raster_scene() {
// Three overlapping projected triangles. The front triangle has varying
// vertex colors and depth; submission order is deliberately not depth order.
return {960, 600, {0.035, 0.055, 0.10}, {
{{{{160, 100, 0.7, {0.10, 0.38, 0.85}},
{810, 170, 0.7, {0.10, 0.38, 0.85}},
{670, 530, 0.7, {0.10, 0.38, 0.85}}}}},
{{{{220, 470, 0.25, {0.98, 0.21, 0.10}},
{470, 85, 0.40, {0.98, 0.70, 0.18}},
{780, 440, 0.20, {0.12, 0.78, 0.59}}}}},
{{{{95, 390, 0.9, {0.16, 0.48, 0.46}},
{400, 200, 0.9, {0.16, 0.48, 0.46}},
{360, 545, 0.9, {0.16, 0.48, 0.46}}}}}
}};
}
std::expected<void, Error> validate_raster_scene(const RasterScene& scene) {
if (scene.width == 0 || scene.height == 0 || scene.width > max_dimension || scene.height > max_dimension)
return std::unexpected(invalid("Raster dimensions must each be in 1–2048."));
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("Raster exceeds the pixel budget."));
if (scene.triangles.size() > max_triangles)
return std::unexpected(invalid("Raster exceeds the 64-triangle limit."));
const auto pixels = width * height;
if (!scene.triangles.empty() && pixels > max_raster_work / scene.triangles.size())
return std::unexpected(invalid("Raster exceeds the pixel-times-triangle work budget."));
if (!valid_color(scene.background))
return std::unexpected(invalid("Raster background needs finite RGB in 0–1."));
for (const auto& triangle : scene.triangles) {
for (const auto& vertex : triangle.vertices) {
if (!std::isfinite(vertex.x) || !std::isfinite(vertex.y) ||
std::abs(vertex.x) > max_screen_coordinate || std::abs(vertex.y) > max_screen_coordinate ||
!std::isfinite(vertex.depth) || vertex.depth < 0 || vertex.depth > 1 || !valid_color(vertex.color))
return std::unexpected(invalid("Vertex needs bounded finite screen coordinates, depth in 0–1, and RGB in 0–1."));
}
}
return {};
}
std::expected<Image, Error> rasterize(const RasterScene& scene) {
if (auto checked = validate_raster_scene(scene); !checked) return std::unexpected(checked.error());
const auto width = static_cast<std::size_t>(scene.width);
const auto count = width * static_cast<std::size_t>(scene.height); // Validated product.
if (count > std::numeric_limits<std::size_t>::max() / 3)
return std::unexpected(invalid("Raster RGB byte count would overflow."));
try {
Image image{scene.width, scene.height, std::vector<std::uint8_t>(count * 3)};
std::vector<double> depths(count, std::numeric_limits<double>::infinity());
for (std::size_t pixel = 0; pixel < count; ++pixel) {
image.rgb[pixel * 3] = encode(scene.background.r);
image.rgb[pixel * 3 + 1] = encode(scene.background.g);
image.rgb[pixel * 3 + 2] = encode(scene.background.b);
}
for (const auto& triangle : scene.triangles) {
const auto& a = triangle.vertices[0];
const auto& b = triangle.vertices[1];
const auto& c = triangle.vertices[2];
const double area = edge(a, b, c.x, c.y);
if (std::abs(area) <= 1e-12) continue; // A degenerate primitive covers nothing.
// Finite ±8192 input makes floor/ceil -> int conversions representable.
// Clamp the box BEFORE looping or indexing; off-screen geometry is safe.
const int min_x = std::max(0, static_cast<int>(std::floor(std::min({a.x, b.x, c.x}))));
const int min_y = std::max(0, static_cast<int>(std::floor(std::min({a.y, b.y, c.y}))));
const int max_x = std::min(static_cast<int>(scene.width) - 1, static_cast<int>(std::ceil(std::max({a.x, b.x, c.x}))) - 1);
const int max_y = std::min(static_cast<int>(scene.height) - 1, static_cast<int>(std::ceil(std::max({a.y, b.y, c.y}))) - 1);
for (int y = min_y; y <= max_y; ++y) {
for (int x = min_x; x <= max_x; ++x) {
const double px = static_cast<double>(x) + 0.5;
const double py = static_cast<double>(y) + 0.5;
const double wa = edge(b, c, px, py) / area;
const double wb = edge(c, a, px, py) / area;
const double wc = edge(a, b, px, py) / area;
if (wa < 0 || wb < 0 || wc < 0) continue; // Works for either winding.
const double depth = wa * a.depth + wb * b.depth + wc * c.depth;
const auto index = static_cast<std::size_t>(y) * width + static_cast<std::size_t>(x);
if (depth >= depths[index]) continue; // Near wins; exact ties keep the first.
depths[index] = depth;
image.rgb[index * 3] = encode(wa * a.color.r + wb * b.color.r + wc * c.color.r);
image.rgb[index * 3 + 1] = encode(wa * a.color.g + wb * b.color.g + wc * c.color.g);
image.rgb[index * 3 + 2] = encode(wa * a.color.b + wb * b.color.b + wc * c.color.b);
}
}
}
return image; // Both owners clean up automatically on return or exception.
} catch (const std::bad_alloc&) {
return std::unexpected(Error{ErrorCode::allocation, "Raster image or depth allocation failed."});
}
}
} // 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
