I'm very new to computer graphics and trying to implement a raytracer based on the book Computer Graphics from Scratch. When I tried to add specular reflection, I don't get the output that look like on the book. I don't know how to debug this, I mean running a debugger stepwise doesn't seems to helpful. I look at the code on the book, compare the formulas, they looks like the same. Kind apologies for the long code:
#include <cmath>
#include <cstdint>
#include <limits>
#include <utility>
#include <vector>
#define STBI_ONLY_PNG // decode only for PNG format
#define STBI_FAILURE_USERMSG // for better image load failure messages
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "3rdparty/stb/stb_image_write.h"
namespace {
struct point {
double x{}, y{}, z{};
};
struct vec3d {
double x{}, y{}, z{};
};
vec3d operator-(const point p, const point q) { return vec3d{p.x - q.x, p.y - q.y, p.z - q.z}; }
point operator+(const vec3d v, const point p) { return point{v.x + p.x, v.y + p.y, v.z + p.z}; }
vec3d operator+(const vec3d v, const vec3d w) { return vec3d{v.x + w.x, v.y + w.y, v.z + w.z}; }
vec3d operator-(const vec3d v, const vec3d w) { return vec3d{v.x - w.x, v.y - w.y, v.z - w.z}; }
vec3d operator-(const vec3d v) { return vec3d{-1.0 * v.x, -1.0 * v.y, -1.0 * v.z}; }
bool operator==(const vec3d v, const vec3d w) { return v.x == w.x && v.y == w.y && v.z == w.z; }
vec3d operator*(const double k, const vec3d v) { return vec3d{k * v.x, k * v.y, k * v.z}; }
double length(const vec3d v) { return sqrt(v.x * v.x + v.y * v.y + v.z * v.z); }
vec3d normalize(vec3d v) { return (1.0 / length(v)) * v; }
double dot(const vec3d v, const vec3d w) { return (v.x * w.x) + (v.y * w.y) + (v.z * w.z); }
vec3d cross(const vec3d v, const vec3d w) {
vec3d r;
r.x = v.y * w.z - v.z * w.y;
r.y = v.z * w.x - v.x * w.z;
r.z = v.x * w.y - v.y * w.x;
return r;
}
} // namespace
int main() {
using namespace std;
constexpr size_t canvas_width = 600;
constexpr size_t canvas_height = 600;
constexpr size_t channels = 4; // r,g,b,a
constexpr size_t viewport_width = 1;
constexpr size_t viewport_height = 1;
constexpr size_t viewport_distance = 1;
constexpr double minus_inf = numeric_limits<double>::min();
constexpr double plus_inf = numeric_limits<double>::max();
using canvas_t = uint8_t[canvas_width][canvas_height][channels];
using point_t = point;
struct color_t {
uint8_t r{}, g{}, b{}, a{};
};
enum class light_type { ambient, directional, point };
struct light {
light_type type;
double intensity;
point_t position;
};
struct sphere {
double radius;
point_t center;
color_t color;
int specular;
};
canvas_t canvas;
point_t O{0.0, 0.0, 0.0}; // camera position
const sphere sphere_1{1.0, point_t{0, -1, 3}, color_t{255, 0, 0, 255}, 500};
const sphere sphere_2{1.0, point_t{2, 0, 4}, color_t{255, 0, 255, 255}, 500};
const sphere sphere_3{1.0, point_t{-2, 0, 4}, color_t{0, 255, 0, 255}, 10};
const vector spheres{sphere_1, sphere_2, sphere_3};
const light light_1{light_type::ambient, 0.2};
const light light_2{light_type::point, 0.6, point_t{2, 1, 0}};
const light light_3{light_type::directional, 0.2, point_t{1, 4, 4}};
const vector lights{light_1, light_2, light_3};
auto canvas2viewport = [=](const double x, const double y) -> vec3d {
const double Vx = (x * viewport_width) / canvas_width;
const double Vy = (y * viewport_height) / canvas_height;
return vec3d{Vx, Vy, double(viewport_distance)};
};
auto computeLighting = [&](const point_t P, const vec3d N, const vec3d V, const int specular) {
double total_intensity = 0;
for (const auto light : lights) {
if (light.type == light_type::ambient) {
total_intensity += light.intensity;
} else {
vec3d L;
if (light.type == light_type::point) {
L = light.position - P;
} else {
L = light.position - point_t{0, 0, 0};
}
if (const double n_dot_l = dot(N, L); n_dot_l > 0) { // diffuse reflection
total_intensity += light.intensity * n_dot_l / (length(N) * length(L));
}
if (specular != -1) { // specular reflection
const vec3d R = (2 * dot(N, L) * N) - L;
if (const double r_dot_v = dot(R, V); r_dot_v > 0) {
total_intensity += light.intensity + pow((r_dot_v / (length(R) * length(V))), specular);
}
}
}
}
return total_intensity;
};
auto intersectRaySphere = [=](const point_t O, const vec3d D, const sphere s) -> auto {
const double r = s.radius;
const vec3d OC = O - s.center;
const double a = dot(D, D);
const double b = 2 * dot(OC, D);
const double c = dot(OC, OC) - r * r;
const double discriminant = b * b - 4 * a * c;
if (discriminant < 0) {
return make_pair(plus_inf, plus_inf);
}
const double t1 = (-b + sqrt(discriminant)) / (2 * a);
const double t2 = (-b - sqrt(discriminant)) / (2 * a);
return pair{t1, t2};
};
auto traceRay = [&](const point_t O, const vec3d D, const int min, const int max) -> color_t {
double closest_t = plus_inf;
sphere closest_sphere;
bool sphere_changed = false;
for (const sphere &sp : spheres) {
const auto [t1, t2] = intersectRaySphere(O, D, sp);
if ((t1 > min && t1 < max) && t1 < closest_t) {
closest_t = t1;
closest_sphere = sp;
sphere_changed = true;
}
if ((t2 > min && t2 < max) && t2 < closest_t) {
closest_t = t2;
closest_sphere = sp;
sphere_changed = true;
}
}
if (!sphere_changed) {
return color_t{255, 255, 255, 255}; // white
}
const point_t P = closest_t * D + O;
vec3d N = P - closest_sphere.center;
N = normalize(N);
const double total_light = computeLighting(P, N, -D, closest_sphere.specular);
return color_t{uint8_t(closest_sphere.color.r * total_light),
uint8_t(closest_sphere.color.g * total_light),
uint8_t(closest_sphere.color.b * total_light), 255};
};
// x in [-300, 300)
// y in [-300, 300)
auto put_pixel = [&](const int x, const int y, const color_t p) {
const size_t sx = (canvas_width / 2) + x;
const size_t sy = ((canvas_height / 2) - y) - 1;
if (sx < 0 || sx >= canvas_width || sy < 0 || sy >= canvas_height)
return;
canvas[sy][sx][0] = p.r;
canvas[sy][sx][1] = p.g;
canvas[sy][sx][2] = p.b;
canvas[sy][sx][3] = 255;
};
for (int y = -300; y < 300; ++y) {
for (int x = -300; x < 300; ++x) {
vec3d D = canvas2viewport(x, y);
color_t c = traceRay(O, D, 1, 100'000);
put_pixel(x, y, c);
}
}
stbi_write_png("specular_light.png", canvas_width, canvas_height, channels, canvas,
canvas_width * channels);
}