Cub3D

Introduction

Cub3D is the evolution of our so_long, and we will use the same functions:

  • Map filling.
  • Keyboard input handling.
  • On-screen image rendering.

Understanding Ray Casting

Ray casting is the foundation of Cub3D, enabling the creation of 3D environments using 2D techniques. It simulates the projection of rays from the player’s perspective to generate depth and perspective.

Key Concepts:

  1. Ray Projection: Transforms rays into visible walls.
  2. Texturing: Applies detail to surfaces using advanced mathematics.

Implementation Details

Beyond ray casting, the following are required:

  • Management of image buffers.
  • Texture coloring.

Environment Setup

Required Tools:

  • MinilibX library for graphical rendering.

Recommendation:
Use 42-CLI to simplify MLX installation (compatible with macOS and Linux).


The Mathematics of Ray Casting

Step 1: Ray Direction

This involves determining the ray’s angle relative to the player’s view and converting it into a unit vector.
(Calculating the ray’s unit vector based on the player’s position and orientation.)

int x = 0;
while (x < WIN_WIDTH) {
    double camera_x = 2 * x / (double)WIN_WIDTH - 1;
    double ray_dir_x = dir_x + plane_x * camera_x;
    double ray_dir_y = dir_y + plane_y * camera_x;
    // ...
}

Here, we calculate the ray’s direction based on the player’s direction (dir_x, dir_y), their plane (plane_x, plane_y), and the camera plane. The camera_x variable represents the ray’s x-coordinate in camera space, used to compute the ray’s direction vector.

Step 2: Delta Distance

Calculate the distance (delta) between consecutive grid intersections.
This is achieved by determining how far the ray must travel to reach the next grid line in the x or y direction.

double delta_dist_x = fabs(1 / ray_dir_x);
double delta_dist_y = fabs(1 / ray_dir_y);

This gives the distance the ray must travel to reach the next grid line in each direction. Note that pos_x and pos_y refer to the player’s position.

Step 3: Initial Step and Side Distances

Now, we need to calculate the initial side distances of the ray in the x and y directions. The variables step_x and step_y determine the direction in which the ray moves through the grid. The variables side_dist_x and side_dist_y initially represent the distance the ray must travel from its current position to the next grid line in the x or y direction. Later, these variables are updated with the delta distance as the ray traverses the grid.

if (ray_dir_x < 0) {
    step_x = -1;
    side_dist_x = (pos_x - map_x) * delta_dist_x;
} else {
    step_x = 1;
    side_dist_x = (map_x + 1.0 - pos_x) * delta_dist_x;
}
// ...

Step 4: Digital Differential Analysis (DDA)

The next step in the raycasting algorithm is performing a Digital Differential Analysis (DDA) to determine the distance to the next grid line in the x or y direction. This involves traversing the grid and calculating the distance to the next line in each direction. We also note the side of the wall we hit (0 for x, 1 for y). Once we hit a wall (defined here as ‘1’, but it can be defined otherwise), we exit the loop.

while (42) {
    if (side_dist_x < side_dist_y) {
        side_dist_x += delta_dist_x;
        map_x += step_x;
        side = 0;
    } else {
        side_dist_y += delta_dist_y;
        map_y += step_y;
        side = 1;
    }
    if (map[map_x][map_y] == '1') break;
}

Step 5: Wall Height

Calculate the wall height based on the distance to the wall. We use the wall_dist variable to determine the distance from the ray’s current position to the wall. Then, we compute the line height (line_height) on the screen based on this distance.

double wall_dist = (side == 0)
    ? (map_x - pos_x + (1 - step_x) / 2) / ray_dir_x
    : (map_y - pos_y + (1 - step_y) / 2) / ray_dir_y;

int line_height = (int)(WIN_HEIGHT / wall_dist);

Texture Handling

In the so_long project, we simply rendered images using MLX’s built-in function. However, since we’re now in a 3D world, we need to manually calculate which pixels are rendered. To do this, we can discard our textures after initialization, storing them in a buffer. The buffer will be an array of integers, where each integer represents a pixel’s color.

I found the best way to handle this is with the following data type:

Data Structure

Here’s a snippet of how you can update the pixel map and, more importantly, derive a pixel’s color from a texture.

#define NUM_TEXTURES 4
typedef struct s_data {
    int *texture_buffer[NUM_TEXTURES]; // Buffer for 64x64 textures
} t_data;

Imagine you have a 64x64 pixel texture. The size of texture_buffer[n] will be sizeof(int) * 64 * 64. To get a pixel, you can use the following formula: texture_buffer[n][y * 64 + x]. This skips y rows by multiplying the texture’s width and then adds x to get the pixel.

To retrieve a pixel value from an MLX image pointer, you need to use the mlx_get_data_addr function. You can access a pixel like this: img->addr[y * img->width + x]. I recommend reading the documentation for this function to understand how and why it works.

Pixel Access

We use a pixel map representing the pixels visible in the window at a 1:1 scale. Thus, right after projecting a ray and determining the wall height, we calculate each pixel for that ray. After performing ray projection, we can draw all non-zero values on the map. All zero values are drawn as the ceiling or floor color.

Note: This is not our solution. Below is the link to the explanation and calculations.

#define TEXTURE_SIZE 64

typedef enum e_cardinal_direction {
    NORTH = 0,
    SOUTH = 1,
    WEST = 2,
    EAST = 3
} t_cardinal_direction;

t_cardinal_direction dir;
int tex_x;
int color;
double pos;
double step;

dir = ft_get_cardinal_direction();
tex_x = (int)(wall_x * TEXTURE_SIZE);

if ((side == 0 && ray_dir_x < 0) || (side == 1 && ray_dir_y > 0))
    tex_x = TEXTURE_SIZE - tex_x - 1;

step = 1.0 * TEXTURE_SIZE / line_height;
pos = (draw_start - WIN_HEIGHT / 2 + line_height / 2) * step;

while (draw_start < draw_end) {
    // Here, you can calculate the pixel color from the texture
    int tex_y = (int)pos & (TEXTURE_SIZE - 1);
    pos += step;
    color = texture_buffer[dir][tex_y * TEXTURE_SIZE + tex_x];
    // Draw the pixel on the pixel map
    pixel_map[draw_start * WIN_WIDTH + x] = color;
    draw_start++;
}
color = texture_buffer[dir][y * 64 + x];

Efficient Rendering

Use mlx_get_data_addr to manipulate images directly:

t_img image;
image.img = mlx_new_image(mlx_ptr, WIN_WIDTH, WIN_HEIGHT);
image.addr = (int *)mlx_get_data_addr(image.img, &image.bpp, &image.line_length, &image.endian);
image.addr[y * (image.line_length / 4) + x] = 0x00FF00; // Assign green color

Performance Optimization

  • Avoid rendering individual pixels: Use image buffers.
  • Smooth movement: Implement adaptive speeds for different hardware.

Common Mistakes

  1. Misunderstood math: Difficult to debug.
  2. Non-continuous movement: Ensure keys maintain action.
  3. Fixed speeds: Adjust for system consistency.

Conclusion

Cub3D is a challenging yet rewarding project. Enjoy the process of building your 3D engine!

Additional Resources

Project Diagrams:

Diagrama de arquitectura