Welcome to MiniRT
This project was made in collaboration with 🌈 Sarah Mclacke 🌈
MiniRT is about a ray-tracing engine that renders realistic
3D computer-generated images through ray-tracing techniques.
eVerYTinG bEloW tHiS liNe wAs ME gOiNg "hhhUUUUUhhhhhh?????????????" (aka. cat meme)
A ray tracer works a bit differently than your eye.
Ray tracing simulates how rays of light interact with objects to produce realistic images.
Unlike how our eyes work, which detect light coming from objects,
a ray-tracing algorithm traces rays from the camera (the "eye") into the scene,
determining which objects they hit and rendering the scene accordingly. "hUh??" moment
You get 3 main steps to building a ray tracer:
- Step 1) Calculate the ray from the “eye” through the pixel,
⇾ Set up your camera and calculate a ray per pixel (width x height). - Step 2) Determine which objects the ray intersects,
⇾ Calculate your intersection points with each object (save that information in a struct) - Step 3) Compute a color for the closest intersection point.
⇾ Calculate, with the Phong reflection model, your light per light source and shadow.
Step 1;
You get 2D grid, which only have x and y coordinates. Coordinates(x, y)
And you get a 3D grid, which has x, y and z coordinates. Coordinates(x, y, z)
x: left/right
⇾ x = right (positive numbers) | -x = left (negative numbers)y: up/down
⇾ y = right (positive numbers) | -y = left (negative numbers)z: forward/back forward
⇾ z = right (positive numbers) | -z = left (negative numbers)
Online 3D grid for visualizing: 3D-Graph
-
- Set up the width, height and image ratio of the window/image.
image_width and image_height you can choose yourself. (a nice size: WIDTH = 1800; and HEIGHT = 1900;)
image_ratio = image_height / image_width;
- Set up the width, height and image ratio of the window/image.
-
- Calculate the viewport, so what your imaginary camera sees.
pos_x and pos_y is at what pixel you are currently.
viewport_w = 2 * ((pos_x + 0.5) / image_width) - 1;
viewport_h = (1 - 2 * ((pos_y + 0.5) / image_height)) * image_ratio;
- Calculate the viewport, so what your imaginary camera sees.
-
- Calculate each delta pixel.
M_PI ⇾ This is a constant representing the value of pi. (M_PI = 3.14159265358979323846)
pixel_x = viewport_w * (tan((fov / 2) * (M_PI / 180)));
pixel_y = viewport_h * (tan((fov / 2) * (M_PI / 180)));
- Calculate each delta pixel.
-
- Initialize the ray vector and the ray place.
Make sure your camera vector works, in other words, make sure your camera can rotate.
We did it like this; it's not the most correct way, but it works (if it works, don't tough it)
- Initialize the ray vector and the ray place.
forward = normalize(camera.vector);
-------------------------------------
/* Camera vector(x,y,z)
* - If camera vector(0,1,0) -> (fabs(forward.y) == 1.0)
* - We give it init_ray_pos(1, 0, 0).
* - In anyother case it will stay init_ray_pos(0, 1, 0).
*/
if (fabs(forward.y) == 1.0)
right = normalize(cross_product(forward, init_ray_pos(1, 0, 0)));
else
right = normalize(cross_product(forward, init_ray_pos(0, 1, 0)));
-------------------------------------
up = cross_product(right, forward);
pixel_direction = plus(plus(scale_vector(right, screen.pixel_delta_x),
scale_vector(up, screen.pixel_delta_y)), forward);
return (normalize(pixel_direction));Step 2;
For intersecting objects, you get different options and examples online,
take the one that makes most sense for you.
Sphere, start with the sphere, it is the easiest. (Plenty examples online)
- Do camera, screen testing with the sphere:
○ Check for distortion at the edges of the screen.
○ Make sure you understand how your grid/position of objects works.
○ Make sure, if you have two spheres next to each other,
that they intersect correctly with each other.
Plane is easy as well.
- Make sure you understand that a plane is infinite.
○ Make sure if you change the vector that all vector directions work.
○ It is very important to make sure that the surface normal is correct;
otherwise later, the light might give you problems.
Cylinder is a b**ch!
- Cylinders consist out of two or three objects.
○ The body and the two caps.
○ The body ⇾ it will be in the beginning infinite,
so you need to cut/trim it at the correct height.
○ The caps are two planes, that are trimmed/cut as well.
○ Make sure the caps are perfectly on the body,
sometimes there is space in between cap and body,
you will see the space once you added light.
○ Make sure the surface normal of the caps is correct.
○ Make sure all vector directions work correctly (look correct)
Make sure you have all the objects, before adding light.
Make sure you take the object closest to the camera. (Or do this in step 3)
Step 3;
Look at the light and shadow sections.
Make sure you take the object closest to the camera. (If you haven't in step 2)
Check all surface normals, sometimes they cause problems with the light.
For example;
a Plane below a light source has the correct lighting, but a plane above a light source has no light at all.
If that happens: flip the surface normal of the plane above (make the surface normal positive), so it shows the light.
cyl->normal = mult_vecdub(cyl->vector, -1); // Surface normal points downwards -> (mult_vecdub() = multiplying a vector and a double)
cyl->normal = cyl->vector; // Surface normal points upwardsIf the checked sphere looks good from the center, check the sides; there will be distortion.
So rather calculate a checkerboard sphere that touches at the poles.
Or make a texture that rotates towards the camera, so it always looks good.
(A lot of "hUh??"moments)
- calculating the direction of light rays,
- determining the visibility of the light source from the surface point of the object. (Shadow part)
- and then applying illumination models such as Phong or Blinn-Phong to compute
the final colour of the object.
Determine the direction of light rays from the light source(s) to the surface point of the object.
If the light source is directional (like the sun) , you only need the direction vector.
If the light source is a point light, you'll need to calculate the direction vector from
the surface point to the light source position.
Determine if the surface point of the object is in shadow or not.
Apply an illumination model (such as Phong or Blinn-Phong) to compute
the final color of the object at the surface point.
This involves calculating ambient, diffuse, and specular components based on the surface properties,
light properties, and view direction.
You go through each object and each object goes through each light.
2 objects, 3 lights.
object1 will be caclulated with light1, light2, light3
then, object2 will be calculated with light1, light2, light3
while (++i < Number_of_objects)
{
while (++k < Number_of_lights)
{
- For each light we get:
- intersection_point,
- light_direction,
- current light information (You need the colour, position and ratio of the current light)
if (Shadow)
{
- Shadow -> visibility of the light source from the surface point of the object.
}
else
{
- Phong reflection model
- Ambient Light (General illumination in the scene)
- Diffuse Light (Direct light from the source)
- Specular Light (Highlights on shiny surfaces)
}
}
- Clamp Light result, so the rgb colours don't overflow, if it overflows it prints black.
}⭕ Make sure for your Phong reflection model that your base colour is calculated
before you do diffuse and spectular light.
Else your texture (Checkerboard) won't have light and shadow.
Determine if the surface point of the object is in shadow or not.
You can achieve this by casting a shadow ray from the surface point towards the light source
and checking if it intersects with any other object in the scene.
If it intersects, the surface point is in shadow; otherwise, it's light.
So, instead of calculating what makes logical sence, to calculate a ray from the light to the object,
you need to calculate a ray from the object to the light.More "hUh??"moments
If the ray (on the way to the light) hits another object, shadow will be drawn.
If the ray makes it susscesfully to the light, light/Illumination will be calculated.
Filed of view (FOV)
- between 70° and 90° is the sweet spot. Less or more will distort the image.
- Distortion will always happen near the edges of the image.
Normalizing a vector ⇾ Decimal numbers (0.0)
- Your number input (excluding rgb) need to take decimal numbers.
- Thats why we normalize our vectors.
- The normalization process ensures that the vectors have a consistent scale or magnitude,
which can be important in certain operations such as distance calculations, clustering, or classification. - So normalize every vector, either in the beginning (while parsing)
or every vector you entcounter durring your calculations.
Selecting objects
Selecting objects is not part of the project.
We just thought it's a nice and easy way for the user/evaluater.
to click on a object and do something with it.
- You can select all objects, but you can only do something with the sphere.
- If a sphere is selected you can press arrow key up
- It will then add the Checkerboard on the sphere.
- Add more textures to the sphere or give other objects textures aswell.
- Move and rotate objects.
- Make objects bigger or smaller.
you need mlx functions:
- mlx_key_hook(); -> is for key movement (ESC, Arrow up).
- mlx_mouse_hook(); -> is for mouse movement (selcet objects).🐭 Mouse movement (selcet objects):
Quick explanation how the mouse map works.
- There is a 2D mouse_map[x][y].
- While we loop through every pixel, we fill in this map.
- If no object is at that position fill in -1.
- If there is an object, give it a number (intersection point found).
Example:
Sphere1 = 0; (red)
Sphere2 = 1; (blue)
Plane = 2; (green)
mouse_map:
-1 -1 -1 -1 -1 ⬛⬛⬛⬛⬛
-1 -1 0 -1 -1 ⬛⬛🟥⬛⬛
-1 0 0 0 -1 ⬛🟥🟥🟥⬛
0 0 0 0 0 -> 🟥🟥🟥🟥🟥
-1 0 0 0 -1 ⬛🟥🟥🟥⬛
-1 -1 0 -1 2 ⬛⬛🟥⬛🟩
-1 -1 -1 2 2 ⬛⬛⬛🟩🟩- Find the position of the mouse, with mlx_get_mouse_pos();.
- If the mouse map is -1 at that position, so no object has been selected.
- Else if the mouse map is NOT -1 at that position. Highlight the object.
○ Just set/draw a white pixel with a bit of offset inwards next to the position. - For removeing the highlight, remove the colour of the pixel (set it to 0).
- How is that possible ☝️ ?
- Use mlx_image_to_window() to put a layer on top of the 'original'
○ It loads faster because it doesn't go through each pixel and recalculates everything. - That's why to remove the highlight, you can set that colour to 0, so it will just unset those pixels.
🗝️ Key movement (ESC, Arrow up):
🔴 - If the pressed key is 'ESC key', close and free window.
🟡 - If an object is selected and the key 'Arrow up' is pressed,
we change the pattern of that object.
🟢 - You need to count how many arrow-ups you have.
Because arrowUp-1 should change pattern, arrowUp-2 should change
to a different pattern or original form.
After that, you need to reset your arrow-up count
🔵 - Change the pattern of that object.
void ft_key_action(mlx_key_data_t keydata, t_data *data)
{
(void)keydata;
if (mlx_is_key_down(data->mlx, MLX_KEY_ESCAPE)) 🔴
mlx_close_window(data->mlx);
if (data->mouse.selected == true && mlx_is_key_down(data->mlx, MLX_KEY_UP)) 🟡
{
data->objs[data->i_am]->i++;
if (data->objs[data->i_am]->i > 1) 🟢
data->objs[data->i_am]->i = 0;
change_pattern(data, data->objs[data->i_am]); 🔵
}
}
-----------------------------------------------------------------------------------------------
void change_pattern()
{
- Check what type the object is : if (obj->type == E_SPHERE)
{
- if (obj->i == 1) -> is for Checkerboard. (obj->what_pattern = 1)
- else -> is for Normal. (obj->what_pattern = 0)
- go through each pixel again and redo calculations (🚩 I Don't recommand 🚩)
}
}🚩 : BECAUSE: you recalculate all pixels, it will take a while.
It would be wiser to use mlx_image_to_window()
to put a layer on top of the 'original'.
In the very beginning, initialize all pixels and store
that information for each pixel in a pixel struct (object, distance, colour, light, etc.)
Reuse that pixel struct, so you don't need to
recalculate everything.
It's also easier for if you want object rotation,
so the original information is saved, and you can access it fast.
About Colour
Use this webside you find you rgb colours 👉 RGB Color Picker 👈
You can pick your colour and it will give you the rgb number.
Check that your colour overlap is correct.
Just an example of `.rt` file
Minirt.rt file
#Ambient lighting (A):
#Identifier #Ratio #R,G,B
A 0.2 255,255,255
# --------------------------------------------------------------------------------------------
#Camera (C):
#Identifier #Coordinates #3D vector #FOV
C 0,0,-1 0,0,1 70
# --------------------------------------------------------------------------------------------
#Light (L):
#Identifier #Coordinates #Ratio #R,G,B
L 2,2,0 0.6 255,255,255
# --------------------------------------------------------------------------------------------
#Sphere (sp):
#Identifier #Coordinates #Diameter #R,G,B
sp 0,0,-10 12.6 10,0,255
# --------------------------------------------------------------------------------------------
#Plane (pl):
#Identifier #Coordinates #3D vector #R,G,B
pl 10,3,-10 0,1,0 0,0,225
# --------------------------------------------------------------------------------------------
#Cylinder (cy):
#Identifier #Coordinates #3D vector #Diameter #Height #R,G,B
cy -5,0,-5 0,0,1 14.2 21.42 10,0,255
# --------------------------------------------------------------------------------------------
#Triangle (tr) -> optional:
#Identifier #Coordinates1 #Coordinates2 #Coordinates3 #R,G,B
tr -7,6,-11 -7,-6,-11 10,0,-11 255,102,102
To execute the program, follow the steps below:
- Compile the RayTracer by running the following command:
$ make- Finally, execute the program using the following command:
$ ./miniRT scenes/sphere/sphere_circle_back.rtOR
$ make open-
Ray Tracing
-
C Library Math Functions
-
Shading & Lighting
-
Troubleshooting & Tips





