0
$\begingroup$

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);
}

My output is look like this this

and this and this

the output on the book is :like this

$\endgroup$

1 Answer 1

1
$\begingroup$

This code below can cause an 8bit overflow.

auto traceRay = [&](const point_t O, const vec3d D, const int min, const int max) -> color_t {

...

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};

Here values above 255 will wrap around to 0 as the upper bits are lost (e.g. 255 + 1 = 0x100, but as an 8 bit value it is 0x00). This is what is causing the banding in your image.

To fix this you should check if each element of closest_sphere.color * total_light is below the maximum uint8_t value before casting from double to uint8 occurs.

for example:

return color_t{uint8_t(std::min(closest_sphere.color.r * total_light, 255.0)),
                   uint8_t(std::min(closest_sphere.color.g * total_light, 255.0)),
                   uint8_t(std::min(closest_sphere.color.b * total_light, 255.0)), 255};
$\endgroup$
2
  • $\begingroup$ Based on your answer I clamped the value in [0,255] and the problem is solved. $\endgroup$
    – user15776
    Commented Feb 16, 2021 at 18:55
  • $\begingroup$ Clamping is the right answer :) Take a look at this section of the book: gabrielgambetta.com/computer-graphics-from-scratch/… $\endgroup$
    – ggambetta
    Commented Oct 23, 2022 at 7:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.