|
Path Tracing
An Introduction with Examples in C++ |
|
Prof. David Bernstein |
| Computer Science Department |
| bernstdh@jmu.edu |
Ray class, an empty (for semantics
only) Material interface, a Shape3D
interface, an Intersection class,
a CompositeShape (in the sense of the composite
pattern) class, some classes that implement
the Shape3D interface, and a Light
classPathTracingMaterial interfaceRayTracingMaterial
interfacePathTracer
Light
PathTracingMaterial Interface#ifndef edu_jmu_cs_PathTracingMaterial_h
#define edu_jmu_cs_PathTracingMaterial_h
#include "../Intersection.h"
#include "../Material.h"
#include "../Ray.h"
#include <glm/glm.hpp>
/**
* Requirements of a PathTracingMaterial.
*
* @author Prof. David Bernstein, James Madison University
*/
class PathTracingMaterial: public Material {
public:
/**
* Propagate the given Ray.
*
* @param ray The Ray to propagate
* @param intersection The intersection of the Ray and the Shape3D
* @param attenuation The (outbound) attenuation
* @param propagated The (outbound) propagated Ray
* @retun true if propagation occurs; false otherwise
*/
virtual bool propagate(const Ray& ray, const Intersection& intersection,
glm::vec3* attenuation, Ray* propagated) const = 0;
};
#endif
Matte Class
#ifndef edu_jmu_cs_Matte_h
#define edu_jmu_cs_Matte_h
#include "PathTracingMaterial.h"
/**
* An encapsulation of a path tracing Material that is a perfect diffuser
* and, hence, appears to be matte. It will appear the same from any angle.
*
* @author Prof. David Bernstein, James Madison University
*/
class Matte : public PathTracingMaterial {
private:
glm::vec3 albedo;
public:
/**
* Explicit Value Constructor.
*
* @param a The albedo
*/
explicit Matte(const glm::vec3& a);
/**
* Propogate the incoming Ray in a random direction.
*/
virtual bool propagate(const Ray& ray, const Intersection& intersection,
glm::vec3* attenuation, Ray* propagated) const;
};
#endif
Matte Class (cont.)
#include "Matte.h"
#include "../graphics3dUtilities.h"
Matte::Matte(const glm::vec3& a) : albedo(a) {
}
bool Matte::propagate(const Ray& ray, const Intersection& intersection,
glm::vec3* attenuation, Ray* propagated) const {
glm::vec3 target = intersection.p + intersection.n +
randomPointInUnitDisk();
*propagated = Ray(intersection.p, target - intersection.p);
*attenuation = albedo;
return true;
}
glm::vec3 randomPointInUnitDisk() {
//Generate a point at random and see if it is in the unit disk.
// If not, try again.
// Another approach is to use the spherical Fibonacci sequence
// as discussed in Sanger et al.
glm::vec3 p;
do {
p = 2.0f * (glm::vec3(static_cast<float>(drand48()),
static_cast<float>(drand48()), 0.0f)
- glm::vec3(1.0f, 1.0f, 0.0f));
} while (dot(p, p) >= 1.0f);
return p;
}
Glossy Class
#ifndef edu_jmu_cs_Glossy_h
#define edu_jmu_cs_Glossy_h
#include "PathTracingMaterial.h"
/**
* A Material that has specular reflections.
*
* @author Prof. David Bernstein, James Madison University
*/
class Glossy: public PathTracingMaterial {
private:
glm::vec3 albedo;
float fuzz;
public:
/**
* Explicit Value Constructor.
*
* @param a The albedo
* @param f The fuzz factir
*/
Glossy(const glm::vec3& a, float f);
/**
* Propogate the incoming Ray based on the angle of incidence.
*/
virtual bool propagate(const Ray& ray, const Intersection& intersection,
glm::vec3* attenuation, Ray* propagated) const;
};
#endif
Glossy Class (cont.)
#include "Glossy.h"
#include "../graphics3dUtilities.h"
Glossy::Glossy(const glm::vec3& a, float f) : albedo(a) {
if (f < 1.0f) fuzz = f;
else fuzz = 1.0f;
}
bool Glossy::propagate(const Ray& ray, const Intersection& intersection,
glm::vec3* attenuation, Ray* propagated) const {
glm::vec3 reflected = reflect(normalize(ray.d),
intersection.n);
*propagated = Ray(intersection.p,
reflected + (fuzz*randomPointInUnitDisk()));
*attenuation = albedo;
return (dot(propagated->d, intersection.n) > 0);
}
Transparent Class
#ifndef edu_jmu_cs_Transparent_h
#define edu_jmu_cs_Transparent_h
#include "PathTracingMaterial.h"
/**
* A Material that is Transparent and, hence, both reflects and
* transmits the ray (after refraction).
*
* @author Prof. David Bernstein, James Madison University
*/
class Transparent : public PathTracingMaterial {
public:
float refractionIndex;
/**
* Explicit Value Constructor.
*
* @param ri The index of refraction
*/
explicit Transparent(float ri);
/**
* Randomly either reflect the incoming Ray (based on the angle of
* incidence) or trasnmit it (based on the index of refraction).
*/
virtual bool propagate(const Ray& ray, const Intersection& intersection,
glm::vec3* attenuation, Ray* propagated) const;
};
#endif
Transparent Class (cont.)
#include "Transparent.h"
#include "../graphics3dUtilities.h"
#include <iostream>
Transparent::Transparent(float ri) : refractionIndex(ri) {
}
bool Transparent::propagate(const Ray& ray, const Intersection& intersection,
glm::vec3* attenuation, Ray* propagated) const {
*attenuation = glm::vec3(1.0f, 1.0f, 1.0f);
glm::vec3 reflected = reflect(ray.d, intersection.n);
glm::vec3 refracted;
float kappaR; // The proportion of the energy that is reflected
if (refract(ray.d, intersection.n, refractionIndex, &refracted))
fresnel(ray.d, intersection.n, refractionIndex, &kappaR);
else
kappaR = 1.0;
kappaR = clamp(0.0f, 1.0f, kappaR);
// In this case, treat kappaR as the probability that the Ray is reflected
if (static_cast<float>(drand48()) < kappaR)
*propagated = Ray(intersection.p, reflected);
else
*propagated = Ray(intersection.p, refracted);
return true;
}
bool refract(const glm::vec3 &I, const glm::vec3 &N, const float &ior,
glm::vec3* refracted) {
glm::vec3 i = normalize(I);
glm::vec3 n = N;
float etai = 1; // Assuming the ray comes from air
float etat = ior;
float cosi = clamp(-1, 1, dot(i, n));
if (cosi < 0) { // The ray is from the air to the object
cosi = -cosi;
} else { // The Ray is from the object to the air
std::swap(etai, etat);
n = -N;
}
float ratio = etai / etat;
float discriminant = 1 - ratio*ratio * (1 - cosi * cosi);
if (discriminant > 0.0f) {
*refracted = ratio * (i - cosi*n) - n * sqrtf(discriminant);
return true;
} else {
return false;
}
}
void fresnel(const glm::vec3 &I, const glm::vec3 &N,
const float &ior, float* kR) {
float cosi = clamp(-1, 1, dot(I, N));
float etai = 1, etat = ior;
if (cosi > 0) {
std::swap(etai, etat);
}
// Compute sini using Snell's law
float sint = etai / etat * sqrtf(std::max(0.f, 1 - cosi * cosi));
if (sint >= 1) { // Total internal reflection
*kR = 1;
} else {
float cost = sqrtf(std::max(0.f, 1 - sint * sint));
cosi = fabsf(cosi);
float Rs = ((etat * cosi) - (etai * cost))
/ ((etat * cosi) + (etai * cost));
float Rp = ((etai * cosi) - (etat * cost))
/ ((etai * cosi) + (etat * cost));
*kR = (Rs * Rs + Rp * Rp) / 2;
*kR = clamp(0.0f, 1.0f, *kR);
}
// kT = 1 - kR because of the conservation of energy
}
glm::vec3 lightIntensityAt(const glm::vec3 &p, const glm::vec3 n,
const Tracer* tracer) {
glm::vec3 lightAmt(0.0f);
glm::vec3 oShadow = p;
for (int i = 0; i < tracer->lightsSize; ++i) {
glm::vec3 l = tracer->light[i]->location - p;
float lightDistanceSquared = dot(l, l);
l = normalize(l);
float cosl = std::max(0.0f, dot(l, n));
// Determine if the point is in the shadow of a relevant object
Intersection shadowIntersection;
bool inShadow = tracer->scene->intersectsWith(Ray(oShadow, l),
0.001, MAXFLOAT, &shadowIntersection)
&& shadowIntersection.t * shadowIntersection.t < lightDistanceSquared;
if (!inShadow)
lightAmt += glm::vec3(1.0f) * tracer->light[i]->color * cosl;
}
return lightAmt;
}
glm::vec3 randomPointInUnitDisk() {
//Generate a point at random and see if it is in the unit disk.
// If not, try again.
// Another approach is to use the spherical Fibonacci sequence
// as discussed in Sanger et al.
glm::vec3 p;
do {
p = 2.0f * (glm::vec3(static_cast<float>(drand48()),
static_cast<float>(drand48()), 0.0f)
- glm::vec3(1.0f, 1.0f, 0.0f));
} while (dot(p, p) >= 1.0f);
return p;
}
/**
* Note: The orientation of l is reversed from some presentations.
*/
glm::vec3 reflect(const glm::vec3& l, const glm::vec3& n) {
return l - 2*dot(l, n)*n;
}
bool refract(const glm::vec3 &I, const glm::vec3 &N, const float &ior,
glm::vec3* refracted) {
glm::vec3 i = normalize(I);
glm::vec3 n = N;
float etai = 1; // Assuming the ray comes from air
float etat = ior;
float cosi = clamp(-1, 1, dot(i, n));
if (cosi < 0) { // The ray is from the air to the object
cosi = -cosi;
} else { // The Ray is from the object to the air
std::swap(etai, etat);
n = -N;
}
float ratio = etai / etat;
float discriminant = 1 - ratio*ratio * (1 - cosi * cosi);
if (discriminant > 0.0f) {
*refracted = ratio * (i - cosi*n) - n * sqrtf(discriminant);
return true;
} else {
return false;
}
}
float schlick(float cosine, float etat) {
// Assume the other material is air (with an IOR of 1)
float r0 = (1.0f - etat) / (1.0f + etat);
r0 = r0 * r0;
return r0 + (1.0f - r0)*pow((1.0f - cosine), 5.0f);
}
PathTracer Class
#ifndef edu_jmu_cs_PathTracer_h
#define edu_jmu_cs_PathTracer_h
#include "../Tracer.h"
/**
* An encapsulation of a simple PathTracer.
*
* @author Prof. David Bernstein, James Madison University
*/
class PathTracer: public Tracer {
private:
int ns;
public:
/**
* Explicit Value Constructor.
*
* @param s The scene
* @param l The lights
* @param ls The number of lights (only one is used)
* @param ms The maximum number of steps/propagations
* @param samples The number of samples
*/
PathTracer(Shape3D* s, Light** l, int ls, int ms, int samples);
virtual glm::vec3 trace(const Ray& r, int steps) const;
virtual void render(Camera cam, FrameBuffer *fb, int width, int height) const;
};
#endif
PathTracer Class (cont.)
#include "../graphics3dUtilities.h"
#include "PathTracer.h"
#include "PathTracingMaterial.h"
#include <iostream>
PathTracer::PathTracer(Shape3D* s, Light** l, int ls, int ms, int samples) {
scene = s;
light = l;
lightsSize = ls;
maxSteps = ms;
ns = samples;
}
glm::vec3 PathTracer::trace(const Ray& r, int steps) const {
Intersection intersection;
if (scene->intersectsWith(r, 0.001, MAXFLOAT, &intersection)) {
Ray propagated;
glm::vec3 attenuation;
PathTracingMaterial* material =
((PathTracingMaterial*)(intersection.material));
if (steps < maxSteps && material->propagate(r, intersection,
&attenuation, &propagated)) {
// Recurse
return attenuation * trace(propagated, steps + 1);
} else {
// Done with the recursion so return black
return glm::vec3(0.0f, 0.0f, 0.0f);
}
} else {
// Return the color of the Ray from the Light
glm::vec3 d = normalize(r.d);
float t = 0.5*(d.y + 1.0);
return (1.0f - t) * glm::vec3(1.0f, 1.0f, 1.0f) + t * light[0]->color;
}
}
void PathTracer::render(Camera cam, FrameBuffer *fb,
int width, int height) const {
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
glm::vec3 col(0.0f, 0.0f, 0.0f);
for (int sample=0; sample < ns; sample++) {
float s = static_cast<float>(i + static_cast<float>(drand48()))
/ static_cast<float>(width);
float t = static_cast<float>(j + static_cast<float>(drand48()))
/ static_cast<float>(height);
Ray r = cam.getRay(s, t);
col += trace(r, 0);
}
col /= static_cast<float>(ns);
col = glm::vec3(sqrt(col[0]), sqrt(col[1]), sqrt(col[2]));
int ir = clamp(0, 255, static_cast<int>(255.99*col[0]));
int ig = clamp(0, 255, static_cast<int>(255.99*col[1]));
int ib = clamp(0, 255, static_cast<int>(255.99*col[2]));
fb->setPixel(i, j, ir, ig, ib);
}
std::cout << "Row " << j << " of " << height << "\n";
}
}