This project is a collaborative effort between:
- Natalie: Responsible for the
.rt
file parser and the implementation of lighting and shadows. - Alex: Focused on MiniLibX (graphical library) integration, ray-object intersections, the renderer, and this README.
-
Clone the repository and navigate into the project directory:
git clone https://github.com/Busedame/miniRT miniRT && cd miniRT
-
Build the project using the provided Makefile:
make
-
Follow the instructions provided in the output to run the program.
The Makefile
automatically detects your OS and selects the correct MiniLibX library for compilation.
The .rt
files define the elements and configurations for the scene to be rendered:
-
- Identifier:
A
- Ambient lighting ratio (brightness, range: [0.0, 1.0]):
0.2
- Color in RGB ([0-255]):
255, 255, 255
- Identifier:
-
- Identifier:
C
- Position (XYZ coordinates):
60, 0, 0
- Normalized orientation vector (range: [-1.0, 1.0]):
1.0, 0.0, 0.0
- Field of view (FOV) in degrees ([0-180]):
170
- Identifier:
-
- Identifier:
L
- Position (XYZ coordinates):
90, 50, 70
- Lighting ratio (brightness, range: [0.0, 1.0]):
0.7
- (Optional) Color in RGB ([0-255]):
255, 255, 255
(default: white)
- Identifier:
-
- Identifier:
pl
- Position (XYZ coordinates) of a point on the plane:
0.0, 0.0, -500.0
- Normalized orientation vector (range: [-1, 1]):
-1.0, 1.0, 1.0
- Color in RGB ([0-255]):
0, 0, 225
- Identifier:
-
- Identifier:
sp
- Position (XYZ coordinates) of the center:
90, 0.0, 0.0
- Diameter:
60
- Color in RGB ([0-255]):
0, 0, 255
- Identifier:
-
- Identifier:
cy
- Position (XYZ coordinates) of the center:
90.0, 0.0, 0.0
- Normalized orientation vector (axis, range: [-1, 1]):
0.0, 1.0, 1.0
- Diameter:
30
- Height:
210.42
- Color in RGB ([0-255]):
0, 0, 255
- Identifier:
This section outlines the mathematical approach to detecting intersections between rays and various geometric objects. While this overview document was not directly used for deriving all the mathematical formulations and functions presented here, it provides an excellent summary of the fundamental concepts and calculations.
A ray is represented as:
Where:
-
$P(t)$ : The point on the ray at distance$(t)$ from the ray's origin. It represents a location along the path defined by the ray, calculated by moving from the ray's starting point in the direction of the ray's direction vector. -
$O$ : The ray's origin in 3D space. This point marks the location where the ray begins its journey through space (camera). -
$\vec{D}$ : The normalized direction vector of the ray. A normalized vector has a magnitude (or length) of 1, ensuring that the scalar$(t)$ directly corresponds to the distance traveled along the ray. -
$t$ : A scalar value indicating the distance along the ray. It scales the direction vector, determining how far along the ray the point$P(t)$ is. When the direction vector is normalized, the value of$(t)$ directly represents the magnitude of the distance from the ray’s origin.
To find the intersection of a ray with a plane, we use the plane equation:
Where:
-
$P$ : Is any point on the plane. -
$P_0$ Is a known point on the plane -
$\vec{N} $ : The normal vector of the plane, which is perpendicular to the surface.
Substitute the ray equation
Rearrange terms:
Solve for t:
- (
$t$ ) will be positive if the denominator$(\vec{D} \cdot \vec{N} )$ is positive, meaning that the ray is moving towards the plane. The ray will intersect the plane in front of the camera. - (
$t$ ) will be negative if the denominator$(\vec{D} \cdot \vec{N})$ is negative, meaning that the ray is moving away from the plane. The ray will intersect the behind the camera. - If the denominator
$(\vec{D} \cdot \vec{N} )$ is zero (t is undefined or infinite), it means the ray is parallel to the plane and does not intersect it.
In the function, we first check if the ray is not parallel to the plane (t exists or is defined). If the ray is not parallel, we then check if the intersection happens in front of the camera (t is positive). The function returns successfully only if the intersection occurs in front of the camera.
/**
Function to find the intersection of a ray with a plane.
@param ray_origin The starting point of the ray (3D vector).
@param ray_dir The normalized direction vector of the ray.
@param plane Pointer to the plane structure.
@param t A pointer to store the distance to the intersection point (if found).
@return `1` if an intersection is found in the FOV (and `t` is set to the
intersection distance);
`0` if there is no intersection within the FOV (ray is parallel to the
plane or intersection behind the camera).
@note
Due to floating-point precision limitations, directly comparing a dot product to zero can be
unreliable. A small threshold (1e-6) is used to determine if the ray is parallel to the plane.
Values below this threshold are considered too close to zero, indicating parallelism or
preventing division by very small numbers, which could lead to inaccuracies.
*/
int ray_intersect_plane(t_vec3 ray_origin, t_vec3 ray_dir, t_plane *plane, double *t)
{
double denom; // Dot product of ray direction and plane normal
t_vec3 difference; // Vector from ray origin to a point on the plane
// Compute the denominator of the intersection equation
denom = vec3_dot(ray_dir, plane->normal);
// Check if the ray is not parallel to the plane (denom > small threshold)
if (fabs(denom) > 1e-6)
{
// Compute the vector from ray origin to a point on the plane
difference = vec3_sub(plane->point_in_plane, ray_origin);
// Calculate the intersection distance along the ray
*t = vec3_dot(difference, plane->normal) / denom;
// If the intersection distance is non-negative, the intersection is valid
if (*t >= 0.0)
return (1);
}
return (0); // No valid intersection is found
}
Intersection calculations with geometric objects like spheres and cylinders can be solved using quadratic equations. A quadratic equation has the general form:
Where:
-
$x$ : The unknown variable we are solving for. -
$a$ ,$b$ ,$c$ : The known coefficients of the equation (quadratic, linear, and constant, respectively).
The general solution to a quadratic equation is given by the quadratic formula:
For a detailed derivation of the quadratic formula, please refer to ChiliMath Quadratic Formula Derivation.
In context of the miniRT project, calculating intersections with objects like spheres or cylinders involves solving a quadratic equation of the form
which solves into
Where:
-
$t$ : The unknown variable representing the distance from the ray's origin to the intersection points. -
$a$ ,$b$ ,$c$ : Coefficients determined by the ray and object properties (e.g., direction vectors, centers, and radius).
The term under the root is called the discriminant (
-
Δ > 0: Two distinct real solutions (
$t_1$ and$t_2$ ):- The ray intersects the object at two points.
- These points correspond to entering and exiting the object.
-
Δ = 0: One real solution (
$t_1 = t_2$ ):- The ray is tangent to the object, touching it at a single point.
-
Δ < 0: No real solutions:
- The ray does not intersect the object.
/**
Calculates the discriminant of a quadratic equation `ax^2 + bx + c = 0`, which solves into
`x = (-b ± sqrt(b^2 - 4ac)) / 2a`.
The discriminant `D = b^2 - 4ac` determines the nature of the roots:
- if `D > 0`, there are two real roots (the ray intersects the object at two
points).
- if `D = 0`, there is one real root (the ray is tangent to the object, touching
it at one point).
- if `D < 0`, there are no real roots (the ray does not intersect the object).
@param a The coefficient of the quadratic term in quadratic equation.
@param b The coefficient of the linear term in quadratic equation.
@param c The constant term in quadratic equation.
@return The discriminant of the quadratic equation.
*/
double calculate_discriminant(double a, double b, double c)
{
double discriminant; // The value of the discriminant
// Calculate the discriminant using the formula D = b^2 - 4ac
discriminant = (b * b) - (4.0 * a * c);
return (discriminant); // Return the computed discriminant
}
The intersection distances
If (
To find where a ray intersects a sphere, we start with the general equation of the sphere:
Where:
-
$P$ : Is any point on the sphere's surface. -
$C$ : The center of the sphere. -
$r$ : The radius of the sphere.
Now, substitute the ray equation
Let
Expand the dot product:
Since
As explained above, this solves into:
Where the coefficients are:
$a = 1$ $b = 2(\vec{OC} \cdot \vec{D}$ )$c = (\vec{OC} \cdot \vec{OC}) - r^2$
The following function first checks if there are any real solutions for (
/**
Function to find the intersection of a ray with a sphere.
@param ray_origin The starting point of the ray in 3D space (vector).
@param ray_dir The normalized direction vector of the ray.
@param sphere Pointer to the sphere structure (contains center and radius).
@param t Pointer to store the distance to the first intersection point (if found);
could be the entry or exit point (if the ray is inside the sphere).
@return `1` if an intersection is found (and t is set to the
intersection distance);
`0` if there is no intersection.
@note `a = (ray_dir . ray_dir)` is 1.0 if the ray direction vector is normalized.
*/
int ray_intersect_sphere(t_vec3 ray_origin, t_vec3 ray_dir, t_sphere *sphere, double *t)
{
t_vec3 oc; // Vector from ray origin to sphere center
double b; // Linear coefficient in the quadratic equation
double c; // Constant coefficient in the quadratic equation
double discriminant; // Discriminant of the quadratic equation
// Compute vector from ray origin to sphere center
oc = vec3_sub(ray_origin, sphere->center);
// Compute coefficients for the quadratic equation
b = 2.0 * vec3_dot(oc, ray_dir);
c = vec3_dot(oc, oc) - (sphere->radius * sphere->radius);
// Calculate the discriminant to check for intersections
discriminant = calculate_discriminant(1.0, b, c);
// If the discriminant is negative, there are no real solutions (no intersection)
if (discriminant < 0.0)
return (0);
// Calculate the distance to the first intersection point (smallest root)
*t = calculate_entry_distance(1.0, b, discriminant);
// Check if the entry point is valid (distance must be non-negative)
if (*t >= 0.0)
return (1);
// Calculate the distance to the second intersection point (largest root)
*t = calculate_exit_distance(1.0, b, discriminant);
// Check if the exit point is valid (distance must be non-negative)
if (*t >= 0.0)
return (1);
return (0); // No valid intersection found
}
For a cylinder with:
- An axis passing through a reference point
$C=(C_x, C_y, C_z)$ , - Radius
$r$ , - And a normalized orientation vector
$\vec{U}$ , which represents the direction of the cylinder's axis,
The general equation for a point
Now define the vector from the reference point (
-
$(P_x - C_x)^2 + (P_y - C_y)^2 + (P_z - C_z)^2 = \Vert P \Vert^2$ : The square of the distance from the axis reference point$C$ to the surface point$P$ . -
$(\vec{P} - \vec{U})^2$ : The squared projection of$\vec{P}$ onto the axis direction$\vec{U}$ , which measures the component of$\vec{P}$ along the cylinder's axis. - Subtracting
$(\vec{P} - \vec{U})^2$ removes the contribution of$\vec{P}$ along the axis, leaving only the radial distance from the axis.
The cylinder’s surface is defined by ensuring the perpendicular (radial) distance from the axis equals the radius (
The parametric form of the ray equation is:
Substituting this into the cylinder equation results in:
which is the same as:
Let (
Expanding the two squared terms gives:
Expanding the squared terms of the second part of the equation gives:
Grouping all this into a quadratic form (
-
$a = (\vec{D} \cdot \vec{D}) - (\text{axis-dot-ray})^2$ -
$b = 2\left( (\vec{OC} \cdot \vec{D}) - (\text{axis-dot-oc} \times \text{axis-dot-ray})\right)$ -
$c = (\vec{OC} \cdot \vec{OC}) - (\text{axis-dot-oc})^2 - r^2$
The following function calculates the intersection of a ray with a cylinder using the above derivations.
/**
Function to find the intersection of a ray with a cylinder.
@param ray_origin The starting point of the ray in 3D space.
@param ray_dir The normalized direction vector of the ray.
@param cylinder Pointer to the cylinder structure.
@param t Pointer to store the distance to the first intersection point (if found);
could be the entry or exit point (if the ray starts inside the cylinder).
@return `1` if an intersection is found (and `t` is set to the intersection distance);
`0` if there is no intersection.
@note
This function determines intersections with an infinite cylinder surface. It does not
account for:
- The height bounds of the cylinder
- Intersection with the cylinder's end caps
*/
int ray_intersect_cylinder(t_vec3 ray_origin, t_vec3 ray_dir, t_cylinder *cylinder, double *t)
{
t_vec3 oc;
double axis_dot_ray;
double axis_dot_oc;
double a;
double b;
double c;
double discriminant;
// Compute the vector from ray origin to the cylinder center
oc = vec3_sub(ray_origin, cylinder->center);
// Compute the dot products
axis_dot_ray = vec3_dot(ray_dir, cyl->orientation);
axis_dot_oc = vec3_dot(oc, cyl->orientation);
// Compute coefficients of the quadratic equation:
a = vec3_dot(ray_dir, ray_dir) - (axis_dot_ray * axis_dot_ray);
b = 2 * (vec3_dot(oc, ray_dir) - (axis_dot_oc * axis_dot_ray));
c = vec3_dot(oc, oc) - (axis_dot_oc * axis_dot_oc) - (cylinder->radius * cylinder->radius)
discriminant = calculate_discriminant(a, b, c);
// If the discriminant is negative, no real solutions exist (no intersection)
if (discriminant < 0)
return (0);
// Calculate the entry distance along the ray (smallest root of the quadratic)
*t = calculate_entry_distance(cylinder->ixd.a, cylinder->ixd.b, cylinder->ixd.discriminant);
// Check if the entry point is valid (distance must be non-negative)
if (*t >= 0.0)
return (1);
// Calculate the exit distance along the ray (second root of the quadratic)
*t = calculate_exit_distance(cylinder->ixd.a, cylinder->ixd.b, cylinder->ixd.discriminant);
// Check if the exit point is valid (distance must be non-negative)
if (*t >= 0.0)
return (1);
return (0); // No valid intersection found
}
Please note that this function calculates the intersection of a ray with an infinite cylinder, not yet considering the cylinder's height and end caps. So far, it only detects intersections with the cylinder's lateral surface:
The blue and red objects are both infinite cylinders.
To account for the height boundaries of the cylinder, follow these steps:
-
Find the intersection point:
Use the ray equation with the calculated intersection distance ($t$ ) to find the intersection point ($P$ ):
- Compute vector from cylinder's center to intersection point:
-
Project this vector onto the cylinder's axis:
Find the component of ($\vec{V}$ ) along the cylinder's axis by projecting ($\vec{V}$ ) onto the normalized axis direction vector ($\vec{U}$ ):
- Compare the projection length to the height bounds:
The cylinder's height is split symmetrically around its center. If the projection length satisfies the condition below, then the intersection point is within the height bounds of the cylinder. Otherwise, it is outside the cylinder's finite height.
/**
Function to check whether a given intersection point on an infinite cylinder lies
within the cylinder's finite height bounds.
@param ray_origin The origin of the ray in 3D space.
@param ray_dir The normalized direction vector of the ray.
@param t The distance along the ray to the intersection point.
@param cylinder Pointer to the cylinder structure.
@return `1` if the intersection point lies within the cylinder's
height bounds;
`0` otherwise.
*/
static int check_cylinder_height(t_vec3 ray_origin, t_vec3 ray_dir, double t, t_cylinder *cylinder)
{
t_vec3 intersection_point; // The intersection point on the cylinder
t_vec3 center_to_point; // Vector from cylinder center to the intersection point
double projection_length; // Length of projection onto cylinder's orientation
double half_height; // Half of the cylinder's total height
// Compute the intersection point in 3D space
intersection_point = vec3_add(ray_origin, vec3_mult(ray_dir, t));
// Compute the vector from the cylinder's center to the intersection point
center_to_point = vec3_sub(intersection_point, cylinder->center);
// Project this vector onto the cylinder's orientation axis
projection_length = vec3_dot(center_to_point, cylinder->orientation);
// Compute half of the cylinder's height
half_height = cylinder->height / 2.0;
// Check if the projection falls within the cylinder's height bounds
if (projection_length >= -half_height && projection_length <= half_height)
return (1);
return (0); // The intersection point lies outside the height bounds
}
int ray_intersect_cylinder(t_vec3 ray_origin, t_vec3 ray_dir, t_cylinder *cylinder, double *t)
{
// [...] same as in `ray_intersect_cylinder()` above
// Check if the entry point is valid and lies within the cylinder's height bounds
if (*t >= 0.0 && check_cylinder_height(ray_origin, ray_dir, *t, cylinder))
return (1);
// Calculate the exit distance along the ray
*t = calculate_exit_distance(cylinder->ixd.a, cylinder->ixd.b, cylinder->ixd.discriminant);
// Check if the exit point is valid and lies within the cylinder's height bounds
if (*t >= 0.0 && check_cylinder_height(ray_origin, ray_dir, *t, cylinder))
return (1);
return (0); // No valid intersection found
}
The blue and red cylinders are finite in height but have no caps. Looking through the blue cylinder.
To account for the cylinder's end caps, the goal is to check if a ray intersects the circular regions at the top or bottom of the cylinder. These regions can be treated as planes with finite radii. The steps to determine an intersection with a cap are as follows:
-
Represent the cap as a plane:
Each cap is a circular disk on a plane perpendicular to the cylinder's axis. The plane equation for a cap is:$(P - C_{\text{cap}}) \cdot \vec{U} = 0$ Here:
-
$(P)$ is a point on the plane (we will test for the ray-cap intersection). -
$(C)$ is the center of the cap (top or bottom). -
$(\vec{U})$ is the normalized orientation vector of the cylinder's axis.
-
-
Find the ray-plane intersection:
Substitute the ray equation into the plane equation:$( O + t \vec{D} - C_\text{cap} ) \cdot \vec{U} = 0$ Where:
-
$(O)$ is the ray origin. -
$(\vec{D})$ is the normalized direction vector of the ray -
$(t)$ is the distance from$(O)$ to the intersection point.
Simplify:
$\left(\vec{OC}_\text{cap} \cdot \vec{U} + t(\vec{D} \cdot \vec{U}) \right) = 0$ Solve for
$t = - \frac{\vec{OC}_\text{cap} \cdot \vec{U}}{\vec{D} \cdot \vec{U}}$ -
-
Check the intersection point against the cap's radius:
Once ($t$ ) is computed, the intersection point$(P(t))$ can be calculated using the ray equation. The intersection point lies within the cap if the squared length of this vector is less than or equal to the squared radius of the cap:$\Vert P(t) - C_{\text{cap}} \Vert^2 \leq r^2$
/**
Function to check intersection with the cylinder's cap (top or bottom).
@param ray_origin The origin of the ray.
@param ray_dir The normalized direction vector of the ray.
@param cylinder Pointer to the cylinder structure.
@param t Pointer to store the intersection distance if valid.
@param flag_top Indicator for which cap to check:
- `0`: bottom cap
- otherwise: top cap.
@return `1` if the ray intersects the cap within its radius;
`0` otherwise.
*/
int ray_intersect_cap(t_vec3 ray_origin, t_vec3 ray_dir, t_cylinder *cyl, double *t, int flag_top)
{
t_vec3 cap_center; // Center of the cap being checked
t_vec3 cap_normal; // Normal vector of the cap
double denominator; // fraction's denominator: dot product of ray direction and cap normal;
double numerator // fraction's numerator: dot product of OC-vec and cap normal
double t_hit; // Distance to the intersection point (ray x cap) along the ray
t_vec3 p_hit; // Computed intersection point on the cap
t_vec3 difference; // Vector from cap center to intersection point
// Determine cap center and normal based on the flag
if (flag_top)
{
// Top cap: offset cylinder center by half its height along the orientation
cap_center = vec3_add(cyl->center, vec3_mult(cyl->orientation, cyl->height / 2.0));
cap_normal = cyl->orientation;
}
else
{
// Bottom cap: offset cylinder center by half its height in the opposite direction
cap_center = vec3_sub(cyl->center, vec3_mult(cyl->orientation, cyl->height / 2.0));
cap_normal = vec3_mult(cyl->orientation, -1.0);
}
// Compute the denominator of the intersection equation (projection of ray direction onto cap normal)
denominator = vec3_dot(ray_dir, cap_normal);
// If the denominator is near zero, the ray is parallel to the cap and cannot intersect
if (fabs(denominator) < 1e-6)
return (0);
// Calculate the distance t_cap to the intersection point on the cap plane
numerator = vec3_dot((vec3_sub(ray_origin, cap_center), cap_normal));
t_hit = - numerator / denominator;
// If the intersection is behind the ray's origin, discard it
if (t_hit < 0.0)
return (0);
// Compute the actual intersection point in 3D space
p_hit = vec3_add(ray_origin, vec3_mult(ray_dir, t_hit));
// Check if the intersection point lies within the cap's radius
difference = vec3_sub(p_hit, cap_center);
if (vec3_dot(difference, difference) <= (cyl->radius * cyl->radius))
{
// Valid intersection: store the distance and return success
*t = t_hit;
return (1);
}
return (0); // No valid intersection within the cap's radius
}
Looking at the end cap of the closed blue cylinder.
In the ray-object intersection detection functions above, several variables are independent of the ray's direction and remain constant for a given object (e.g., distance vectors such
as (
Precomputing these constants reduced my computation time by three-quarters for the simple scene shown in the figures above (1x plane, 2x spheres, 2x cylinders, 1440 x 900 resolution). This improvement is especially noticeable when using memory-checking tools such as Valgrind, reducing the compilation time from ~68 sec to ~18 sec.
A pinhole camera model can be used to describe how a 3D scene is projected onto a 2D screen (viewport). The pinhole model has the following properties
- Rays originate from the camera's position (the "eye") and pass through a virtual screen plane (the viewport).
- The field of view (FOV) defines the angular range visible to the camera, which determines the extent of the scene captured.
- The view frustum is a truncated pyramid extending from the camera's position toward the viewport. The rectangular screen at the base of the frustum defines the visible scene.
Pinhole camera model illustrating the FOV frustum and the rectangular screen for 2D projection (viewport).
Top: Camera's FOV viewed from above. Bottom: 2D projection onto the screen.
The Field of View (FOV) represents how much of the 3D scene is visible to the camera. Depending on the orientation of the camera, the FOV could be horizontal or vertical:
- The vertical FOV (
$\text{FOV}_v$ ) is the angle between the top and bottom edges of the view frustum. - The horizontal FOV (
$\text{FOV}_h$ ) is the angle between the left and right edges of the view frustum.
Vertical FOV is often the most common in graphics programming, but horizontal FOV can also be defined depending on the viewport dimensions.
We employ trigonometric functions, specifically the tangent function, to calculate how this FOV scales the projection from 3D space onto 2D screen space.
Imagine a right triangle formed by:
- The camera's position (the "eye") as the vertex.
- A point on the top edge of the screen as one endpoint.
- The center of the screen as the other endpoint.
The angle between the screen's center and the top edge of the frustum corresponds to
Using
The tangent function defines the "scaling factor" that maps world-space distances to screen-space distances, ensuring that closer objects appear larger and distant objects appear smaller.
Changing the FOV changes
In C, trigonometric functions expect their input angles to be in radians, not degrees. Therefore, the FOV angle is converted using the formula
The direction of a ray corresponding to a pixel on the viewport is calculated using normalized device coordinates. These calculations map the 2D screen space into 3D world-space rays.
Steps to Calculate Ray Direction:
- FOV Scaling Factor:
The tangent of half the vertical FOV defines how much the view scales with distance.
-
Normalization of Pixel Coordinates:
When projecting a 3D scene from world space to a 2D viewport, we need to map the 2D pixel coordinates of the screen to a common mathematical range known as normalized device coordinates (NDC). These coordinates range from -1 to 1 in both horizontal and vertical directions. This mapping allows the rendering process to operate independently of the actual screen resolution, making the projection consistent regardless of the screen size.We map these screen pixel positions (
x
,y
) to a range of [-1, 1] so they are consistent and independent of the screen's resolution:- Horizontal NDC Mapping:
norm_x = (2.0 * (x + 0.5) / WINDOW_W) - 1.0
ensures that the leftmost pixel maps to-1
and the rightmost pixel maps to1
. The term(x + 0.5)
ensures to center the mapping is at the pixel's center rather than at the pixel's edge (so the values fornorm_x
are close to but not exactly-1
and1
, differing by a small fraction). - Vertical NDC Mapping:
norm_y = (1.0 - (2.0 * (y + 0.5) / WINDOW_H))
, with the topmost pixel mapping to1
and bottommost one mapping to-1
. Similar to the horizontal case,(y + 0.5)
ensures the mapping is centered on the pixel.
- Horizontal NDC Mapping:
-
Aspect Ratio Adjustment:
The aspect ratio ensures that the spatial proportions of objects remain accurate across displays with different width-to-height ratios. Without this adjustment, objects might appear stretched or squished, especially on non-square screens.The aspect ratio is defined as
aspect_ratio = WINDOW_W / WINDOW_H
.In this implementation, the vertical FOV is used as the starting point for perspective projection calculations. This means that the vertical dimensions are already correctly scaled according to the screen height and FOV.
Thus, the aspect ratio is applied to the horizontal NDC calculation only:
norm_x = ((2.0 * (x + 0.5) / WINDOW_W) - 1.0) * aspect_ratio
-
Putting It All Together:
- Map pixel indices (
x
,y
) to the normalized device coordinate range [-1, 1]. - Adjust horizontal values by the aspect ratio to maintain spatial proportions for non-square displays.
- Scale both normalized x and y values by the field of view's scaling factor derived from the tangent of half the vertical FOV.
- Normalize these values to ensure they map correctly to 3D space for ray calculations.
- Map pixel indices (
/**
Compute the direction vector of a ray passing through a given pixel in the camera's view.
@param x The horizontal pixel coordinate on the screen.
@param y The vertical pixel coordinate on the screen.
@param cam The camera object containing the FOV in degrees.
@return The normalized direction vector of the ray in camera space.
@note
The z-component of the ray direction is conventionally set to 1.0
This positions the projection plane (or screen) at z = 1.0 in camera space, simplifying the
perspective projection calculations. The resulting vector is then normalized to ensure it has
a unit length, making it independent of the initial choice for the z-component.
*/
t_vec3 compute_ray_direction(int x, int y, t_cam cam)
{
double scale; // Scaling factor from the vertical FOV
double aspect_ratio; // Ratio of screen width to height
double norm_x; // Normalized x-coordinate in NDC
double norm_y; // Normalized y-coordinate in NDC
t_vec3 ray_dir; // Ray direction vector
scale = tan((cam.fov / 2) * M_PI / 180.0);
aspect_ratio = (double)WINDOW_W / (double)WINDOW_H;
// Map pixel coordinates to normalized device coordinates (NDC)
norm_x = ((2.0 * (x + 0.5) / WINDOW_W) - 1.0) * aspect_ratio * scale;
norm_y = (1.0 - (2.0 * (y + 0.5) / WINDOW_H)) * scale;
// Construct the direction vector in camera space
ray_dir.x = norm_x;
ray_dir.y = norm_y;
ray_dir.z = 1.0; // Pointing forward in camera space.
// Normalize the direction vector to ensure it has a unit length
return (vec3_norm(ray_dir));
}
In a ray-tracing system, the camera's orientation defines how the rays originating from the camera are aligned with the 3D scene. To compute ray directions in world space, you must transform the rays from camera space, where the z-axis points forward, to the orientation defined by the camera's position and rotation in the scene.
The camera's orientation in 3D space is defined by three mutually orthogonal vectors:
cam_right
: Points to the right of the camera's view (x-axis).cam_up
: Points upward from the camera's perspective (y-axis).cam_orientation
(provided by .rt file): Points forward along the camera's line of sight (z-axis).
These vectors form a basis for the camera's local coordinate system. To transform a direction vector from camera space to world space, you combine these basis vectors weighted by the direction's components in camera space.
-
Calculate the Ray Direction in Camera Space:
The ray direction in camera space is computed from the pixel's normalized coordinates and the field of view, as shown in the earlier function above:-
$\text{ray-cam-dir} = (x', y', z')$ , where:- (
$x'$ ): Scaled and aspect-ratio-adjusted horizontal NDC coordinate. - (
$y'$ ): Scaled vertical NDC coordinate. - (
$z'$ ): Always 1.0, pointing forward in camera space
- (
-
-
Transform the Ray Direction in Camera Space to World Space:
After calculating the ray's direction in camera space, we need to transform this direction into world space, where the entire 3D scene is defined. The formula for this transformation is:$\text{ray-world-dir} = (x' \times cam-right) + (y' \times cam-up) + (z' \times cam-orientation)$
-
Normalize the Resulting Vector: To ensure that the ray direction is a unit vector, normalize the resulting world-space vector.
This full implementation of the compute_ray_direction
function incorporates the camera orientation:
/**
Function to compute the ray direction for a given pixel in a camera's view,
considering the camera's field of view (FOV), aspect ratio, and orientation.
@param x The horizontal pixel coordinate on the screen.
@param y The vertical pixel coordinate on the screen.
@param cam The camera object containing the FOV, orientation vector, and position.
@return The normalized direction vector of the ray passing through the pixel, in world space.
*/
static t_vec3 compute_ray_direction(int x, int y, t_cam cam)
{
double scale; // Scaling factor from the vertical FOV
double aspect_ratio; // Ratio of screen width to height
double norm_x; // Normalized x-coordinate in NDC
double norm_y; // Normalized y-coordinate in NDC
t_vec3 cam_right; // The rightward direction vector of the camera in world space
t_vec3 cam_up; // The upward direction vector of the camera in world space
t_vec3 ray_cam_dir; // The direction vector of the ray in camera space
t_vec3 ray_world_dir; // The direction vector of the ray in world space
scale = tan((cam.fov / 2) * M_PI / 180.0);
aspect_ratio = (double)WINDOW_W / (double)WINDOW_H;
// Map pixel coordinates to normalized device coordinates (NDC)
norm_x = ((2.0 * (x + 0.5) / WINDOW_W) - 1.0) * aspect_ratio * scale;
norm_y = (1.0 - (2.0 * (y + 0.5) / WINDOW_H)) * scale;
// Ray direction in camera space
ray_cam_dir = vec3_new(norm_x, norm_y, 1.0);
// The cross product produces a vector that is orthogonal to both input vectors.
cam_right = vec3_norm(vec3_cross(vec3_new(0, 1, 0), scene->cam.ori));
cam_up = vec3_norm(vec3_cross(scene->cam.ori, scene->cam.right));
// Transform the ray direction in camera space to world space:
ray_world_dir = vec3_add(
vec3_add(
vec3_mult(cam.right, ray_cam_dir.x),
vec3_mult(cam.up, ray_cam_dir.y)),
vec3_mult(cam.ori, ray_cam_dir.z));
return (vec3_norm(ray_world_dir)); // Return normalized ray direction vector in world space
}