Transparent/noisy spheres when using simple diffuse calculations

Viewed 118

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 Answers

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

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:

Related