744 lines
19 KiB
C++
744 lines
19 KiB
C++
/*
|
|
Cornell Box: triangle-driven rasterization + Z-buffer + recursive planar mirror reflection (render-to-texture)
|
|
|
|
Constraints satisfied:
|
|
- NO "main camera per-pixel ray cast & triangle intersection".
|
|
- Visibility is from projection + triangle rasterization + Z-test only.
|
|
- Mirror reflection uses recursive offscreen rendering from a mirrored camera, then samples that image.
|
|
|
|
Key fixes vs the broken/dark versions:
|
|
- Add tone mapping + gamma encoding on output (prevents "too dark" or "washed out / flat").
|
|
- Use point light + ambient with face-forward normals for diffuse.
|
|
- Mirror cache keyed by plane (two triangles on a face share one reflection render).
|
|
- When rendering the mirror pass, exclude all triangles coplanar with that mirror plane to avoid self-occlusion.
|
|
|
|
Build:
|
|
g++ -O2 -std=c++17 cornell_raster_reflect.cpp -o cornell
|
|
|
|
Run:
|
|
./cornell
|
|
|
|
Output:
|
|
output.ppm (P3)
|
|
*/
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
static constexpr float kEps = 1e-6f;
|
|
static constexpr float kInf = std::numeric_limits<float>::infinity();
|
|
|
|
struct Vec2 {
|
|
float x = 0, y = 0;
|
|
Vec2() = default;
|
|
Vec2(float _x, float _y) : x(_x), y(_y) {
|
|
}
|
|
Vec2 operator+(const Vec2 &o) const {
|
|
return { x + o.x, y + o.y };
|
|
}
|
|
Vec2 operator-(const Vec2 &o) const {
|
|
return { x - o.x, y - o.y };
|
|
}
|
|
Vec2 operator*(float s) const {
|
|
return { x * s, y * s };
|
|
}
|
|
};
|
|
|
|
struct Vec3 {
|
|
float x = 0, y = 0, z = 0;
|
|
Vec3() = default;
|
|
Vec3(float _x, float _y, float _z) : x(_x), y(_y), z(_z) {
|
|
}
|
|
Vec3 operator+(const Vec3 &o) const {
|
|
return { x + o.x, y + o.y, z + o.z };
|
|
}
|
|
Vec3 operator-(const Vec3 &o) const {
|
|
return { x - o.x, y - o.y, z - o.z };
|
|
}
|
|
Vec3 operator*(float s) const {
|
|
return { x * s, y * s, z * s };
|
|
}
|
|
Vec3 operator/(float s) const {
|
|
return { x / s, y / s, z / s };
|
|
}
|
|
Vec3 &operator+=(const Vec3 &o) {
|
|
x += o.x;
|
|
y += o.y;
|
|
z += o.z;
|
|
return *this;
|
|
}
|
|
};
|
|
|
|
static inline Vec3 operator*(float s, const Vec3 &v) {
|
|
return v * s;
|
|
}
|
|
|
|
static inline float 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 {
|
|
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 float length(const Vec3 &v) {
|
|
return std::sqrt(dot(v, v));
|
|
}
|
|
|
|
static inline Vec3 normalize(const Vec3 &v) {
|
|
float len = length(v);
|
|
if (len < kEps)
|
|
return { 0, 0, 0 };
|
|
return v / len;
|
|
}
|
|
|
|
static inline Vec3 clamp01(const Vec3 &c) {
|
|
return {
|
|
std::clamp(c.x, 0.0f, 1.0f),
|
|
std::clamp(c.y, 0.0f, 1.0f),
|
|
std::clamp(c.z, 0.0f, 1.0f),
|
|
};
|
|
}
|
|
|
|
static inline Vec3 hadamard(const Vec3 &a, const Vec3 &b) {
|
|
return { a.x * b.x, a.y * b.y, a.z * b.z };
|
|
}
|
|
|
|
static inline Vec3 reinhardTonemap(const Vec3 &c) {
|
|
// Per-channel Reinhard; simple and stable for this demo.
|
|
return {
|
|
c.x / (1.0f + c.x),
|
|
c.y / (1.0f + c.y),
|
|
c.z / (1.0f + c.z),
|
|
};
|
|
}
|
|
|
|
static inline float gammaEncode(float linear, float gamma = 2.2f) {
|
|
linear = std::max(0.0f, linear);
|
|
return std::pow(linear, 1.0f / gamma);
|
|
}
|
|
|
|
static inline Vec3 gammaEncode(const Vec3 &linear, float gamma = 2.2f) {
|
|
return { gammaEncode(linear.x, gamma), gammaEncode(linear.y, gamma), gammaEncode(linear.z, gamma) };
|
|
}
|
|
|
|
enum class MaterialType {
|
|
Diffuse = 0,
|
|
Metal = 1
|
|
};
|
|
|
|
struct Material {
|
|
MaterialType type = MaterialType::Diffuse;
|
|
Vec3 albedo = { 1, 1, 1 }; // Diffuse base color
|
|
float reflectance = 1.0f; // Metal reflectance [0,1]
|
|
};
|
|
|
|
struct Triangle {
|
|
Vec3 p0, p1, p2;
|
|
Material mat;
|
|
Vec3 normal; // flat normal (unit)
|
|
};
|
|
|
|
static inline Vec3 computeFlatNormal(const Triangle &t) {
|
|
return normalize(cross(t.p1 - t.p0, t.p2 - t.p0));
|
|
}
|
|
|
|
struct Plane {
|
|
Vec3 p; // point on plane
|
|
Vec3 n; // unit normal (oriented)
|
|
};
|
|
|
|
static inline float signedDistance(const Plane &pl, const Vec3 &x) {
|
|
return dot(pl.n, x - pl.p);
|
|
}
|
|
|
|
static inline Vec3 reflectPointAboutPlane(const Vec3 &x, const Plane &pl) {
|
|
float d = signedDistance(pl, x);
|
|
return x - (2.0f * d) * pl.n;
|
|
}
|
|
|
|
static inline Vec3 reflectVectorAboutPlane(const Vec3 &v, const Plane &pl) {
|
|
return v - (2.0f * dot(v, pl.n)) * pl.n;
|
|
}
|
|
|
|
struct Image {
|
|
int w = 0, h = 0;
|
|
std::vector<Vec3> pix; // linear HDR-ish buffer
|
|
|
|
void reset(int width, int height, Vec3 clear = { 0, 0, 0 }) {
|
|
w = width;
|
|
h = height;
|
|
pix.assign((size_t)w * (size_t)h, clear);
|
|
}
|
|
|
|
Vec3 get(int x, int y) const {
|
|
if (x < 0 || x >= w || y < 0 || y >= h)
|
|
return { 0, 0, 0 };
|
|
return pix[(size_t)y * (size_t)w + (size_t)x];
|
|
}
|
|
|
|
void set(int x, int y, const Vec3 &c) {
|
|
if (x < 0 || x >= w || y < 0 || y >= h)
|
|
return;
|
|
pix[(size_t)y * (size_t)w + (size_t)x] = c;
|
|
}
|
|
|
|
Vec3 sampleNearest(float u, float v) const {
|
|
if (w <= 0 || h <= 0)
|
|
return { 0, 0, 0 };
|
|
int x = (int)std::floor(u * (float)w);
|
|
int y = (int)std::floor(v * (float)h);
|
|
x = std::clamp(x, 0, w - 1);
|
|
y = std::clamp(y, 0, h - 1);
|
|
return get(x, y);
|
|
}
|
|
|
|
void writePPM_P3(const std::string &path) const {
|
|
std::ofstream out(path, std::ios::out);
|
|
if (!out) {
|
|
std::cerr << "Failed to open: " << path << "\n";
|
|
return;
|
|
}
|
|
out << "P3\n"
|
|
<< w << " " << h << "\n255\n";
|
|
for (int y = 0; y < h; ++y) {
|
|
for (int x = 0; x < w; ++x) {
|
|
Vec3 c = get(x, y);
|
|
|
|
// Fix: tone mapping + gamma for display
|
|
c = reinhardTonemap(c);
|
|
c = gammaEncode(c, 2.2f);
|
|
c = clamp01(c);
|
|
|
|
int r = (int)std::round(c.x * 255.0f);
|
|
int g = (int)std::round(c.y * 255.0f);
|
|
int b = (int)std::round(c.z * 255.0f);
|
|
out << r << " " << g << " " << b << "\n";
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
struct DepthBuffer {
|
|
int w = 0, h = 0;
|
|
std::vector<float> z; // camera-space z, smaller=closer
|
|
|
|
void reset(int width, int height) {
|
|
w = width;
|
|
h = height;
|
|
z.assign((size_t)w * (size_t)h, kInf);
|
|
}
|
|
|
|
float get(int x, int y) const {
|
|
return z[(size_t)y * (size_t)w + (size_t)x];
|
|
}
|
|
|
|
void set(int x, int y, float v) {
|
|
z[(size_t)y * (size_t)w + (size_t)x] = v;
|
|
}
|
|
};
|
|
|
|
struct RenderTarget {
|
|
Image color;
|
|
DepthBuffer depth;
|
|
};
|
|
|
|
struct Camera {
|
|
Vec3 origin;
|
|
Vec3 forward;
|
|
Vec3 right;
|
|
Vec3 up;
|
|
|
|
float fovY_deg = 60.0f;
|
|
float nearZ = 0.05f;
|
|
float farZ = 50.0f;
|
|
|
|
int imgW = 640;
|
|
int imgH = 480;
|
|
|
|
static Camera lookAt(const Vec3 &eye, const Vec3 &target, const Vec3 &upHint,
|
|
float fovY_deg, float nearZ, float farZ,
|
|
int w, int h) {
|
|
Camera c;
|
|
c.origin = eye;
|
|
c.forward = normalize(target - eye);
|
|
c.right = normalize(cross(c.forward, upHint));
|
|
c.up = normalize(cross(c.right, c.forward));
|
|
c.fovY_deg = fovY_deg;
|
|
c.nearZ = nearZ;
|
|
c.farZ = farZ;
|
|
c.imgW = w;
|
|
c.imgH = h;
|
|
return c;
|
|
}
|
|
|
|
Vec3 worldToCamera(const Vec3 &p) const {
|
|
Vec3 d = p - origin;
|
|
return { dot(d, right), dot(d, up), dot(d, forward) }; // z>0 in front
|
|
}
|
|
|
|
bool projectToScreen(const Vec3 &worldP, Vec2 &outXY, float &outInvZ, float &outCamZ) const {
|
|
Vec3 pc = worldToCamera(worldP);
|
|
float z = pc.z;
|
|
outCamZ = z;
|
|
if (z <= nearZ)
|
|
return false;
|
|
|
|
float aspect = (float)imgW / (float)imgH;
|
|
float fovY = fovY_deg * (3.1415926535f / 180.0f);
|
|
float tanHalf = std::tan(fovY * 0.5f);
|
|
|
|
// Perspective projection with vertical fov
|
|
float ndcX = (pc.x / (z * tanHalf)) / aspect;
|
|
float ndcY = (pc.y / (z * tanHalf));
|
|
|
|
float px = (ndcX * 0.5f + 0.5f) * (float)imgW;
|
|
float py = (1.0f - (ndcY * 0.5f + 0.5f)) * (float)imgH;
|
|
|
|
outXY = { px, py };
|
|
outInvZ = 1.0f / z;
|
|
return true;
|
|
}
|
|
};
|
|
|
|
struct ScreenVertex {
|
|
Vec2 s;
|
|
float invZ;
|
|
Vec3 worldP;
|
|
};
|
|
|
|
static inline float edgeFunction(const Vec2 &a, const Vec2 &b, const Vec2 &c) {
|
|
return (c.x - a.x) * (b.y - a.y) - (c.y - a.y) * (b.x - a.x);
|
|
}
|
|
|
|
static inline float triangleAreaPixels(const Vec2 &a, const Vec2 &b, const Vec2 &c) {
|
|
return std::abs(edgeFunction(a, b, c)) * 0.5f;
|
|
}
|
|
|
|
struct RenderConfig {
|
|
int maxDepth = 2;
|
|
float minMirrorAreaPixels = 6.0f;
|
|
Vec3 clearColor = { 0.0f, 0.0f, 0.0f }; // linear clear
|
|
};
|
|
|
|
struct RenderContext {
|
|
const std::vector<Triangle> *tris = nullptr;
|
|
RenderConfig cfg;
|
|
|
|
// Simple point light (no shadows)
|
|
Vec3 lightPos = { 0.0f, 1.95f, 0.0f };
|
|
Vec3 lightColor = { 1.0f, 1.0f, 1.0f };
|
|
float lightIntensity = 25.0f;
|
|
|
|
Vec3 ambient = { 0.06f, 0.06f, 0.06f };
|
|
};
|
|
|
|
static inline Vec3 faceForwardToView(Vec3 n, const Vec3 &worldP, const Camera &cam) {
|
|
Vec3 V = normalize(cam.origin - worldP);
|
|
if (dot(n, V) < 0.0f)
|
|
n = n * -1.0f;
|
|
return n;
|
|
}
|
|
|
|
static inline Vec3 shadeDiffuse(const Triangle &t,
|
|
const Vec3 &worldP,
|
|
const Vec3 &worldN,
|
|
const Camera &cam,
|
|
const RenderContext &ctx) {
|
|
Vec3 N = faceForwardToView(worldN, worldP, cam);
|
|
|
|
Vec3 Lvec = ctx.lightPos - worldP;
|
|
float dist2 = std::max(kEps, dot(Lvec, Lvec));
|
|
Vec3 L = normalize(Lvec);
|
|
|
|
float ndotl = std::max(0.0f, dot(N, L));
|
|
float atten = 1.0f / dist2;
|
|
|
|
Vec3 direct = (ctx.lightIntensity * ndotl * atten) * ctx.lightColor;
|
|
Vec3 lit = ctx.ambient + direct;
|
|
|
|
return hadamard(t.mat.albedo, lit);
|
|
}
|
|
|
|
static inline Plane orientedPlaneTowardCamera(const Triangle &t, const Camera &cam) {
|
|
Plane pl;
|
|
pl.p = t.p0;
|
|
pl.n = normalize(t.normal);
|
|
// Ensure camera is on the positive side (for consistent mirroring orientation)
|
|
float s = dot(pl.n, cam.origin - pl.p);
|
|
if (s < 0.0f)
|
|
pl.n = pl.n * -1.0f;
|
|
return pl;
|
|
}
|
|
|
|
static inline Camera mirrorCameraAcrossPlane(const Camera &cam, const Plane &pl) {
|
|
Camera mc = cam;
|
|
mc.origin = reflectPointAboutPlane(cam.origin, pl);
|
|
mc.forward = normalize(reflectVectorAboutPlane(cam.forward, pl));
|
|
mc.up = normalize(reflectVectorAboutPlane(cam.up, pl));
|
|
mc.right = normalize(cross(mc.forward, mc.up));
|
|
mc.up = normalize(cross(mc.right, mc.forward));
|
|
return mc;
|
|
}
|
|
|
|
static inline bool triangleCoplanarWithPlane(const Triangle &t, const Plane &pl) {
|
|
// Exclude triangles on the same plane (both triangles of a quad face)
|
|
// Use both parallel-normal test and vertex distance test.
|
|
float parallel = std::abs(dot(normalize(t.normal), pl.n));
|
|
if (parallel < 0.999f)
|
|
return false;
|
|
|
|
float d0 = std::abs(signedDistance(pl, t.p0));
|
|
float d1 = std::abs(signedDistance(pl, t.p1));
|
|
float d2 = std::abs(signedDistance(pl, t.p2));
|
|
return (d0 < 1e-4f && d1 < 1e-4f && d2 < 1e-4f);
|
|
}
|
|
|
|
struct PlaneKey {
|
|
int nx = 0, ny = 0, nz = 0;
|
|
int d = 0; // plane equation: n·x + d = 0 (quantized)
|
|
bool operator==(const PlaneKey &o) const {
|
|
return nx == o.nx && ny == o.ny && nz == o.nz && d == o.d;
|
|
}
|
|
};
|
|
|
|
struct PlaneKeyHash {
|
|
size_t operator()(const PlaneKey &k) const noexcept {
|
|
// Simple hash combine
|
|
size_t h = 1469598103934665603ull;
|
|
auto mix = [&](uint64_t v) {
|
|
h ^= v + 0x9e3779b97f4a7c15ull + (h << 6) + (h >> 2);
|
|
};
|
|
mix((uint64_t)(uint32_t)k.nx);
|
|
mix((uint64_t)(uint32_t)k.ny);
|
|
mix((uint64_t)(uint32_t)k.nz);
|
|
mix((uint64_t)(uint32_t)k.d);
|
|
return h;
|
|
}
|
|
};
|
|
|
|
static inline PlaneKey makePlaneKey(const Plane &pl) {
|
|
// Quantize plane normal and offset for caching; oriented plane already.
|
|
// Plane equation: n·x + d = 0 where d = -n·p
|
|
float d = -dot(pl.n, pl.p);
|
|
const float sN = 10000.0f;
|
|
const float sD = 10000.0f;
|
|
|
|
PlaneKey k;
|
|
k.nx = (int)std::round(pl.n.x * sN);
|
|
k.ny = (int)std::round(pl.n.y * sN);
|
|
k.nz = (int)std::round(pl.n.z * sN);
|
|
k.d = (int)std::round(d * sD);
|
|
return k;
|
|
}
|
|
|
|
struct MirrorCacheEntry {
|
|
Plane plane;
|
|
Camera mirrorCam;
|
|
RenderTarget rt;
|
|
bool ready = false;
|
|
};
|
|
|
|
static RenderTarget renderPass(const Camera &cam,
|
|
const RenderContext &ctx,
|
|
int recursionDepth,
|
|
const Plane *excludeCoplanarPlaneOrNull);
|
|
|
|
static RenderTarget renderPass(const Camera &cam,
|
|
const RenderContext &ctx,
|
|
int recursionDepth,
|
|
const Plane *excludeCoplanarPlaneOrNull) {
|
|
RenderTarget rt;
|
|
rt.color.reset(cam.imgW, cam.imgH, ctx.cfg.clearColor);
|
|
rt.depth.reset(cam.imgW, cam.imgH);
|
|
|
|
// Cache planar reflections per plane (per pass)
|
|
std::unordered_map<PlaneKey, MirrorCacheEntry, PlaneKeyHash> mirrorCache;
|
|
mirrorCache.reserve(64);
|
|
|
|
for (int ti = 0; ti < (int)ctx.tris->size(); ++ti) {
|
|
const Triangle &t = (*ctx.tris)[ti];
|
|
|
|
if (excludeCoplanarPlaneOrNull && triangleCoplanarWithPlane(t, *excludeCoplanarPlaneOrNull)) {
|
|
continue;
|
|
}
|
|
|
|
Vec2 s0, s1, s2;
|
|
float invZ0, invZ1, invZ2;
|
|
float z0, z1, z2;
|
|
|
|
bool ok0 = cam.projectToScreen(t.p0, s0, invZ0, z0);
|
|
bool ok1 = cam.projectToScreen(t.p1, s1, invZ1, z1);
|
|
bool ok2 = cam.projectToScreen(t.p2, s2, invZ2, z2);
|
|
|
|
// Minimal clipping: if any vertex behind near, skip triangle
|
|
if (!(ok0 && ok1 && ok2))
|
|
continue;
|
|
|
|
ScreenVertex sv[3];
|
|
sv[0] = { s0, invZ0, t.p0 };
|
|
sv[1] = { s1, invZ1, t.p1 };
|
|
sv[2] = { s2, invZ2, t.p2 };
|
|
|
|
float areaPx = triangleAreaPixels(sv[0].s, sv[1].s, sv[2].s);
|
|
|
|
float minXf = std::floor(std::min({ sv[0].s.x, sv[1].s.x, sv[2].s.x }));
|
|
float maxXf = std::ceil(std::max({ sv[0].s.x, sv[1].s.x, sv[2].s.x }));
|
|
float minYf = std::floor(std::min({ sv[0].s.y, sv[1].s.y, sv[2].s.y }));
|
|
float maxYf = std::ceil(std::max({ sv[0].s.y, sv[1].s.y, sv[2].s.y }));
|
|
|
|
int minX = std::clamp((int)minXf, 0, cam.imgW - 1);
|
|
int maxX = std::clamp((int)maxXf, 0, cam.imgW - 1);
|
|
int minY = std::clamp((int)minYf, 0, cam.imgH - 1);
|
|
int maxY = std::clamp((int)maxYf, 0, cam.imgH - 1);
|
|
|
|
float triArea = edgeFunction(sv[0].s, sv[1].s, sv[2].s);
|
|
if (std::abs(triArea) < kEps)
|
|
continue;
|
|
float invTriArea = 1.0f / triArea;
|
|
|
|
for (int y = minY; y <= maxY; ++y) {
|
|
for (int x = minX; x <= maxX; ++x) {
|
|
Vec2 p = { (float)x + 0.5f, (float)y + 0.5f };
|
|
|
|
float w0 = edgeFunction(sv[1].s, sv[2].s, p) * invTriArea;
|
|
float w1 = edgeFunction(sv[2].s, sv[0].s, p) * invTriArea;
|
|
float w2 = edgeFunction(sv[0].s, sv[1].s, p) * invTriArea;
|
|
|
|
if (w0 < -kEps || w1 < -kEps || w2 < -kEps)
|
|
continue;
|
|
|
|
// Perspective-correct interpolation: invZ at pixel
|
|
float invZ = w0 * sv[0].invZ + w1 * sv[1].invZ + w2 * sv[2].invZ;
|
|
if (invZ <= kEps)
|
|
continue;
|
|
float camZ = 1.0f / invZ;
|
|
|
|
float oldZ = rt.depth.get(x, y);
|
|
if (camZ >= oldZ)
|
|
continue;
|
|
|
|
// Interpolate world position perspective-correctly
|
|
Vec3 worldP = (w0 * sv[0].worldP * sv[0].invZ + w1 * sv[1].worldP * sv[1].invZ + w2 * sv[2].worldP * sv[2].invZ) / invZ;
|
|
|
|
rt.depth.set(x, y, camZ);
|
|
|
|
Vec3 outLinear { 0, 0, 0 };
|
|
|
|
if (t.mat.type == MaterialType::Diffuse) {
|
|
outLinear = shadeDiffuse(t, worldP, t.normal, cam, ctx);
|
|
} else {
|
|
float refl = std::clamp(t.mat.reflectance, 0.0f, 1.0f);
|
|
|
|
bool canRecurse = (refl > 0.0f) && (recursionDepth < ctx.cfg.maxDepth) && (areaPx >= ctx.cfg.minMirrorAreaPixels);
|
|
|
|
Vec3 base = shadeDiffuse(t, worldP, t.normal, cam, ctx);
|
|
|
|
if (!canRecurse) {
|
|
outLinear = base;
|
|
} else {
|
|
Plane pl = orientedPlaneTowardCamera(t, cam);
|
|
PlaneKey key = makePlaneKey(pl);
|
|
|
|
auto it = mirrorCache.find(key);
|
|
if (it == mirrorCache.end()) {
|
|
MirrorCacheEntry entry;
|
|
entry.plane = pl;
|
|
entry.mirrorCam = mirrorCameraAcrossPlane(cam, pl);
|
|
|
|
// Render mirrored pass, excluding this mirror plane (avoid self)
|
|
entry.rt = renderPass(entry.mirrorCam, ctx, recursionDepth + 1, &entry.plane);
|
|
entry.ready = true;
|
|
|
|
it = mirrorCache.emplace(key, std::move(entry)).first;
|
|
}
|
|
|
|
const MirrorCacheEntry &entry = it->second;
|
|
|
|
Vec2 rxy;
|
|
float rinvZ, rcamZ;
|
|
if (entry.ready && entry.mirrorCam.projectToScreen(worldP, rxy, rinvZ, rcamZ)) {
|
|
float u = rxy.x / (float)entry.rt.color.w;
|
|
float v = rxy.y / (float)entry.rt.color.h;
|
|
|
|
if (u >= 0.0f && u <= 1.0f && v >= 0.0f && v <= 1.0f) {
|
|
Vec3 sample = entry.rt.color.sampleNearest(u, v);
|
|
// Ideal mirror: mostly reflection; keep tiny base to avoid "reflection outside screen => black"
|
|
float baseKeep = 0.08f;
|
|
outLinear = sample * refl + base * (1.0f - refl);
|
|
outLinear = outLinear + baseKeep * base;
|
|
} else {
|
|
outLinear = base;
|
|
}
|
|
} else {
|
|
outLinear = base;
|
|
}
|
|
}
|
|
}
|
|
|
|
rt.color.set(x, y, outLinear);
|
|
}
|
|
}
|
|
}
|
|
|
|
return rt;
|
|
}
|
|
|
|
// --- Scene building helpers ---
|
|
|
|
static void addQuad(std::vector<Triangle> &tris,
|
|
const Vec3 &a, const Vec3 &b, const Vec3 &c, const Vec3 &d,
|
|
const Material &mat,
|
|
bool ccw = true) {
|
|
Triangle t1, t2;
|
|
if (ccw) {
|
|
t1.p0 = a;
|
|
t1.p1 = b;
|
|
t1.p2 = c;
|
|
t2.p0 = a;
|
|
t2.p1 = c;
|
|
t2.p2 = d;
|
|
} else {
|
|
t1.p0 = a;
|
|
t1.p1 = c;
|
|
t1.p2 = b;
|
|
t2.p0 = a;
|
|
t2.p1 = d;
|
|
t2.p2 = c;
|
|
}
|
|
t1.mat = mat;
|
|
t2.mat = mat;
|
|
t1.normal = computeFlatNormal(t1);
|
|
t2.normal = computeFlatNormal(t2);
|
|
tris.push_back(t1);
|
|
tris.push_back(t2);
|
|
}
|
|
|
|
static void addBox(std::vector<Triangle> &tris,
|
|
const Vec3 &mn, const Vec3 &mx,
|
|
const Material &mat) {
|
|
Vec3 p000 { mn.x, mn.y, mn.z };
|
|
Vec3 p001 { mn.x, mn.y, mx.z };
|
|
Vec3 p010 { mn.x, mx.y, mn.z };
|
|
Vec3 p011 { mn.x, mx.y, mx.z };
|
|
Vec3 p100 { mx.x, mn.y, mn.z };
|
|
Vec3 p101 { mx.x, mn.y, mx.z };
|
|
Vec3 p110 { mx.x, mx.y, mn.z };
|
|
Vec3 p111 { mx.x, mx.y, mx.z };
|
|
|
|
addQuad(tris, p000, p001, p011, p010, mat, true); // -X
|
|
addQuad(tris, p100, p110, p111, p101, mat, true); // +X
|
|
addQuad(tris, p000, p100, p101, p001, mat, true); // -Y
|
|
addQuad(tris, p010, p011, p111, p110, mat, true); // +Y
|
|
addQuad(tris, p000, p010, p110, p100, mat, true); // -Z
|
|
addQuad(tris, p001, p101, p111, p011, mat, true); // +Z
|
|
}
|
|
|
|
static std::vector<Triangle> buildCornellBoxScene() {
|
|
std::vector<Triangle> tris;
|
|
tris.reserve(256);
|
|
|
|
// Room: x in [-1,1], y in [0,2], z in [-1,1]; front is open (no wall at z=+1)
|
|
float xmin = -1.0f, xmax = 1.0f;
|
|
float ymin = 0.0f, ymax = 2.0f;
|
|
float zmin = -1.0f, zmax = 1.0f;
|
|
|
|
Material white { MaterialType::Diffuse, { 0.85f, 0.85f, 0.85f }, 0.0f };
|
|
Material red { MaterialType::Diffuse, { 0.85f, 0.20f, 0.20f }, 0.0f };
|
|
Material green { MaterialType::Diffuse, { 0.20f, 0.85f, 0.20f }, 0.0f };
|
|
|
|
// Floor (y=ymin)
|
|
addQuad(tris,
|
|
{ xmin, ymin, zmin }, { xmax, ymin, zmin }, { xmax, ymin, zmax }, { xmin, ymin, zmax },
|
|
white, true);
|
|
|
|
// Ceiling (y=ymax)
|
|
addQuad(tris,
|
|
{ xmin, ymax, zmax }, { xmax, ymax, zmax }, { xmax, ymax, zmin }, { xmin, ymax, zmin },
|
|
white, true);
|
|
|
|
// Back wall (z=zmin)
|
|
addQuad(tris,
|
|
{ xmin, ymin, zmin }, { xmin, ymax, zmin }, { xmax, ymax, zmin }, { xmax, ymin, zmin },
|
|
white, true);
|
|
|
|
// Left wall (x=xmin)
|
|
addQuad(tris,
|
|
{ xmin, ymin, zmax }, { xmin, ymax, zmax }, { xmin, ymax, zmin }, { xmin, ymin, zmin },
|
|
red, true);
|
|
|
|
// Right wall (x=xmax)
|
|
addQuad(tris,
|
|
{ xmax, ymin, zmin }, { xmax, ymax, zmin }, { xmax, ymax, zmax }, { xmax, ymin, zmax },
|
|
green, true);
|
|
|
|
// Two metal boxes
|
|
Material blueMetal { MaterialType::Metal, { 0.10f, 0.20f, 0.95f }, 1.0f };
|
|
Material yellowMetal { MaterialType::Metal, { 0.95f, 0.85f, 0.10f }, 1.0f };
|
|
|
|
addBox(tris,
|
|
Vec3 { -0.75f, 0.0f, -0.20f },
|
|
Vec3 { -0.25f, 0.65f, 0.35f },
|
|
blueMetal);
|
|
|
|
addBox(tris,
|
|
Vec3 { 0.20f, 0.0f, -0.70f },
|
|
Vec3 { 0.75f, 1.05f, -0.10f },
|
|
yellowMetal);
|
|
|
|
for (auto &t : tris)
|
|
t.normal = computeFlatNormal(t);
|
|
return tris;
|
|
}
|
|
|
|
int main() {
|
|
const int W = 520;
|
|
const int H = 520;
|
|
|
|
std::vector<Triangle> scene = buildCornellBoxScene();
|
|
|
|
// Keep camera outside-ish but close; perspective should be obvious
|
|
Camera cam = Camera::lookAt(
|
|
/*eye*/ Vec3 { 0.0f, 1.0f, 2.7f },
|
|
/*target*/ Vec3 { 0.0f, 1.0f, 0.0f },
|
|
/*up*/ Vec3 { 0.0f, 1.0f, 0.0f },
|
|
/*fov*/ 55.0f,
|
|
/*near*/ 0.05f,
|
|
/*far*/ 50.0f,
|
|
/*w,h*/ W, H);
|
|
|
|
RenderContext ctx;
|
|
ctx.tris = &scene;
|
|
|
|
ctx.cfg.maxDepth = 2;
|
|
ctx.cfg.minMirrorAreaPixels = 8.0f;
|
|
ctx.cfg.clearColor = { 0.0f, 0.0f, 0.0f };
|
|
|
|
// Light near ceiling center
|
|
ctx.lightPos = { 0.0f, 1.95f, -0.1f };
|
|
ctx.lightColor = { 1.0f, 1.0f, 1.0f };
|
|
ctx.lightIntensity = 30.0f;
|
|
ctx.ambient = { 0.06f, 0.06f, 0.06f };
|
|
|
|
RenderTarget rt = renderPass(cam, ctx, /*recDepth*/ 0, /*excludePlane*/ nullptr);
|
|
rt.color.writePPM_P3("output.ppm");
|
|
|
|
std::cout << "Wrote output.ppm (" << W << "x" << H << ")\n";
|
|
std::cout << "Convert if needed:\n";
|
|
std::cout << " magick output.ppm output.png\n";
|
|
return 0;
|
|
}
|