2

I've been trying to write a raytracer but I came across a problem when trying to implement simple diffuse calculations (trying to replicate the first ones from Ray Tracing in One Weekend but without a guide)

Here's the relevant code:

Intersection/diffuse calculations:

#pragma once

#include "Camera.h"
#include <cmath>
#include "Defs.h"

template<typename O>
class Sphere{
  O Radius;
  Point3<O> Center;
  Color<O> Col;

public:
  Sphere(O radius, Point3<O> center, Color<O> color);

    O quadratic(Ray<O> ray_in, O &disc, O t_Min, O t_Max);
    bool intersect(Ray<O> ray_in, rayInfo<O> &info, O t_Max);
};

template<typename O>
Sphere<O>::Sphere(O radius, Point3<O> center, Color<O> color) : Radius(radius), Center(center), Col(color) {}

template<typename O>
O Sphere<O>::quadratic(Ray<O> ray_in, O &disc, O t_Min, O t_Max){
  Point3<O> origin = ray_in.Origin;
  Vec3<O> direction = ray_in.Direction;

  Vec3<O> o = origin-Center;
  O a = direction.dot(direction);
  O b = 2 * direction.dot(o);
  O c = o.dot(o) - (Radius * Radius);
  O discriminant = b * b - 4 * (a * c);

  if (discriminant < 0){
      return false;
  }

  disc = ((-b - sqrt(discriminant)) / (2 * a));

  if (disc > t_Max || t_Min > disc){
      disc = ((-b + sqrt(discriminant)) / (2 * a));
      if (disc > t_Max || t_Min > disc){
          return false;
      }
  }

  return true;
}

template<typename O>
bool Sphere<O>::intersect(Ray<O> ray_in, rayInfo<O> &info, O t_Max){

    O disc;

    if (quadratic(ray_in, disc, info.Min, t_Max)){

        Point3<O> p = ray_in.at(disc);
        Vec3<O> normal = (p - Center) / Radius;

        info.Point = p;
        info.Normal = normal;
        info.front_face();
        info.Min = disc;

        return true;
    }
    else{
        return false;
    }
}

Tracer class:

#pragma once

#include <iostream>
#include "Shapes.h"
#include "Defs.h"
#include "Image.h"

template<typename O>
class Tracer{
  std::vector<Sphere<O>> Shapes;

public:
  Tracer(std::vector<Sphere<O>> shapes);
  void iterator(Ray<O> &ray, O &depth, O t_Max, O t_Min);

};

template<typename O>
Tracer<O>::Tracer(std::vector<Sphere<O>> shapes) : Shapes(shapes) {}

template<typename O>
void Tracer<O>::iterator(Ray<O> &ray, O &depth, O t_Max, O t_Min){

  O conc = 1;
  Color<O> col(0.4f, 0.8f, 0.9f);
  bool hit = false;

  rayInfo<O> info;
  info.Min = t_Min;

  if (depth <= 0)
      conc = 0;

  while (depth > 0){

    for (auto i = Shapes.begin(); i != Shapes.end(); i++){

      if (i->intersect(ray, info, t_Max)){
        conc *= 0.28;
        hit = true;
      }
    }

    if (!hit){
      break;
    }

    Vec3<O> circ = Vec3<O>::random_in_unit_sphere();
    Point3<O> target = info.Point + info.Normal + circ;
    ray = Ray<O>(info.Point, target - info.Point);
    info.Min = t_Min;
    hit = false;
    depth--;
  }

  col = col * conc;
  Image<O>::ColorPixel(std::cout, col);
}

And main just in case:

#include <iostream>
#include <cmath>
#include "../Matrix.h"
#include "Camera.h"
#include <vector>
#include "Image.h"
#include "Shapes.h"
#include "Tracer.h"
#include "../Defs.h"

template<typename O>
using Point3 = Vec3<O>;

template<typename O>
using Color = Vec3<O>;

int main(){
  const int img_width = 640;
  const int img_height = 480;
  const float img_ratio = img_width/img_height;
  float t_Max = infinity; float t_Min = 0.01; float depth = 50.0f;

  float inv_width = 1 / float(img_width);
  float inv_height = 1 / float(img_height);

  std::vector<Sphere<float>> shapes;

  Camera<float> cam1(40.0f, img_ratio, Point3<float>(0.0f, 0.0f, 0.0f), Point3<float>(0.0f, 0.0f, -1.0f), Vec3<float>(0.0f, 1.0f, 0.0f));

  Sphere<float> cir1(0.4f, Point3<float>(0.0f, 0.0f, -1.0f), Color<float>(0.7f, 0.3f, 0.2f));
  Sphere<float> cir2(3.0f, Point3<float>(0.0f, -3.0f, -1.0f), Color<float>(0.2f, 0.7f, 0.8f));
  Sphere<float> cir3(0.5f, Point3<float>(1.0f, 0.0f, -1.0f), Color<float>(0.2f, 0.3f, 0.7f));
  shapes.push_back(cir1);
  shapes.push_back(cir2);
  shapes.push_back(cir3);

  Tracer<float> tracer(shapes);

  std::cout << "P3\n" << img_width << ' ' << img_height << "\n255" << std::endl;

  Ray<float> ray(Point3<float>(0.0f), Vec3<float>(0.0f));

  for (int j = 0; j < img_height; j++)
  {
    std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush;
    for (int i = 0; i < img_width; i++){
        depth = 50.0f;
        float x = i;
        float y = j;

        cam1.screenCords(x, y, img_width, img_height);

        ray = cam1.get_raydir(x, y);
        //ray = Ray<float>(Vec3<float>(x1, y1, 1), Point3<float>(0.0f, 0.0f, 0.0f));
        tracer.iterator(ray, depth, t_Max, t_Min);
    }
  }
  std::cerr << "\n done " << std::endl;
}

