Ray Tracing
Overview
This project is made with a modular approach. There are two main components at play:
- Editor
- Engine
The editor
allows the designer to create a blueprint for the final scene. It generates a JSON
file, containing data, describing the scene which is passed to the engine
to do the actual heavy lifting. The engine
then generates the final render which later can be saved as file and is also rendered on a window.
editor
communicating with engine
Introduction
In computer graphics, ray tracing
is one of the rendering techniques where photorealistic images are generated by using techniques which model the physical light phenomena.
Components
There are 2 major components.
Editor
The designer can construct a blueprint for the final render using the editor
which generates a JSON
file, describing the scene. This project does NOT have any editor
yet but it is possible to make one and easily integrate it with the engine
since the interface is already implemented.
Engine
The engine
contains all the algorithms and behaviors which do the computational heavy lifting to construct an image based on the blueprint provided by the JSON
file.
The engine
is a composition of 3 components.
- Scene
- Camera
- Window Manager
Scene
Scene
1 is the structure itself which is constructed from the blueprint, provided by the JSON
configuration.
Camera
Camera
2 captures the scene.
Window Manager
Window Manager
3 manages the window on which the captured image is displayed.
Other than these components, engine
contains an additional flag called save_img
which determines if we want to save the output into rendered_img.png
or not.
bool save_img = false;
Engine
also includes a JSON
container which will contain the parsed data.
Json config_data;
void parseJson(std::string);
which is later used to initialize the 3 components.
void setupScene();
void setupWindow();
void setupCam();
Execution Flow
Basic flow of Engine::run()
void Engine::run() {
Hittable_list& HL_ref_scene = S_scene.create_scene();
sf::Image* I_ref_image = Cam_camera.render(HL_ref_scene);
if (save_img)
I_ref_image->saveToFile("./imgs/rendered_img.png");
WM_window.display(I_ref_image);
}
Objects and Materials
At the moment, there is only sphere
4 present in the project.
Sphere
There are 3 things common in all spheres
.4
- Center (a 3D
point
5) - Radius
- Material
Materials
Materials
determine the way light behaves when it hits the surface of a sphere
.4
Lambertian
For this material, we are reflecting the light into random directions based on the normal vector
and reducing light with each light bounce.6
bool scatter(const Ray& r_in, const Hit_record& rec, Color& attenuation, Ray& scattered) const override {
auto scatter_direction = rec.normal + random_unit_vector();
if (scatter_direction.near_zero())
scatter_direction = rec.normal;
scattered = Ray(rec.p, scatter_direction);
attenuation = albedo;
return true;
}
Metal
Get a perfect reflected ray
1 direction.
vec3 reflected = reflect(r_in.direction(), rec.normal);
fuzz
being applied to a reflected ray
.7
Get a random unit vector
6 \(\hat r\), scale it by fuzz
.
fuzz * random_unit_vector()
Then add it to the unit vector
6 of the reflected ray
.7
reflected = unit_vector(reflected) + (fuzz * random_unit_vector());
Then make a ray
7 out of it
scattered = ray(rec.p, reflected);
To make sure that the scattered ray
7 is projected outside of the surface after hitting it, we check if the dot product
6 is positive.
Error correction
return (dot(scattered.direction(), rec.normal) > 0);
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override {
vec3 reflected = reflect(r_in.direction(), rec.normal);
reflected = unit_vector(reflected) + (fuzz * random_unit_vector());
scattered = ray(rec.p, reflected);
attenuation = albedo;
return (dot(scattered.direction(), rec.normal) > 0);
}
Read functions
in utils to understand how reflection
and refraction
works.
Dielectric
The refractive index
of outside medium is \(\eta^\prime\) and inside one is \(\eta\).
In the main()
, the index is passed as
Therefore, if the ray
7 is coming from outside, ri
is
double ri = rec.front_face ? (1.0 / refraction_index) : refraction_index;
Then for unit vector
,6 we have
vec3 unit_direction = unit_vector(r_in.direction());
From the refract()
in utils, we know that
double cos_theta = std::fmin(dot(-unit_direction, rec.normal), 1.0);
Now using the identity
double sin_theta = std::sqrt(1.0 - cos_theta*cos_theta);
Light cannot refract
if
In that case, we reflect the light.
For reflection, we can also use Schlick's approximation
.
if (cannot_refract || reflectance(cos_theta, ri) > random_double())
direction = reflect(unit_direction, rec.normal);
else
direction = refract(unit_direction, rec.normal, ri);
Therefore, we have
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override {
attenuation = color(1.0, 1.0, 1.0);
double ri = rec.front_face ? (1.0/refraction_index) : refraction_index;
vec3 unit_direction = unit_vector(r_in.direction());
double cos_theta = std::fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = std::sqrt(1.0 - cos_theta*cos_theta);
bool cannot_refract = ri * sin_theta > 1.0;
vec3 direction;
if (cannot_refract || reflectance(cos_theta, ri) > random_double())
direction = reflect(unit_direction, rec.normal);
else
direction = refract(unit_direction, rec.normal, ri);
scattered = ray(rec.p, direction);
return true;
}