Cub3D
Développement d'un moteur de jeu 3D performant inspiré de Wolfenstein 3D, écrit entièrement en C. Implémentation manuelle d'un algorithme de Raycasting pour le rendu graphique, gestion des textures, des collisions et du parsing de cartes dynamiques.
Aperçu du projet
Cub3D
Le projet Cub3D constitue l'un des défis les plus techniques et enrichissants du cursus de l'école 42. Il ne s'agit pas simplement de développer un jeu, mais de concevoir de toutes pièces un moteur graphique capable de simuler un environnement tridimensionnel à partir d'une carte en deux dimensions. Inspiré par la technologie révolutionnaire de Wolfenstein 3D (1992), ce projet impose une contrainte forte : l'utilisation exclusive du langage C et d'une bibliothèque graphique minimaliste, la MiniLibX. Cette restriction force à comprendre et à réimplémenter les mécanismes fondamentaux du rendu graphique, là où des moteurs modernes comme Unity ou Unreal Engine abstraient toute cette complexité.
L'objectif principal est d'explorer les mathématiques appliquées à l'informatique graphique. Contrairement à la 3D moderne qui repose sur la rasterisation de milliers de triangles, le Raycasting est une technique de rendu qui lance des rayons depuis le point de vue du joueur pour déterminer la géométrie de la scène visible. C'est un exercice rigoureux de trigonométrie, d'algèbre linéaire et d'optimisation logicielle, car chaque pixel affiché à l'écran est le résultat d'un calcul précis effectué en temps réel par le processeur.
Au Cœur du Moteur : La Mathématique du Raycasting
Le principe fondamental du moteur repose sur le lancer de rayons. Pour chaque colonne verticale de pixels de l'écran (par exemple, 1920 colonnes pour une résolution Full HD), le moteur émet un rayon virtuel depuis la position du joueur. L'angle de ce rayon est déterminé par la direction du regard du joueur et son champ de vision (FOV). Le défi mathématique consiste à trouver, le plus rapidement possible, le point d'intersection exact entre ce rayon et les murs définis dans la grille de la carte.
Une approche naïve consisterait à avancer le rayon de manière incrémentale (par exemple, tous les 0.1 unité), mais cela serait à la fois imprécis et extrêmement coûteux en temps de calcul. Pour pallier cela, j'ai implémenté l'algorithme DDA (Digital Differential Analyzer). Cet algorithme permet de parcourir la grille de la carte case par case, en sautant directement à la prochaine intersection verticale ou horizontale. Cela garantit qu'aucun mur n'est "traversé" par erreur et réduit drastiquement le nombre de calculs nécessaires par image.
Une fois l'intersection trouvée, il faut calculer la distance entre le joueur et le mur pour déterminer la hauteur à laquelle le mur doit être dessiné à l'écran : plus le mur est proche, plus il apparaît grand. C'est ici qu'intervient une correction cruciale : si l'on utilise la distance euclidienne brute, l'image finale souffre d'une distorsion sphérique connue sous le nom d'effet "fisheye". Pour obtenir un rendu visuellement correct, j'ai dû projeter cette distance sur le vecteur de direction de la caméra, assurant ainsi des murs parfaitement verticaux et une perspective réaliste.
Concepts Mathématiques Clés :
* Vecteurs de Direction et Plan de Caméra : Utilisation de vecteurs normalisés pour représenter la direction du joueur et le plan de projection, permettant de calculer facilement la direction de chaque rayon par simple addition vectorielle.
* Trigonométrie et Matrices de Rotation : Application de matrices de rotation pour gérer les mouvements de la caméra (regarder à gauche ou à droite) en modifiant les coordonnées des vecteurs direction et plan.
* Projection de Distance : Correction de la distance perpendiculaire pour éliminer l'effet de distorsion fisheye inhérent aux projections radiales.
* Mapping de Texture : Calculs de ratios pour transformer les coordonnées d'intersection (monde 3D) en coordonnées de texture (espace UV 2D).
Architecture Logicielle et Gestion des Ressources
Au-delà des mathématiques, Cub3D est un test d'architecture logicielle. Le programme doit être robuste, modulaire et exempt de toute fuite de mémoire. J'ai structuré le code autour d'une structure de données centrale (t_data) qui agit comme le système nerveux du moteur, regroupant l'état du jeu, les configurations, les pointeurs vers les assets graphiques et les données de la carte. Cette centralisation facilite grandement la gestion du cycle de vie de l'application, notamment pour le nettoyage de la mémoire lors de la fermeture.
Le module de Parsing est particulièrement critique. Il est chargé de lire et d'interpréter les fichiers de configuration .cub. Ce n'est pas une simple lecture de fichier : le parser doit valider l'intégrité structurelle de la carte. Il vérifie que la carte est entièrement fermée par des murs (pour éviter que le joueur ne sorte de l'univers de jeu), que les textures spécifiées existent et sont valides, et que les couleurs sont correctement formatées. J'ai implémenté une lecture ligne par ligne robuste qui ignore les espaces superflus et gère les erreurs de formatage avec des messages explicites pour l'utilisateur, garantissant que le moteur ne démarre jamais avec des données corrompues.
La gestion graphique via la MiniLibX a nécessité une approche "bas niveau". Plutôt que d'utiliser des fonctions de dessin pixel par pixel (trop lentes), j'ai manipulé directement les tampons mémoire (buffers) des images. Cela implique de comprendre comment les couleurs sont stockées en mémoire (encodage ARGB, endianness) et d'écrire directement les valeurs hexadécimales dans les adresses mémoire correspondantes. Cette technique est indispensable pour atteindre une fluidité de 60 images par seconde, surtout lors de l'application des textures sur les murs, qui demande de lire un pixel dans une texture source et de l'écrire dans le buffer d'écran des millions de fois par seconde.
Fonctionnalités Techniques Implémentées :
* Rendu Texturé Complet : Support de textures distinctes pour les murs Nord, Sud, Est et Ouest, permettant une orientation visuelle claire dans l'environnement.
* Gestion des Collisions : Système de collision glissante permettant au joueur de longer les murs sans se bloquer, calculé en prédisant la position future par rapport à la grille de la carte.
* Optimisation des Performances : Minimisation des appels systèmes et des calculs trigonométriques dans la boucle principale de rendu pour maximiser le framerate.
* Gestion d'Événements Fluide : Système d'input réactif gérant simultanément les déplacements (ZQSD) et la rotation de la caméra pour une expérience de jeu moderne.
1. L'Algorithme DDA (Digital Differential Analyzer)
C'est le moteur de recherche d'intersection du projet. Cette boucle while est exécutée pour chaque colonne de l'écran. Elle fait "marcher" le rayon à travers la grille jusqu'à ce qu'il rencontre un obstacle. Notez l'efficacité de la logique : on ne vérifie pas chaque coordonnée flottante, mais on saute de frontière de case en frontière de case (side_dist et delta_dist).
// src/Cub3D/raycasting/draw_3d.c
static void perform_dda(t_ray_casting *rc, t_data *data)
{
rc->hit = 0;
// La boucle continue tant qu'aucun mur n'est touché et que le rayon est dans la carte
while (!rc->hit && rc->map_x >= 0 && rc->map_x < data->map->map_width
&& rc->map_y >= 0 && rc->map_y < data->map->map_height)
{
// On compare la distance jusqu'au prochain côté vertical (x) vs horizontal (y)
// On avance dans la direction la plus courte pour ne rater aucune case
if (rc->side_dist_x < rc->side_dist_y)
{
rc->side_dist_x += rc->delta_dist_x;
rc->map_x += rc->step_x;
rc->side = 0; // 0 indique un mur vertical (Est/Ouest)
}
else
{
rc->side_dist_y += rc->delta_dist_y;
rc->map_y += rc->step_y;
rc->side = 1; // 1 indique un mur horizontal (Nord/Sud)
}
// Vérification de la case actuelle dans la grille : est-ce un mur ('1') ?
if (rc->map_x >= 0 && rc->map_x < data->map->map_width
&& rc->map_y >= 0 && rc->map_y < data->map->map_height
&& data->map->map_2d[rc->map_y][rc->map_x] == '1')
rc->hit = 1;
}
}
2. Le Pipeline de Rendu Principal
Cette fonction est le chef d'orchestre du rendu graphique. Elle est appelée à chaque frame. Elle itère sur la largeur de l'écran (data->img->width), initialise le rayon pour chaque colonne, lance le calcul DDA, puis appelle les fonctions de dessin. C'est ici que la 2D de la carte devient la 3D de l'écran.
// src/Cub3D/raycasting/draw_3d.c
void render_3d_view(t_data *data)
{
t_ray_casting rc;
int x;
// Configuration du champ de vision (FOV) à 60 degrés
rc.fov = M_PI / 3;
rc.angle_step = rc.fov / data->img->width;
rc.side = 0;
// Balayage de chaque colonne de pixels de l'écran (de droite à gauche)
x = data->img->width - 1;
while (x >= 0)
{
// 1. Calcul des vecteurs initiaux du rayon pour la colonne x
init_ray_casting(&rc, data, data->img->width - 1 - x);
// 2. Lancement du rayon pour trouver l'impact avec un mur
perform_dda(&rc, data);
// 3. Calcul de la distance corrigée (anti-fisheye) et hauteur du mur
calculate_wall_properties(&rc, data);
// 4. Rendu final de la colonne (Plafond, Mur Texturé, Sol)
draw_vertical_line(data, x, rc.wall_height, &rc);
x--;
}
}
3. Validation et Chargement de la Carte
La robustesse d'un programme C se joue souvent lors de l'initialisation. Cette fonction montre comment le programme valide rigoureusement ses entrées avant de lancer le moteur graphique. Chaque étape (nombre d'arguments, extension de fichier, ouverture, lecture) est un point de contrôle qui peut arrêter l'exécution proprement en cas d'erreur.
// src/Cub3D/parsing/parsing.c
int ft_check_file(char *filename, int ac, t_data *data)
{
int fd;
// Vérification du nombre d'arguments
if (ac != 2)
{
printf("Error\nInvalid number of arguments\n");
return (0);
}
// Validation de l'extension .cub pour s'assurer du bon format
if (!is_valid_ext(filename, ".cub"))
{
printf("Error\nInvalid file name or extension\n");
return (0);
}
// Ouverture sécurisée du fichier
fd = open(filename, O_RDONLY);
// Parsing complet du contenu (textures, couleurs, map)
if (!ft_get_content(fd, data, filename))
{
// Nettoyage des ressources en cas d'échec
if (fd > 0)
close(fd);
return (0);
}
close(fd);
return (1);
}
Publié en January 2025