Here's what it looks like right now:

enter image description here

Edit: When I attenuate outside the shapes loop (by moving `conc *= 0.28 outside of it) my image ends up looking something like this:

enter image description here

I can see something that looks like shadows but it's obviously not the intended behavior.

Edit 2:

As Yavok pointed out, setting info.Min to the vertex on every intersection hit is inverted logic. I should instead be decreasing info.Max so that the ray doesn't go all the way to objects further than the current closest.

I added anti-aliasing and gamma correction of 3 (cubic root) and the image looks much better now. A little strange still, but it's progress:

enter image description here

Edit 3:

It finally works! Turns out I had an error on my random_in_unit_sphere() function. It should look something like this:

static Vec3<T> random_in_unit_sphere(){
        bool flag = true;
        Vec3<T> p;
        while (flag){
            p = randomm(-1, 1);
            auto l = p.length();
            if (l * l < 1) { flag = false; }
        }

        return p;

    }

Thanks to Yakov and Spektre! Much appreciated.

2 Answers2

3

The noise is there because you randomize your diffuse rays:

Vec3<O> circ = Vec3<O>::random_in_unit_sphere();

Each time your ray hits something, you attenuate the color:

    conc *= 0.28;

Obviously, some rays are going to bounce more than others, and, accordingly get darker.

This noise is an expected artifact of any monte-carlo integrator. To reduce the noise you need to increase the number of samples per pixel and apply a de-noiser in the very end.

The "transparency" is there because you apply that attenuation within the intersection loop:

for (auto i = Shapes.begin(); i != Shapes.end(); i++){
  if (i->intersect(ray, info, t_Max)){
    conc *= 0.28; // <-- here
    hit = true;
  }
}

A ray that intersects multiple spheres will be attenuated multiple times, even by spheres that should be obscured. Instead you have to attenuate outside the loop, when you've figured out that your ray bounces:

if (!hit){
  break;
}
conc *= 0.28; // <-- apply attenuation once per bounce
Yakov Galka
  • 70,775
  • 16
  • 139
  • 220
  • Hi. When I attenuate outside the loop the transparency issue is solved but my image ends up looking like this: https://i.stack.imgur.com/YL09B.png – Tippitytopkek Feb 11 '21 at 04:14
  • 1
    @Tippitytopkek: I think you should be decreasing the `Max` instead of increasing the `Min`. Otherwise you're selecting the farther point along the ray instead of the closest one..?? – Yakov Galka Feb 11 '21 at 06:43
  • Ahh that makes sense I completely missed that Random ... – Spektre Feb 11 '21 at 08:03
  • @YakovGalka I don't quite get what you mean. I'm changing info.Min everytime the ray hits something. The only thing I'm decreasing is the depth to avoid a rays bouncing infinitely. – Tippitytopkek Feb 11 '21 at 17:05
  • 2
    @Tippitytopkek: you aren't just "changing info.Min", you are effectively increasing it with each intersection. `quadratic` reports a hit only if `into.Min < disc < t_Max`. `intersect` then updates `info.Min = disc`. But because `info.Min < disc` it increases `info.Min`. Now imagine that your ray intersected a nearby sphere at distance `disc = 10`. You set `info.Min = 10`. Next you check a farther sphere for the same ray, and get `disc = 20`. So `intersect` will set `info.Min = 20` and other members of `info` to describe that farther intersection point. Consequently the `Tracer` is going to use – Yakov Galka Feb 11 '21 at 23:42
  • 2
    ..the farthest of all the intersections, **instead of the nearest**. Your logic is basically backwards, and you should maintain a `Max` distance in the `info` struct and decrease *that*. – Yakov Galka Feb 11 '21 at 23:43
  • I see. That does make a lot more sense than what I was doing. I guess I didn't quite get everything from the book. So I just have to assign the int. to info.Max every iteration instead. I'll try that – Tippitytopkek Feb 12 '21 at 01:45
  • @YakovGalka I'm finally getting to see some proper shadows so that's awesome! The spheres still kind of look like they're transparent (either that or it's reflecting the bigger sphere which is not possible yet lol). It feels good to finally make some progress on this though https://i.stack.imgur.com/tkskX.png – Tippitytopkek Feb 12 '21 at 01:51
1

Too lazy to debug your code however the screenshot and just a quick look at source hints accuracy problems. So try to use 64bit doubles instead of 32 bit floats...

Intersection between ray and ellipsoid/sphere tend to be noisy on just floats... once refraction and reflection is added on top of that the noise multiplies ...

Also sometimes helps using relative coordinates instead of absolute ones (that can make a huge impact even on floats). For more info see:

Spektre
  • 49,595
  • 11
  • 110
  • 380
  • 1
    Thanks for your response. I used doubles but the noise didn't change at all, so it must be that it's caused by whatever is causing the transparency. All the calculations seem to check out every time I review them though. – Tippitytopkek Feb 10 '21 at 21:07