This project contains the following files (right-click files you'd like to download):
simpleshapes.cppraybox.cppgeometry.h//[header]
// A simple program that uses ray-tracing to render a scene made out of spheres
//[/header]
//[compile]
// Download the simpleshapes.cpp and geometry.h files to a folder.
// Open a shell/terminal, and run the following command where the files is saved:
//
// c++ -o simpleshapes simpleshapes.cpp -O3 -std=c++11 -DMAYA_STYLE
//
// Run with: ./simpleshapes. Open the file ./out.png in Photoshop or any program
// reading PPM files.
//[/compile]
//[ignore]
// Copyright (C) 2012 www.scratchapixel.com
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//[/ignore]
#include <cstdio>
#include <cstdlib>
#include <memory>
#include <vector>
#include <utility>
#include <cstdint>
#include <iostream>
#include <fstream>
#include <cmath>
#include <limits>
#include <random>
#include "geometry.h"
const float kInfinity = std::numeric_limits<float>::max();
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<> dis(0, 1);
inline
float clamp(const float &lo, const float &hi, const float &v)
{ return std::max(lo, std::min(hi, v)); }
inline
float deg2rad(const float °)
{ return deg * M_PI / 180; }
inline
Vec3f mix(const Vec3f &a, const Vec3f& b, const float &mixValue)
{ return a * (1 - mixValue) + b * mixValue; }
struct Options
{
uint32_t width;
uint32_t height;
float fov;
Matrix44f cameraToWorld;
};
// [comment]
// Object base class
// [/comment]
class Object
{
public:
Object() : color(dis(gen), dis(gen), dis(gen)) {}
virtual ~Object() {}
// Method to compute the intersection of the object with a ray
// Returns true if an intersection was found, false otherwise
// See method implementation in children class for details
virtual bool intersect(const Vec3f &, const Vec3f &, float &) const = 0;
// Method to compute the surface data such as normal and texture coordnates at the intersection point.
// See method implementation in children class for details
virtual void getSurfaceData(const Vec3f &, Vec3f &, Vec2f &) const = 0;
Vec3f color;
};
// [comment]
// Compute the roots of a quadratic equation
// [/comment]
bool solveQuadratic(const float &a, const float &b, const float &c, float &x0, float &x1)
{
float discr = b * b - 4 * a * c;
if (discr < 0) return false;
else if (discr == 0) {
x0 = x1 = - 0.5 * b / a;
}
else {
float q = (b > 0) ?
-0.5 * (b + sqrt(discr)) :
-0.5 * (b - sqrt(discr));
x0 = q / a;
x1 = c / q;
}
return true;
}
// [comment]
// Sphere class. A sphere type object
// [/comment]
class Sphere : public Object
{
public:
Sphere(const Vec3f &c, const float &r) : radius(r), radius2(r *r ), center(c) {}
// [comment]
// Ray-sphere intersection test
//
// \param orig is the ray origin
//
// \param dir is the ray direction
//
// \param[out] is the distance from the ray origin to the intersection point
//
// [/comment]
bool intersect(const Vec3f &orig, const Vec3f &dir, float &t) const
{
float t0, t1; // solutions for t if the ray intersects
#if 0
// geometric solution
Vec3f L = center - orig;
float tca = L.dotProduct(dir);
if (tca < 0) return false;
float d2 = L.dotProduct(L) - tca * tca;
if (d2 > radius2) return false;
float thc = sqrt(radius2 - d2);
t0 = tca - thc;
t1 = tca + thc;
#else
// analytic solution
Vec3f L = orig - center;
float a = dir.dotProduct(dir);
float b = 2 * dir.dotProduct(L);
float c = L.dotProduct(L) - radius2;
if (!solveQuadratic(a, b, c, t0, t1)) return false;
#endif
if (t0 > t1) std::swap(t0, t1);
if (t0 < 0) {
t0 = t1; // if t0 is negative, let's use t1 instead
if (t0 < 0) return false; // both t0 and t1 are negative
}
t = t0;
return true;
}
// [comment]
// Set surface data such as normal and texture coordinates at a given point on the surface
//
// \param Phit is the point ont the surface we want to get data on
//
// \param[out] Nhit is the normal at Phit
//
// \param[out] tex are the texture coordinates at Phit
//
// [/comment]
void getSurfaceData(const Vec3f &Phit, Vec3f &Nhit, Vec2f &tex) const
{
Nhit = Phit - center;
Nhit.normalize();
// In this particular case, the normal is simular to a point on a unit sphere
// centred around the origin. We can thus use the normal coordinates to compute
// the spherical coordinates of Phit.
// atan2 returns a value in the range [-pi, pi] and we need to remap it to range [0, 1]
// acosf returns a value in the range [0, pi] and we also need to remap it to the range [0, 1]
tex.x = (1 + atan2(Nhit.z, Nhit.x) / M_PI) * 0.5;
tex.y = acosf(Nhit.y) / M_PI;
}
float radius, radius2;
Vec3f center;
};
// [comment]
// Returns true if the ray intersects an object. The variable tNear is set to the closest intersection distance and hitObject
// is a pointer to the intersected object. The variable tNear is set to infinity and hitObject is set null if no intersection
// was found.
// [/comment]
bool trace(const Vec3f &orig, const Vec3f &dir, const std::vector<std::unique_ptr<Object>> &objects, float &tNear, const Object *&hitObject)
{
tNear = kInfinity;
std::vector<std::unique_ptr<Object>>::const_iterator iter = objects.begin();
for (; iter != objects.end(); ++iter) {
float t = kInfinity;
if ((*iter)->intersect(orig, dir, t) && t < tNear) {
hitObject = iter->get();
tNear = t;
}
}
return (hitObject != nullptr);
}
// [comment]
// Compute the color at the intersection point if any (returns background color otherwise)
// [/comment]
Vec3f castRay(
const Vec3f &orig, const Vec3f &dir,
const std::vector<std::unique_ptr<Object>> &objects)
{
Vec3f hitColor = 0;
const Object *hitObject = nullptr; // this is a pointer to the hit object
float t; // this is the intersection distance from the ray origin to the hit point
if (trace(orig, dir, objects, t, hitObject)) {
Vec3f Phit = orig + dir * t;
Vec3f Nhit;
Vec2f tex;
hitObject->getSurfaceData(Phit, Nhit, tex);
// Use the normal and texture coordinates to shade the hit point.
// The normal is used to compute a simple facing ratio and the texture coordinate
// to compute a basic checker board pattern
float scale = 4;
float pattern = (fmodf(tex.x * scale, 1) > 0.5) ^ (fmodf(tex.y * scale, 1) > 0.5);
hitColor = std::max(0.f, Nhit.dotProduct(-dir)) * mix(hitObject->color, hitObject->color * 0.8, pattern);
}
return hitColor;
}
// [comment]
// The main render function. This where we iterate over all pixels in the image, generate
// primary rays and cast these rays into the scene. The content of the framebuffer is
// saved to a file.
// [/comment]
void render(
const Options &options,
const std::vector<std::unique_ptr<Object>> &objects)
{
Vec3f *framebuffer = new Vec3f[options.width * options.height];
Vec3f *pix = framebuffer;
float scale = tan(deg2rad(options.fov * 0.5));
float imageAspectRatio = options.width / (float)options.height;
// [comment]
// Don't forget to transform the ray origin (which is also the camera origin
// by transforming the point with coordinates (0,0,0) to world-space using the
// camera-to-world matrix.
// [/comment]
Vec3f orig;
options.cameraToWorld.multVecMatrix(Vec3f(0), orig);
for (uint32_t j = 0; j < options.height; ++j) {
for (uint32_t i = 0; i < options.width; ++i) {
// [comment]
// Generate primary ray direction. Compute the x and y position
// of the ray in screen space. This gives a point on the image plane
// at z=1. From there, we simply compute the direction by normalized
// the resulting vec3f variable. This is similar to taking the vector
// between the point on the image plane and the camera origin, which
// in camera space is (0,0,0):
//
// ray.dir = normalize(Vec3f(x,y,-1) - Vec3f(0));
// [/comment]
#ifdef MAYA_STYLE
float x = (2 * (i + 0.5) / (float)options.width - 1) * scale;
float y = (1 - 2 * (j + 0.5) / (float)options.height) * scale * 1 / imageAspectRatio;
#elif
float x = (2 * (i + 0.5) / (float)options.width - 1) * imageAspectRatio * scale;
float y = (1 - 2 * (j + 0.5) / (float)options.height) * scale;
#endif
// [comment]
// Don't forget to transform the ray direction using the camera-to-world matrix.
// [/comment]
Vec3f dir;
options.cameraToWorld.multDirMatrix(Vec3f(x, y, -1), dir);
dir.normalize();
*(pix++) = castRay(orig, dir, objects);
}
}
// Save result to a PPM image (keep these flags if you compile under Windows)
std::ofstream ofs("./out.ppm", std::ios::out | std::ios::binary);
ofs << "P6\n" << options.width << " " << options.height << "\n255\n";
for (uint32_t i = 0; i < options.height * options.width; ++i) {
char r = (char)(255 * clamp(0, 1, framebuffer[i].x));
char g = (char)(255 * clamp(0, 1, framebuffer[i].y));
char b = (char)(255 * clamp(0, 1, framebuffer[i].z));
ofs << r << g << b;
}
ofs.close();
delete [] framebuffer;
}
// [comment]
// In the main function of the program, we create the scene (create objects)
// as well as set the options for the render (image widht and height etc.).
// We then call the render function().
// [/comment]
int main(int argc, char **argv)
{
// creating the scene (adding objects and lights)
std::vector<std::unique_ptr<Object>> objects;
// generate a scene made of random spheres
uint32_t numSpheres = 32;
gen.seed(0);
for (uint32_t i = 0; i < numSpheres; ++i) {
Vec3f randPos((0.5 - dis(gen)) * 10, (0.5 - dis(gen)) * 10, (0.5 + dis(gen) * 10));
float randRadius = (0.5 + dis(gen) * 0.5);
objects.push_back(std::unique_ptr<Object>(new Sphere(randPos, randRadius)));
}
// setting up options
Options options;
options.width = 640;
options.height = 480;
options.fov = 51.52;
options.cameraToWorld = Matrix44f(0.945519, 0, -0.325569, 0, -0.179534, 0.834209, -0.521403, 0, 0.271593, 0.551447, 0.78876, 0, 4.208271, 8.374532, 17.932925, 1);
// finally, render
render(options, objects);
return 0;
}