aurora-rendering-engine/experiments/rt8.cpp

607 lines
20 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <fstream>
#include <iostream>
#include <limits>
#include <string>
#include <vector>
static constexpr double kEps = 1e-9;
static inline double clamp01(double v) { return std::max(0.0, std::min(1.0, v)); }
static inline int clampInt(int v, int lo, int hi) { return std::max(lo, std::min(hi, v)); }
struct Vec2 {
double x = 0.0, y = 0.0;
Vec2() = default;
Vec2(double xx, double yy) : x(xx), y(yy) {}
Vec2 operator+(const Vec2& r) const { return Vec2(x + r.x, y + r.y); }
Vec2 operator-(const Vec2& r) const { return Vec2(x - r.x, y - r.y); }
Vec2 operator*(double s) const { return Vec2(x * s, y * s); }
Vec2 operator/(double s) const { return Vec2(x / s, y / s); }
};
static inline Vec2 operator*(double s, const Vec2& v) { return v * s; }
static inline double dot(const Vec2& a, const Vec2& b) { return a.x * b.x + a.y * b.y; }
static inline double cross2(const Vec2& a, const Vec2& b) { return a.x * b.y - a.y * b.x; }
static inline double orient2D(const Vec2& a, const Vec2& b, const Vec2& c) { return cross2(b - a, c - a); }
struct Vec3 {
double x = 0.0, y = 0.0, z = 0.0;
Vec3() = default;
Vec3(double xx, double yy, double zz) : x(xx), y(yy), z(zz) {}
Vec3 operator+(const Vec3& r) const { return Vec3(x + r.x, y + r.y, z + r.z); }
Vec3 operator-(const Vec3& r) const { return Vec3(x - r.x, y - r.y, z - r.z); }
Vec3 operator*(double s) const { return Vec3(x * s, y * s, z * s); }
Vec3 operator/(double s) const { return Vec3(x / s, y / s, z / s); }
Vec3 operator*(const Vec3& r) const { return Vec3(x * r.x, y * r.y, z * r.z); }
Vec3 operator/(const Vec3& r) const { return Vec3(x / r.x, y / r.y, z / r.z); }
};
static inline Vec3 operator*(double s, const Vec3& v) { return v * s; }
static inline double dot(const Vec3& a, const Vec3& b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
static inline Vec3 cross(const Vec3& a, const Vec3& b) {
return Vec3(a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x);
}
static inline double length(const Vec3& v) { return std::sqrt(dot(v, v)); }
static inline Vec3 normalize(const Vec3& v) {
double len = length(v);
if (len < kEps) return Vec3(0, 0, 0);
return v / len;
}
static inline Vec3 clamp01(const Vec3& c) { return Vec3(clamp01(c.x), clamp01(c.y), clamp01(c.z)); }
struct Triangle2D {
Vec2 a, b, c;
double areaAbs() const { return std::abs(orient2D(a, b, c)) * 0.5; }
Vec2 minXY() const {
return Vec2(std::min({a.x, b.x, c.x}), std::min({a.y, b.y, c.y}));
}
Vec2 maxXY() const {
return Vec2(std::max({a.x, b.x, c.x}), std::max({a.y, b.y, c.y}));
}
};
static inline bool barycentric2D(const Vec2& p, const Vec2& a, const Vec2& b, const Vec2& c, Vec3& wOut) {
double denom = orient2D(a, b, c);
if (std::abs(denom) < kEps) return false;
double w0 = orient2D(p, b, c) / denom;
double w1 = orient2D(a, p, c) / denom;
double w2 = 1.0 - w0 - w1;
wOut = Vec3(w0, w1, w2);
return true;
}
static inline bool pointInTri2D(const Vec2& p, const Triangle2D& t, double eps = 1e-8) {
Vec3 w;
if (!barycentric2D(p, t.a, t.b, t.c, w)) return false;
return (w.x >= -eps && w.y >= -eps && w.z >= -eps);
}
static inline bool onSegment(const Vec2& a, const Vec2& b, const Vec2& p, double eps = 1e-9) {
if (std::abs(orient2D(a, b, p)) > eps) return false;
return (std::min(a.x, b.x) - eps <= p.x && p.x <= std::max(a.x, b.x) + eps &&
std::min(a.y, b.y) - eps <= p.y && p.y <= std::max(a.y, b.y) + eps);
}
static inline bool segmentsIntersect2D(const Vec2& p1, const Vec2& p2, const Vec2& q1, const Vec2& q2) {
double o1 = orient2D(p1, p2, q1);
double o2 = orient2D(p1, p2, q2);
double o3 = orient2D(q1, q2, p1);
double o4 = orient2D(q1, q2, p2);
auto sgn = [](double v) -> int {
if (v > 1e-12) return 1;
if (v < -1e-12) return -1;
return 0;
};
int s1 = sgn(o1), s2 = sgn(o2), s3 = sgn(o3), s4 = sgn(o4);
if (s1 * s2 < 0 && s3 * s4 < 0) return true;
if (s1 == 0 && onSegment(p1, p2, q1)) return true;
if (s2 == 0 && onSegment(p1, p2, q2)) return true;
if (s3 == 0 && onSegment(q1, q2, p1)) return true;
if (s4 == 0 && onSegment(q1, q2, p2)) return true;
return false;
}
static inline bool trianglesOverlap2D(const Triangle2D& t1, const Triangle2D& t2) {
Vec2 min1 = t1.minXY(), max1 = t1.maxXY();
Vec2 min2 = t2.minXY(), max2 = t2.maxXY();
if (max1.x < min2.x || max2.x < min1.x || max1.y < min2.y || max2.y < min1.y) return false;
if (pointInTri2D(t1.a, t2) || pointInTri2D(t1.b, t2) || pointInTri2D(t1.c, t2)) return true;
if (pointInTri2D(t2.a, t1) || pointInTri2D(t2.b, t1) || pointInTri2D(t2.c, t1)) return true;
std::array<Vec2, 3> e1a{t1.a, t1.b, t1.c};
std::array<Vec2, 3> e1b{t1.b, t1.c, t1.a};
std::array<Vec2, 3> e2a{t2.a, t2.b, t2.c};
std::array<Vec2, 3> e2b{t2.b, t2.c, t2.a};
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
if (segmentsIntersect2D(e1a[i], e1b[i], e2a[j], e2b[j])) return true;
}
}
return false;
}
enum class MaterialType { Curtain, Diffuse, Metal };
struct Material {
MaterialType type = MaterialType::Diffuse;
Vec3 baseColor = Vec3(1, 1, 1);
double reflectance = 0.85;
};
struct Triangle {
int id = -1;
Vec3 a, b, c;
Vec2 uvA, uvB, uvC;
Material mat;
Vec3 normal() const { return normalize(cross(b - a, c - a)); }
Vec3 centroid() const { return (a + b + c) / 3.0; }
};
struct Image {
int w = 0, h = 0;
std::vector<Vec3> px;
Image(int width, int height) : w(width), h(height), px((size_t)width * (size_t)height, Vec3(0, 0, 0)) {}
void clear(const Vec3& c) { std::fill(px.begin(), px.end(), c); }
Vec3 get(int x, int y) const { return px[(size_t)y * (size_t)w + (size_t)x]; }
void blendPixel(int x, int y, const Vec3& src, double alpha) {
if (x < 0 || x >= w || y < 0 || y >= h) return;
alpha = clamp01(alpha);
Vec3& dst = px[(size_t)y * (size_t)w + (size_t)x];
dst = dst * (1.0 - alpha) + src * alpha;
}
void writePPM(const std::string& path) const {
std::ofstream out(path, std::ios::binary);
if (!out) {
std::cerr << "Cannot open output file: " << path << "\n";
return;
}
out << "P6\n" << w << " " << h << "\n255\n";
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
Vec3 c = clamp01(get(x, y));
auto toSRGB8 = [](double v) -> unsigned char {
v = clamp01(v);
v = std::pow(v, 1.0 / 2.2);
int iv = (int)std::lround(v * 255.0);
iv = clampInt(iv, 0, 255);
return (unsigned char)iv;
};
unsigned char r = toSRGB8(c.x);
unsigned char g = toSRGB8(c.y);
unsigned char b = toSRGB8(c.z);
out.write((char*)&r, 1);
out.write((char*)&g, 1);
out.write((char*)&b, 1);
}
}
}
};
static inline Vec3 reflectPointAcrossPlane(const Vec3& p, const Vec3& planePoint, const Vec3& planeNormalUnit) {
double d = dot(p - planePoint, planeNormalUnit);
return p - planeNormalUnit * (2.0 * d);
}
static inline bool projectPointToPlaneFromOrigin(const Vec3& origin,
const Vec3& point,
const Vec3& planePoint,
const Vec3& planeNormalUnit,
Vec3& hit,
double& tOut) {
Vec3 dir = point - origin;
double denom = dot(dir, planeNormalUnit);
if (denom <= 1e-12) return false;
double t = dot(planePoint - origin, planeNormalUnit) / denom;
if (t <= 1e-12) return false;
hit = origin + dir * t;
tOut = t;
return true;
}
static inline double wrap01(double u) {
u = u - std::floor(u);
if (u < 0) u += 1.0;
return u;
}
static inline Vec3 sampleCheckerUV(const Vec2& uv, const Vec3& base) {
double u = wrap01(uv.x);
double v = wrap01(uv.y);
const double scale = 10.0;
int iu = (int)std::floor(u * scale);
int iv = (int)std::floor(v * scale);
double checker = ((iu + iv) & 1) ? 0.82 : 1.0;
double fu = u * scale - std::floor(u * scale);
double fv = v * scale - std::floor(v * scale);
double line = (fu < 0.05 || fv < 0.05) ? 0.55 : 1.0;
return base * (checker * line);
}
struct RenderSettings {
int maxDepth = 3;
double minViewportAreaPx = 3.0;
double minReflectAreaPx = 8.0;
};
struct RenderLayer {
Vec3 tint = Vec3(1, 1, 1);
double alpha = 1.0;
};
struct Candidate {
const Triangle* tri = nullptr;
Triangle2D projPlane;
Triangle2D projScreen;
double depthMetric = 0.0;
};
static inline Vec2 mapPlaneToScreen(const Vec2& pPlane, const Triangle2D& viewportPlaneTri, const Triangle2D& viewportScreenTri) {
Vec3 w;
if (!barycentric2D(pPlane, viewportPlaneTri.a, viewportPlaneTri.b, viewportPlaneTri.c, w)) return viewportScreenTri.a;
return viewportScreenTri.a * w.x + viewportScreenTri.b * w.y + viewportScreenTri.c * w.z;
}
static Vec3 shadeTriangle(const Triangle& tri, const Vec2& uv, const Vec3& viewOrigin) {
Vec3 n = tri.normal();
Vec3 lightDir = normalize(Vec3(-0.35, 0.9, -0.25)); // 方向光:从上方略偏前
double ndotl = std::max(0.0, dot(n, lightDir));
Vec3 albedo = tri.mat.baseColor;
if (tri.mat.type == MaterialType::Diffuse) {
albedo = sampleCheckerUV(uv, albedo);
double ambient = 0.22;
double diff = ambient + (1.0 - ambient) * ndotl;
return albedo * diff;
}
if (tri.mat.type == MaterialType::Metal) {
double ambient = 0.05;
double diff = ambient + (1.0 - ambient) * std::pow(ndotl, 0.35);
return albedo * diff;
}
return albedo;
}
static void rasterizeTriangleToImage(const Triangle& tri,
const Triangle2D& triScreen,
const Triangle2D& clipScreen,
Image& img,
const Vec3& viewOrigin,
const RenderLayer& layer) {
Vec2 bbMin = triScreen.minXY();
Vec2 bbMax = triScreen.maxXY();
int minX = clampInt((int)std::floor(bbMin.x), 0, img.w - 1);
int maxX = clampInt((int)std::ceil(bbMax.x), 0, img.w - 1);
int minY = clampInt((int)std::floor(bbMin.y), 0, img.h - 1);
int maxY = clampInt((int)std::ceil(bbMax.y), 0, img.h - 1);
for (int y = minY; y <= maxY; ++y) {
for (int x = minX; x <= maxX; ++x) {
Vec2 p((double)x + 0.5, (double)y + 0.5);
if (!pointInTri2D(p, clipScreen)) continue;
Vec3 w;
if (!barycentric2D(p, triScreen.a, triScreen.b, triScreen.c, w)) continue;
if (w.x < -1e-8 || w.y < -1e-8 || w.z < -1e-8) continue;
Vec2 uv = tri.uvA * w.x + tri.uvB * w.y + tri.uvC * w.z;
Vec3 c = shadeTriangle(tri, uv, viewOrigin);
c = c * layer.tint;
img.blendPixel(x, y, c, layer.alpha);
}
}
}
static void renderTriangleWithTriangle(const std::vector<Triangle>& scene,
const Vec3& origin,
const Triangle& viewportWorld,
const Triangle2D& viewportScreen,
Image& img,
int depth,
const RenderSettings& settings,
const RenderLayer& layer,
int ignoreTriId) {
if (depth > settings.maxDepth) return;
double viewportArea = viewportScreen.areaAbs();
if (viewportArea < settings.minViewportAreaPx) return;
Vec3 planePoint = viewportWorld.a;
Vec3 n = normalize(cross(viewportWorld.b - viewportWorld.a, viewportWorld.c - viewportWorld.a));
if (length(n) < 1e-12) return;
if (dot(planePoint - origin, n) < 0.0) n = n * -1.0;
double planeDist = dot(planePoint - origin, n);
if (planeDist <= 1e-9) return;
Vec3 u3 = normalize(viewportWorld.b - viewportWorld.a);
if (length(u3) < 1e-12) return;
Vec3 v3 = normalize(cross(n, u3));
if (length(v3) < 1e-12) return;
Triangle2D viewportPlane;
viewportPlane.a = Vec2(0.0, 0.0);
viewportPlane.b = Vec2(dot(viewportWorld.b - viewportWorld.a, u3), dot(viewportWorld.b - viewportWorld.a, v3));
viewportPlane.c = Vec2(dot(viewportWorld.c - viewportWorld.a, u3), dot(viewportWorld.c - viewportWorld.a, v3));
std::vector<Candidate> candidates;
candidates.reserve(scene.size());
for (const Triangle& tri : scene) {
if (tri.id == ignoreTriId) continue;
if (tri.mat.type == MaterialType::Curtain) continue;
std::array<Vec3, 3> vtx{tri.a, tri.b, tri.c};
std::array<Vec2, 3> proj2;
std::array<double, 3> denom;
bool ok = true;
for (int i = 0; i < 3; ++i) {
Vec3 hit;
double t;
if (!projectPointToPlaneFromOrigin(origin, vtx[i], planePoint, n, hit, t)) { ok = false; break; }
if (t >= 1.0 - 1e-6) { ok = false; break; }
proj2[i] = Vec2(dot(hit - planePoint, u3), dot(hit - planePoint, v3));
denom[i] = dot(vtx[i] - origin, n);
if (denom[i] <= 1e-9) { ok = false; break; }
}
if (!ok) continue;
Triangle2D projPlane{proj2[0], proj2[1], proj2[2]};
if (!trianglesOverlap2D(projPlane, viewportPlane)) continue;
Triangle2D projScreen;
projScreen.a = mapPlaneToScreen(projPlane.a, viewportPlane, viewportScreen);
projScreen.b = mapPlaneToScreen(projPlane.b, viewportPlane, viewportScreen);
projScreen.c = mapPlaneToScreen(projPlane.c, viewportPlane, viewportScreen);
double depthMetric = (denom[0] + denom[1] + denom[2]) / 3.0;
candidates.push_back(Candidate{&tri, projPlane, projScreen, depthMetric});
}
std::sort(candidates.begin(), candidates.end(),
[](const Candidate& lhs, const Candidate& rhs) { return lhs.depthMetric > rhs.depthMetric; });
bool enableCull = (depth == 0);
for (const Candidate& cand : candidates) {
const Triangle& tri = *cand.tri;
if (enableCull) {
Vec3 nn = tri.normal();
Vec3 toOrigin = origin - tri.centroid();
if (dot(nn, toOrigin) <= 0.0) continue;
}
rasterizeTriangleToImage(tri, cand.projScreen, viewportScreen, img, origin, layer);
if (tri.mat.type == MaterialType::Metal) {
double triAreaPx = cand.projScreen.areaAbs();
if (triAreaPx < settings.minReflectAreaPx) continue;
if (depth >= settings.maxDepth) continue;
Vec3 mirrorN = tri.normal();
if (length(mirrorN) < 1e-12) continue;
Vec3 mirroredOrigin = reflectPointAcrossPlane(origin, tri.a, mirrorN);
RenderLayer childLayer;
childLayer.tint = layer.tint * tri.mat.baseColor;
childLayer.alpha = layer.alpha * clamp01(tri.mat.reflectance);
if (childLayer.alpha < 1e-4) continue;
renderTriangleWithTriangle(scene,
mirroredOrigin,
tri,
cand.projScreen,
img,
depth + 1,
settings,
childLayer,
tri.id);
}
}
}
static Triangle makeTri(int& idCounter,
const Vec3& a, const Vec3& b, const Vec3& c,
const Vec2& uvA, const Vec2& uvB, const Vec2& uvC,
const Material& mat) {
Triangle t;
t.id = idCounter++;
t.a = a; t.b = b; t.c = c;
t.uvA = uvA; t.uvB = uvB; t.uvC = uvC;
t.mat = mat;
return t;
}
static void addQuad(std::vector<Triangle>& out, int& idCounter,
const Vec3& p00, const Vec3& p10, const Vec3& p11, const Vec3& p01,
const Material& mat,
bool windingForGivenNormal = true) {
Vec2 uv00(0, 0), uv10(1, 0), uv11(1, 1), uv01(0, 1);
if (windingForGivenNormal) {
out.push_back(makeTri(idCounter, p00, p11, p10, uv00, uv11, uv10, mat));
out.push_back(makeTri(idCounter, p00, p01, p11, uv00, uv01, uv11, mat));
} else {
out.push_back(makeTri(idCounter, p00, p10, p11, uv00, uv10, uv11, mat));
out.push_back(makeTri(idCounter, p00, p11, p01, uv00, uv11, uv01, mat));
}
}
static void addAxisAlignedBox(std::vector<Triangle>& out, int& idCounter,
const Vec3& bmin, const Vec3& bmax,
const Material& mat) {
Vec3 p000(bmin.x, bmin.y, bmin.z);
Vec3 p100(bmax.x, bmin.y, bmin.z);
Vec3 p110(bmax.x, bmax.y, bmin.z);
Vec3 p010(bmin.x, bmax.y, bmin.z);
Vec3 p001(bmin.x, bmin.y, bmax.z);
Vec3 p101(bmax.x, bmin.y, bmax.z);
Vec3 p111(bmax.x, bmax.y, bmax.z);
Vec3 p011(bmin.x, bmax.y, bmax.z);
addQuad(out, idCounter, p001, p101, p111, p011, mat, true); // front +z
addQuad(out, idCounter, p100, p000, p010, p110, mat, true); // back -z
addQuad(out, idCounter, p000, p001, p011, p010, mat, true); // left -x
addQuad(out, idCounter, p101, p100, p110, p111, mat, true); // right +x
addQuad(out, idCounter, p010, p011, p111, p110, mat, true); // top +y
addQuad(out, idCounter, p000, p100, p101, p001, mat, true); // bottom -y
}
struct Camera {
Vec3 origin;
Triangle viewportWorld0;
Triangle viewportWorld1;
Triangle2D viewportScreen0;
Triangle2D viewportScreen1;
void render(const std::vector<Triangle>& scene, Image& img, const RenderSettings& settings) const {
RenderLayer rootLayer;
rootLayer.tint = Vec3(1, 1, 1);
rootLayer.alpha = 1.0;
renderTriangleWithTriangle(scene, origin, viewportWorld0, viewportScreen0, img, 0, settings, rootLayer, -1);
renderTriangleWithTriangle(scene, origin, viewportWorld1, viewportScreen1, img, 0, settings, rootLayer, -1);
}
};
int main() {
const int W = 800;
const int H = 600;
Image img(W, H);
img.clear(Vec3(0.02, 0.02, 0.025));
RenderSettings settings;
settings.maxDepth = 3;
settings.minViewportAreaPx = 2.0;
settings.minReflectAreaPx = 14.0;
std::vector<Triangle> scene;
scene.reserve(128);
int idCounter = 0;
Material redWall; redWall.type = MaterialType::Diffuse; redWall.baseColor = Vec3(0.78, 0.16, 0.16);
Material greenWall; greenWall.type = MaterialType::Diffuse; greenWall.baseColor = Vec3(0.16, 0.78, 0.18);
Material whiteWall; whiteWall.type = MaterialType::Diffuse; whiteWall.baseColor = Vec3(0.82, 0.82, 0.82);
Material metalBlue; metalBlue.type = MaterialType::Metal; metalBlue.baseColor = Vec3(0.12, 0.35, 1.00); metalBlue.reflectance = 0.85;
Material metalYellow; metalYellow.type = MaterialType::Metal; metalYellow.baseColor = Vec3(1.00, 0.86, 0.18); metalYellow.reflectance = 0.85;
// Cornell Box: x [-1,1], y [0,2], z [0,2]
// floor (y=0), normal +y
addQuad(scene, idCounter,
Vec3(-1, 0, 0), Vec3( 1, 0, 0), Vec3( 1, 0, 2), Vec3(-1, 0, 2),
whiteWall, true);
// ceiling (y=2), normal -y用反 winding
addQuad(scene, idCounter,
Vec3(-1, 2, 2), Vec3( 1, 2, 2), Vec3( 1, 2, 0), Vec3(-1, 2, 0),
whiteWall, true);
// back wall (z=2), normal -z
addQuad(scene, idCounter,
Vec3(-1, 0, 2), Vec3( 1, 0, 2), Vec3( 1, 2, 2), Vec3(-1, 2, 2),
whiteWall, false);
// left wall (x=-1), normal +x
addQuad(scene, idCounter,
Vec3(-1, 0, 0), Vec3(-1, 0, 2), Vec3(-1, 2, 2), Vec3(-1, 2, 0),
redWall, true);
// right wall (x=1), normal -x
addQuad(scene, idCounter,
Vec3( 1, 0, 2), Vec3( 1, 0, 0), Vec3( 1, 2, 0), Vec3( 1, 2, 2),
greenWall, true);
// Two metallic boxes (axis-aligned for simplicity)
addAxisAlignedBox(scene, idCounter, Vec3(-0.65, 0.0, 0.55), Vec3(-0.10, 0.70, 1.10), metalBlue);
addAxisAlignedBox(scene, idCounter, Vec3( 0.10, 0.0, 1.05), Vec3( 0.70, 0.95, 1.70), metalYellow);
// Camera + viewport (two triangles)
Camera cam;
cam.origin = Vec3(0.0, 1.0, -3.0);
Material curtain; curtain.type = MaterialType::Curtain; curtain.baseColor = Vec3(0, 0, 0);
Vec3 bl(-1.0, 0.0, -1.0);
Vec3 br( 1.0, 0.0, -1.0);
Vec3 tr( 1.0, 2.0, -1.0);
Vec3 tl(-1.0, 2.0, -1.0);
{
Triangle t0;
t0.id = -100;
t0.a = bl; t0.b = br; t0.c = tl;
t0.uvA = Vec2(0, 1); t0.uvB = Vec2(1, 1); t0.uvC = Vec2(0, 0);
t0.mat = curtain;
cam.viewportWorld0 = t0;
Triangle t1;
t1.id = -101;
t1.a = br; t1.b = tr; t1.c = tl;
t1.uvA = Vec2(1, 1); t1.uvB = Vec2(1, 0); t1.uvC = Vec2(0, 0);
t1.mat = curtain;
cam.viewportWorld1 = t1;
}
// Screen triangles cover whole image rectangle
Vec2 sbl(0.0, (double)H - 1.0);
Vec2 sbr((double)W - 1.0, (double)H - 1.0);
Vec2 str((double)W - 1.0, 0.0);
Vec2 stl(0.0, 0.0);
cam.viewportScreen0 = Triangle2D{sbl, sbr, stl};
cam.viewportScreen1 = Triangle2D{sbr, str, stl};
cam.render(scene, img, settings);
img.writePPM("output.ppm");
std::cerr << "Done. Wrote output.ppm\n";
return 0;
}