« Toute carte est une promesse. Toute ligne, un fil tendu entre deux points de l'espace. »
Ce guide démonte, module par module, l'implémentation d'un moteur de rendu wireframe —
du parsing brut d'un fichier .fdf jusqu'au tracé pixel-parfait d'un segment de Bresenham.
01 // Le projet
Contexte
FdF (Fil de Fer) est un projet graphique du tronc commun de l'école 42. Il introduit la programmation événementielle, la manipulation d'images bas-niveau via la miniLibX, et la géométrie projective. L'objectif : lire une carte d'altitude stockée dans un fichier texte, et la restituer à l'écran sous forme de filaire (wireframe) — un maillage de segments reliant chaque point à ses voisins.
Contraintes du sujet
- Le programme prend un unique argument : un fichier
.fdf. - Utilisation obligatoire de la miniLibX (pas de SDL, pas de OpenGL).
- Aucune fuite mémoire :
leaks/ Valgrind doivent être propres. - Une seule fenêtre, rendu en temps réel, sortie propre sur
ESCet croix fenêtre. - Makefile avec règles
all / clean / fclean / re. - Libft autorisée ;
get_next_lineetft_strsplitfortement recommandées.
Format de fichier .fdf
Une map est une grille de nombres séparés par des espaces. Chaque nombre représente
l'altitude Z du point à la colonne x et la ligne y.
Une couleur optionnelle peut être accolée après une virgule, en hexadécimal :
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10 10 0 0 10 10 0 0 0 10 10 10 10 10 0 0 0 0 0 10 10 0 0 10 10 0 0 0 0 0 0 0 10 10 0 0 0 0 10 10 0 0 10 10 0 0 0 0 0 0 0 10 10 0 0 0 0 10 10 10 10 10 10 0 0 0 0 10 10 10 10 0 0 0 0 0 0 10 10 10 10 10 0 0 0 10 10 0 0 0 0 0 0 0 0 0 0 0 0 10 10 0 0 0 10 10 0 0 0 0 0 0 0 0 0 0 0 0 10 10 0 0 0 10 10 10 10 10 10 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Un point peut porter une couleur explicite : 20,0xFF0000 signifie
altitude 20, couleur rouge pur. Si aucune couleur n'est donnée, le moteur calcule
automatiquement une teinte basée sur l'altitude (gradient violet → orange, voir §7).
Exemple avec couleurs
0 0 0 0 0 0 0 0 0 0 0 10 10 10 10 10 10 10 10 0 0 10 20,0xFF0000 15,0xFF0000 12 15,0xFF0000 17,0xFF0000 20,0xFF0000 10 0 0 10 15,0xFF0000 10 12 15,0xFF0000 15,0xFF0000 15,0xFF0000 10 0 0 5 15,0xFF0000 10 12 15,0xFF0000 15,0xFF0000 13 10 0 0 5 10 5 7 12 12 12 10 0 0 5 7 1 2 7 5 5 7 0 0 3 0 0 1 2 2 2 5 0 0 1 0 0 0 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0
02 // La miniLibX
La miniLibX est une surcouche minimale au serveur X11, écrite par l'école 42. Elle expose sept fonctions essentielles pour ouvrir une fenêtre, y dessiner, et réagir aux événements clavier/souris. Le moteur FdF les utilise toutes.
Le pipeline complet
Initialisation — fonction init_fdf
Le coeur du démarrage. On crée successivement la connexion au serveur X, la fenêtre, l'image buffer, puis on récupère l'adresse mémoire brute du buffer.
static void init_fdf(t_fdf *fdf, t_map *map) { fdf->map = map; fdf->mlx_ptr = mlx_init(); if (!fdf->mlx_ptr) { ft_putstr_fd("Error: mlx_init failed\n", 2); exit(1); } fdf->win_ptr = mlx_new_window(fdf->mlx_ptr, WIN_WIDTH, WIN_HEIGHT, "FdF"); fdf->img_ptr = mlx_new_image(fdf->mlx_ptr, WIN_WIDTH, WIN_HEIGHT); fdf->img_data = mlx_get_data_addr(fdf->img_ptr, &fdf->bpp, &fdf->size_line, &fdf->endian); init_camera(fdf); init_mouse(fdf); center_map(fdf); }
Boucle principale — main
int main(int argc, char **argv) { t_map *map; t_fdf fdf; if (argc != 2) { ft_putstr_fd("usage: ./fdf [map_file]\n", 2); return (1); } map = parse_map(argv[1]); if (!map) { ft_putstr_fd("error: could not parse map\n", 2); return (1); } init_fdf(&fdf, map); draw_map(&fdf); mlx_hook(fdf.win_ptr, 2, 1, key_hook, &fdf); mlx_hook(fdf.win_ptr, 4, 1, mouse_down, &fdf); mlx_hook(fdf.win_ptr, 5, 1, mouse_up, &fdf); mlx_hook(fdf.win_ptr, 6, 1, mouse_move, &fdf); mlx_hook(fdf.win_ptr, 17, 0, close_hook, &fdf); mlx_loop(fdf.mlx_ptr); return (0); }
Table récapitulatif des fonctions mlx
| Fonction | Rôle | Utilisation FdF |
|---|---|---|
mlx_init | Établit la connexion au serveur X11 | Une fois au démarrage |
mlx_new_window | Crée une fenêtre (largeur, hauteur, titre) | 1600×900 "FdF" |
mlx_new_image | Alloue un buffer image en mémoire | Double buffer pour le rendu |
mlx_get_data_addr | Retourne le pointeur char* vers les pixels | Écriture directe par img_pixel_put |
mlx_put_image_to_window | Copie l'image buffer vers la fenêtre | Une fois par draw_map |
mlx_hook | Enregistre un callback pour un événement X11 | Touches, souris, fermeture |
mlx_loop | Boucle événementielle bloquante | Toujours la dernière instruction |
mlx_string_put | Dessine du texte dans la fenêtre | HUD (contrôles affichés) |
mlx_destroy_window | Ferme la fenêtre et libère les ressources | Sur ESC ou croix |
mlx_pixel_put effectue un round-trip complet vers le serveur X
pour chaque pixel : encodage, envoi réseau IPC, redraw local. Sur une map 100×100, on
trace ~20 000 segments, soit des millions d'appels — l'écran clignote et l'application rame.
mlx_get_data_addr donne un pointeur direct vers la mémoire vidéo.
On écrit chaque pixel comme un simple *(unsigned int*)dst = color —
un seul accès mémoire, zéro IPC. Puis un seul appel à
mlx_put_image_to_window pousse toute l'image d'un coup.
Gain : de plusieurs secondes à ~16 ms (60 FPS).
03 // Parsing des maps
Le parsing est la fondation. Une erreur ici propage des segfaults jusqu'au rendu.
Le module parse.c lit le fichier en deux passes :
la première compte les dimensions, la seconde remplit les grilles
grid (altitudes) et colors (couleurs).
Structure t_map
typedef struct s_map { int **grid; /* altitudes Z [y][x] */ int **colors; /* couleurs custom [y][x] (-1 = auto) */ int width; int height; int min_z; /* altitude minimale (pour gradient) */ int max_z; } t_map;
Parsing — fonction parse_map
t_map *parse_map(char *filename) { t_map *map; int fd; char *line; char *pitcher; char **split; int y; int x; int ret; fd = open(filename, O_RDONLY); if (fd < 0) return (NULL); map = (t_map *)malloc(sizeof(t_map)); if (!map) return (close(fd), NULL); map->width = 0; map->height = 0; pitcher = NULL; line = NULL; /* ── Passe 1 : dimensions ── */ ret = get_next_line(fd, &line, &pitcher); if (ret <= 0 || !line) return (close(fd), free(map), free(line), free(pitcher), NULL); map->width = count_words(line); while (ret > 0) { map->height++; free(line); line = NULL; ret = get_next_line(fd, &line, &pitcher); } free(line); free(pitcher); close(fd); /* ── Allocation des grilles ── */ map->grid = malloc(sizeof(int *) * map->height); map->colors = malloc(sizeof(int *) * map->height); if (!map->grid || !map->colors) return (close(fd), free(map), NULL); /* ── Passe 2 : remplissage ── */ fd = open(filename, O_RDONLY); pitcher = NULL; y = 0; while (y < map->height) { line = NULL; get_next_line(fd, &line, &pitcher); split = ft_strsplit(line, ' '); map->grid[y] = malloc(sizeof(int) * map->width); map->colors[y] = malloc(sizeof(int) * map->width); x = 0; while (x < map->width && split[x]) { map->grid[y][x] = ft_atoi(split[x]); map->colors[y][x] = parse_color(split[x]); x++; } free_split(split); free(line); y++; } free(pitcher); close(fd); find_min_max(map); return (map); }
On doit connaître height et width avant
d'allouer les tableaux 2D. La première passe parcourt le fichier pour compter les lignes
et la largeur de la première ligne (toutes les lignes doivent avoir la même largeur).
Ensuite seulement, on peut allouer puis relire pour remplir.
Parsing des couleurs hex — parse_color
La fonction cherche une virgule dans le token. Si elle trouve ,0x
ou ,0X, elle parse ensuite les caractères en base 16.
Les lettres a-f et A-F sont converties
en valeurs 10–15. Si aucune couleur n'est présente, elle retourne -1
(sentinelle « couleur automatique »).
static int parse_color(char *str) { int i; int color; i = 0; color = -1; /* -1 = pas de couleur custom */ while (str[i] && str[i] != ',') i++; if (str[i] == ',') { i++; if (str[i] == '0' && (str[i + 1] == 'x' || str[i + 1] == 'X')) i += 2; color = 0; while (str[i]) { if (str[i] >= '0' && str[i] <= '9') color = color * 16 + (str[i] - '0'); else if (str[i] >= 'a' && str[i] <= 'f') color = color * 16 + (str[i] - 'a' + 10); else if (str[i] >= 'A' && str[i] <= 'F') color = color * 16 + (str[i] - 'A' + 10); i++; } } return (color); }
0xFF0000 se décompose en : 0x (préfixe base 16) +
FF (rouge, 255) + 00 (vert, 0) + 00 (bleu, 0).
La valeur finale est 255 × 65536 + 0 × 256 + 0 = 16711680, stockée
comme un int. Le moteur la passera telle quelle à
img_pixel_put, qui l'écrira en little-endian dans le buffer.
Recherche des extrêmes — find_min_max
Nécessaire pour le gradient de couleurs : on normalise chaque altitude z
entre 0 et 1 via (z - min_z) / (max_z - min_z).
static void find_min_max(t_map *map) { int x; int y; map->min_z = 0; map->max_z = 0; y = 0; while (y < map->height) { x = 0; while (x < map->width) { if (map->grid[y][x] < map->min_z) map->min_z = map->grid[y][x]; if (map->grid[y][x] > map->max_z) map->max_z = map->grid[y][x]; x++; } y++; } }
04 // Structures de données
FdF s'articule autour de cinq structures. La principale, t_fdf,
est un contexte global passé à tous les hooks mlx via un pointeur
void*. Elle agrège la map, la caméra, la souris et les handles mlx.
Toutes les structures
- int x
- int y
- int z
- int color
- int** grid
- int** colors
- int width
- int height
- int min_z
- int max_z
- int zoom
- double x_angle
- double y_angle
- double z_angle
- float z_height
- int offset_x
- int offset_y
- int iso
- int button
- int x
- int y
- int prev_x
- int prev_y
- void* mlx_ptr — handle serveur X
- void* win_ptr — handle fenêtre
- void* img_ptr — handle image buffer
- char* img_data — pixels bruts
- int bpp — bits par pixel (32)
- int size_line — octets par ligne
- int endian — boutisme
- t_map* map → grille d'altitude
- t_camera* cam → transformation 3D→2D
- t_mouse* mouse → état drag/zoom
Diagramme des relations
void* aux hooks mlx ?
L'API miniLibX est générique : int key_hook(int keycode, void *param).
Elle ne sait rien de votre t_fdf. Le contrat est : « voici un pointeur
opaque, je te le rends tel quel à chaque événement ». On passe donc &fdf
en param, et le hook le cast en t_fdf* pour
retrouver tout le contexte (map, caméra, image). C'est l'équivalent C d'une closure.
Définition C complète
typedef struct s_point { int x; int y; int z; int color; } t_point; typedef struct s_map { int **grid; int **colors; int width; int height; int min_z; int max_z; } t_map; typedef struct s_camera { int zoom; double x_angle; double y_angle; double z_angle; float z_height; int offset_x; int offset_y; int iso; } t_camera; typedef struct s_mouse { int button; int x; int y; int prev_x; int prev_y; } t_mouse; typedef struct s_fdf { void *mlx_ptr; void *win_ptr; void *img_ptr; char *img_data; int bpp; int size_line; int endian; t_map *map; t_camera *cam; t_mouse *mouse; } t_fdf;
05 // Projection et rotation
Cœur mathématique du moteur. Une map est un nuage de points 3D (x, y, z).
L'écran est un plan 2D. La projection transforme chaque point 3D en un pixel 2D.
FdF implémente deux projections : isométrique (angles fixes) et
parallèle (angles ajustables). Les deux passent par les mêmes matrices de rotation.
Projection isométrique — formule
L'isométrique classique fait pivoter la grille de 30° autour de X, puis 45° autour de Z, de manière à ce que les trois axes forment des angles égaux de 120°. En coordonnées écran :
Formellement : x' = (x - y) · cos(30°),
y' = (x + y) · sin(30°) - z.
FdF ne calcule pas cette formule directement : il applique trois rotations matricielles
consécutives — ce qui est plus général et permet à l'utilisateur de
faire pivoter la scène en temps réel.
Différence isométrique vs parallèle
| Critère | Isométrique | Parallèle |
|---|---|---|
| Angles | Fixes : X=−35.26°, Y=−30°, Z=+35.26° | Ajustables : X=−30°, Y=−15°, Z=0° |
| Sensation | Vue 3D « classique » (jeux stratégie) | Vue de dessus légèrement inclinée |
Variable iso | 1 | 0 |
| Bascule | Touche SPACE → toggle_projection() | |
Matrices de rotation
Pour chaque axe, on applique la matrice de rotation standard. La rotation se fait
autour d'un axe en modifiant les deux autres coordonnées via
cos(angle) et sin(angle).
Une rotation dans le plan est par définition x' = x·cos θ − y·sin θ,
y' = x·sin θ + y·cos θ. C'est la définition géométrique
du cercle trigonométrique : un point à distance r de l'origine
reste à distance r après rotation. En 3D, on applique cette
transformation deux à deux autour de chaque axe. Les angles sont stockés en
radians (ex. -0.523599 = −30° = −π/6).
Rotation autour de X (modifie Y et Z)
static void rotate_x(int *y, int *z, double angle) { int prev_y; prev_y = *y; *y = prev_y * cos(angle) + *z * sin(angle); *z = prev_y * -sin(angle) + *z * cos(angle); }
Rotation autour de Y (modifie X et Z)
static void rotate_y(int *x, int *z, double angle) { int prev_x; prev_x = *x; *x = prev_x * cos(angle) + *z * sin(angle); *z = prev_x * -sin(angle) + *z * cos(angle); }
Rotation autour de Z (modifie X et Y)
static void rotate_z(int *x, int *y, double angle) { int prev_x; int prev_y; prev_x = *x; prev_y = *y; *x = prev_x * cos(angle) - prev_y * sin(angle); *y = prev_x * sin(angle) + prev_y * cos(angle); }
La fonction project — orchestration
Pour chaque sommet (x, y) de la grille, project
récupère son altitude Z, applique le zoom, centre la grille sur l'origine, enchaîne les
trois rotations, puis re-centre sur le milieu de l'écran avec l'offset de l'utilisateur.
t_point project(int x, int y, t_fdf *fdf) { t_point p; p.z = fdf->map->grid[y][x]; p.color = get_color(fdf->map, p.z, fdf->map->colors[y][x]); p.x = x * fdf->cam->zoom; p.y = y * fdf->cam->zoom; p.z = p.z * fdf->cam->zoom / fdf->cam->z_height; /* aplatissement */ p.x -= (fdf->map->width * fdf->cam->zoom) / 2; /* recentre */ p.y -= (fdf->map->height * fdf->cam->zoom) / 2; rotate_x(&p.y, &p.z, fdf->cam->x_angle); rotate_y(&p.x, &p.z, fdf->cam->y_angle); rotate_z(&p.x, &p.y, fdf->cam->z_angle); p.x += WIN_WIDTH / 2 + fdf->cam->offset_x; p.y += WIN_HEIGHT / 2 + fdf->cam->offset_y; return (p); }
Aplatissement Z — z_height
z_height ?
La ligne p.z = p.z * zoom / z_height divise l'altitude par un
facteur ajustable. Avec z_height = 1, le relief est intact.
En appuyant sur −, on incrémente z_height
de 0.1 : le relief s'aplatit. Avec +, on le décrémente :
le relief s'exagère. Ceci permet de visualiser des maps quasi-planes
(comme basictest.fdf avec des pentes de 1) en exagérant l'échelle verticale.
Initialisation de la caméra
static void init_camera(t_fdf *fdf) { fdf->cam = (t_camera *)malloc(sizeof(t_camera)); if (!fdf->cam) exit(1); /* zoom auto : la map tient dans la fenêtre */ fdf->cam->zoom = ft_min(WIN_WIDTH / fdf->map->width / 2, WIN_HEIGHT / fdf->map->height / 2); if (fdf->cam->zoom < 1) fdf->cam->zoom = 1; /* angles isométriques : −35.26° / −30° / +35.26° */ fdf->cam->x_angle = -0.615472907; fdf->cam->y_angle = -0.523599; fdf->cam->z_angle = 0.615472907; fdf->cam->z_height = 1; fdf->cam->offset_x = 0; fdf->cam->offset_y = 0; fdf->cam->iso = 1; }
06 // Algorithme de Bresenham
Une fois deux sommets projetés en 2D, il faut tracer le segment qui les relie. On utilise l'algorithme de Bresenham (1965), qui ne manipule que des entiers — aucune division dans la boucle, aucune opération flottante. C'est l'algorithme de référence pour le tracé de lignes sur une grille discrète.
Principe
Soit un segment de S = (x0, y0) à E = (x1, y1).
À chaque itération, on avance d'un pixel dans la direction dominante (X si la pente est
< 1, Y sinon). La question : faut-il aussi avancer dans l'autre direction ?
Bresenham répond en accumulant une erreur err = dx − dy :
- Si
2·err > −dy→ on avance en X,err −= dy - Si
2·err < dx→ on avance en Y,err += dx
L'erreur mesure la dérive verticale accumulée. Quand elle dépasse un demi-pixel, on « saute » une ligne — exactement comme on arrondit à l'entier le plus proche.
Diagramme pas à pas
Code complet — draw_line
void draw_line(t_fdf *fdf, t_point s, t_point e) { int dx; int dy; int sx; int sy; int err; int e2; int color; dx = ft_abs(e.x - s.x); dy = ft_abs(e.y - s.y); sx = s.x < e.x ? 1 : -1; sy = s.y < e.y ? 1 : -1; err = dx - dy; color = e.color; if (s.color != e.color) color = s.color; while (1) { img_pixel_put(fdf, s.x, s.y, color); if (s.x == e.x && s.y == e.y) break ; e2 = 2 * err; if (e2 > -dy) { err -= dy; s.x += sx; } if (e2 < dx) { err += dx; s.y += sy; } } }
Anatomie du code
| Variable | Rôle |
|---|---|
dx, dy | Distance absolue en X et Y |
sx, sy | Signe du pas (+1 ou −1) selon la direction S→E |
err | Erreur accumulée, initialisée à dx − dy |
e2 | Erreur doublée (évite les flottants pour la comparaison au demi-pixel) |
color | Couleur du segment (couleur de S si S et E diffèrent) |
Écriture d'un pixel — img_pixel_put
void img_pixel_put(t_fdf *fdf, int x, int y, int color) { char *dst; if (x < 0 || x >= WIN_WIDTH || y < 0 || y >= WIN_HEIGHT) return ; /* clip hors écran */ dst = fdf->img_data + (y * fdf->size_line + x * (fdf->bpp / 8)); *(unsigned int *)dst = color; }
img_data est un tableau 1D de char (octets).
Pour atteindre le pixel (x, y) : on saute y
lignes complètes (chaque ligne = size_line octets), puis
x pixels (chaque pixel = bpp/8 octets, soit 4 pour du 32 bits).
On caste l'adresse en unsigned int* et on écrit la couleur en une seule
instruction machine. Aucune fonction mlx appelée.
Boucle de tracé — draw_map
Pour chaque sommet, on trace deux segments : vers le voisin de droite et vers le voisin du bas. Cela génère le maillage wireframe complet.
void draw_map(t_fdf *fdf) { t_point p1; t_point p2; int x; int y; ft_bzero(fdf->img_data, WIN_WIDTH * WIN_HEIGHT * (fdf->bpp / 8)); y = 0; while (y < fdf->map->height) { x = 0; while (x < fdf->map->width) { p1 = project(x, y, fdf); if (x < fdf->map->width - 1) { p2 = project(x + 1, y, fdf); draw_line(fdf, p1, p2); /* voisin droite */ } if (y < fdf->map->height - 1) { p2 = project(x, y + 1, fdf); draw_line(fdf, p1, p2); /* voisin bas */ } x++; } y++; } mlx_put_image_to_window(fdf->mlx_ptr, fdf->win_ptr, fdf->img_ptr, 0, 0); draw_hud(fdf); }
07 // Gestion des couleurs
Le moteur gère deux sources de couleurs : les couleurs custom spécifiées
dans le fichier .fdf, et un gradient automatique
calculé à partir de l'altitude lorsque aucune couleur n'est fournie.
Gradient par altitude
Le gradient violet → orange évoque les cartes de relief topographique. La fonction
get_color normalise l'altitude entre 0 et 1, puis sélectionne
un palier parmi cinq :
| Palier (%) | Couleur | Hex | Sensation |
|---|---|---|---|
| 0 – 20% | Violet profond | 0x432371 | Fonds / creux |
| 20 – 40% | Mauve | 0x714674 | Pied de pente |
| 40 – 60% | Rose taupe | 0x9F6976 | Pente moyenne |
| 60 – 80% | Saumon | 0xCC8B79 | Haut de pente |
| 80 – 100% | Orange clair | 0xFAAE7B | Sommets |
Code — get_color
int get_color(t_map *map, int z, int custom) { double percent; int max; if (custom >= 0) /* couleur explicite dans le .fdf */ return (custom); max = map->max_z - map->min_z; if (max == 0) return (0x432371); /* map plate → couleur unique */ percent = (double)(z - map->min_z) / max; if (percent < 0.2) return (0x432371); if (percent < 0.4) return (0x714674); if (percent < 0.6) return (0x9F6976); if (percent < 0.8) return (0xCC8B79); return (0xFAAE7B); }
-1 et non-couleur
Si un point n'a pas de couleur dans le .fdf, parse_color
renvoie -1. Le test custom >= 0 signifie :
« si on a une vraie couleur, l'utiliser ; sinon, calculer la couleur du gradient ».
Toutes les valeurs valides (0x000000 à 0xFFFFFF) sont ≥ 0, donc -1
ne peut jamais être confondu avec une couleur réelle.
Couleurs custom dans .fdf
Dans le fichier, on colle la couleur à l'altitude avec une virgule. La fonction
parse_color (vue §3) extrait l'entier. Exemples :
10 → altitude 10, couleur auto (gradient) 20,0xFF0000 → altitude 20, couleur rouge pur 5,0x00FF00 → altitude 5, couleur vert pur 0,0xFFFFFF → altitude 0, couleur blanc 15,0xff8800 → minuscules aussi acceptées (X/x insensibles)
Comment le segment décide sa couleur
Dans draw_line, on regarde les couleurs des deux extrémités :
color = e.color;
if (s.color != e.color)
color = s.color; /* couleur du sommet source prioritaire */
Une amélioration possible serait un gradient interpolé le long du segment
(color = s.color + (e.color − s.color) · t), mais l'implémentation
actuelle choisit simplement la couleur d'une extrémité — ce qui donne un rendu « cell-shaded »
où chaque facette a une teinte nette.
HUD — draw_hud
Une fois l'image affichée, on dessine par-dessus un petit panneau de texte qui rappelle
les contrôles. mlx_string_put écrit directement dans la fenêtre
(pas dans le buffer image), donc ce texte est « volatile » — il disparaît dès qu'on
redessine l'image, d'où son appel systématique à la fin de draw_map.
void draw_hud(t_fdf *fdf) { mlx_string_put(fdf->mlx_ptr, fdf->win_ptr, 5, 0, 0xFFFFFF, "Left Click: Pan"); mlx_string_put(fdf->mlx_ptr, fdf->win_ptr, 5, 20, 0xFFFFFF, "Right Click: Rotate x/y"); mlx_string_put(fdf->mlx_ptr, fdf->win_ptr, 5, 40, 0xFFFFFF, "Mid Click: Rotate z"); if (fdf->cam->iso) mlx_string_put(fdf->mlx_ptr, fdf->win_ptr, 5, 60, 0xFFFFFF, "Space: Proj (Isometric)"); else mlx_string_put(fdf->mlx_ptr, fdf->win_ptr, 5, 60, 0xFFFFFF, "Space: Proj (Parallel)"); mlx_string_put(fdf->mlx_ptr, fdf->win_ptr, 5, 80, 0xFFFFFF, "R: Reset"); mlx_string_put(fdf->mlx_ptr, fdf->win_ptr, 5, 100, 0xFFFFFF, "-/+: Flatten Z"); }
08 // Événements clavier et souris
La miniLibX est événementielle : mlx_loop attend indéfiniment
des événements X11 et déclenche les callbacks enregistrés via mlx_hook.
Chaque callback reçoit le paramètre void* passé à l'enregistrement
— ici, &fdf.
Table des contrôles
| Périphérique | Entrée | Code | Action |
|---|---|---|---|
| Clavier | ESC | 65307 | Quitter (libère la map, détruit la fenêtre) |
| Clavier | ← | 65361 | Décaler la map à gauche (offset_x − 10) |
| Clavier | → | 65363 | Décaler la map à droite (offset_x + 10) |
| Clavier | ↑ | 65362 | Décaler la map vers le haut (offset_y − 10) |
| Clavier | ↓ | 65364 | Décaler la map vers le bas (offset_y + 10) |
| Clavier | + | 65451 | Exagérer le relief (z_height − 0.1) |
| Clavier | − | 65453 | Aplatir le relief (z_height + 0.1) |
| Clavier | SPACE | 32 | Basculer projection isométrique ↔ parallèle |
| Clavier | R | 114 | Réinitialiser la caméra (zoom, angles, offset) |
| Souris | Molette haut | btn 4 | Zoom avant (zoom + 2) |
| Souris | Molette bas | btn 5 | Zoom arrière (zoom − 2, min 1) |
| Souris | Clic gauche + drag | btn 1 | Translater la map (offset_x/y) |
| Souris | Clic droit + drag | btn 3 | Rotation X/Y (selon dx/dy du drag) |
| Souris | Clic milieu + drag | btn 2 | Rotation Z |
| Fenêtre | Croix de fermeture | evt 17 | Quitter proprement |
Hook clavier — key_hook
int key_hook(int keycode, void *param) { t_fdf *fdf; fdf = (t_fdf *)param; if (keycode == ESC_KEY) { free_map(fdf->map); mlx_destroy_window(fdf->mlx_ptr, fdf->win_ptr); exit(0); } if (keycode == ARROW_L) fdf->cam->offset_x -= 10; else if (keycode == ARROW_R) fdf->cam->offset_x += 10; else if (keycode == ARROW_U) fdf->cam->offset_y -= 10; else if (keycode == ARROW_D) fdf->cam->offset_y += 10; else if (keycode == PLUS_KEY || keycode == MINUS_KEY) mod_height(keycode, fdf); else if (keycode == SPACE_KEY) toggle_projection(fdf); else if (keycode == R_KEY) reset_camera(fdf); draw_map(fdf); return (0); }
Modulation de l'altitude — mod_height
static void mod_height(int keycode, t_fdf *fdf) { if (keycode == MINUS_KEY) fdf->cam->z_height += 0.1; /* aplatit */ else if (keycode == PLUS_KEY) fdf->cam->z_height -= 0.1; /* exagère */ if (fdf->cam->z_height < 0.1) fdf->cam->z_height = 0.1; /* clamp bas */ if (fdf->cam->z_height > 10) fdf->cam->z_height = 10; /* clamp haut */ }
Hook souris — clics et mouvements
int mouse_down(int button, int x, int y, void *param) { t_fdf *fdf; fdf = (t_fdf *)param; if (button == 4 || button == 5) mouse_zoom(button, fdf); else { fdf->mouse->button = button; fdf->mouse->prev_x = x; fdf->mouse->prev_y = y; } return (0); } int mouse_move(int x, int y, void *param) { t_fdf *fdf; fdf = (t_fdf *)param; if (fdf->mouse->button == 3) { /* clic droit : rotation X/Y */ fdf->cam->x_angle += (y - fdf->mouse->prev_y) * 0.002; fdf->cam->y_angle += (x - fdf->mouse->prev_x) * 0.002; fdf->mouse->prev_x = x; fdf->mouse->prev_y = y; draw_map(fdf); } else if (fdf->mouse->button == 1) { /* clic gauche : translation */ fdf->cam->offset_x += (x - fdf->mouse->prev_x); fdf->cam->offset_y += (y - fdf->mouse->prev_y); fdf->mouse->prev_x = x; fdf->mouse->prev_y = y; draw_map(fdf); } else if (fdf->mouse->button == 2) move_z(x, y, fdf); /* clic milieu : rotation Z */ return (0); }
Zoom molette — mouse_zoom
static void mouse_zoom(int button, t_fdf *fdf) { if (button == 4) fdf->cam->zoom += 2; else if (button == 5) fdf->cam->zoom -= 2; if (fdf->cam->zoom < 1) fdf->cam->zoom = 1; draw_map(fdf); }
mlx_hook vs mlx_key_hook
| Critère | mlx_key_hook | mlx_hook |
|---|---|---|
| Événements couverts | Clavier uniquement (KeyRelease) | Tous : clavier, souris, expose, fermeture |
| Signature callback | int (*)(int keycode, void*) | int (*)(...) — variable selon l'événement |
| Pression vs relâchement | Seulement relâchement (Release) | Pression (KeyPress) via event 2 |
| Fermeture fenêtre | Impossible | Événement 17 (ClientMessage) → croix |
| Boutons souris | Impossible | Événements 4 (ButtonPress), 5 (ButtonRelease), 6 (MotionNotify) |
mlx_hook
mlx_key_hook ne déclenche qu'au relâchement d'une touche —
impossible à utiliser pour une interface fluide. mlx_hook avec
l'événement 2 (KeyPress) répond à la pression. De plus,
seul mlx_hook permet d'attraper la fermeture par la croix
(événement 17) — sans lui, cliquer sur la croix laisserait le
processus tourner indéfiniment dans mlx_loop.
Codes d'événements X11 utilisés
| Code | Nom X11 | Sens | Callback FdF |
|---|---|---|---|
2 | KeyPress | Une touche est pressée | key_hook |
4 | ButtonPress | Bouton souris pressé | mouse_down |
5 | ButtonRelease | Bouton souris relâché | mouse_up |
6 | MotionNotify | La souris bouge | mouse_move |
17 | ClientMessage | Fermeture fenêtre (croix) | close_hook |
Sortie propre — close_hook
int close_hook(void *param) { t_fdf *fdf; fdf = (t_fdf *)param; free_map(fdf->map); mlx_destroy_window(fdf->mlx_ptr, fdf->win_ptr); exit(0); }
09 // Bonus implémentés
Au-delà du sujet minimum (afficher la map en isométrique), FdF propose une série de fonctionnalités bonus qui améliorent l'expérience utilisateur et démontrent la maîtrise de la miniLibX.
| Bonus | Statut | Comment l'utiliser | Implémentation |
|---|---|---|---|
| Rotation interactive | ✓ | Clic droit + drag (X/Y), clic milieu + drag (Z) | mouse_move modifie x/y/z_angle |
| Zoom molette | ✓ | Molette haut/bas | mouse_zoom ± 2 sur zoom |
| Échelle automatique | ✓ | Automatique au chargement | ft_min(WIN_W/w/2, WIN_H/h/2) |
| Aplatissement Z | ✓ | + / − | mod_height ajuste z_height |
| Translation clavier | ✓ | Flèches directionnelles | key_hook modifie offset_x/y |
| Translation souris | ✓ | Clic gauche + drag | mouse_move bouton 1 |
| Reset caméra | ✓ | R | reset_camera réinit. tous les paramètres |
| HUD des contrôles | ✓ | Toujours visible (haut-gauche) | draw_hud via mlx_string_put |
| Couleurs custom | ✓ | Syntaxe z,0xRRGGBB dans le .fdf | parse_color + sentinelle -1 |
| Gradient par altitude | ✓ | Automatique si pas de couleur custom | get_color 5 paliers violet→orange |
| Projections multiples | ✓ | SPACE bascule iso/parallèle | toggle_projection change les angles |
| Sortie propre (croix + ESC) | ✓ | ESC ou croix fenêtre | close_hook + handler ESC_KEY |
Reset caméra — reset_camera
void reset_camera(t_fdf *fdf) { fdf->cam->offset_x = 0; fdf->cam->offset_y = 0; fdf->cam->z_height = 1; fdf->cam->zoom = ft_min(WIN_WIDTH / fdf->map->width / 2, WIN_HEIGHT / fdf->map->height / 2); if (fdf->cam->zoom < 1) fdf->cam->zoom = 1; if (fdf->cam->iso) { fdf->cam->x_angle = -0.615472907; fdf->cam->y_angle = -0.523599; fdf->cam->z_angle = 0.615472907; } else { fdf->cam->x_angle = -0.523599; fdf->cam->y_angle = -0.261799; fdf->cam->z_angle = 0; } }
Toggle projection — toggle_projection
static void toggle_projection(t_fdf *fdf) { if (fdf->cam->iso) { /* on bascule vers la parallèle */ fdf->cam->x_angle = -0.523599; /* −30° */ fdf->cam->y_angle = -0.261799; /* −15° */ fdf->cam->z_angle = 0; } else { /* on bascule vers l'isométrique */ fdf->cam->x_angle = -0.615472907; /* −35.26° */ fdf->cam->y_angle = -0.523599; /* −30° */ fdf->cam->z_angle = 0.615472907; /* +35.26° */ } fdf->cam->iso = !fdf->cam->iso; }
Les angles sont stockés en radians. Pour passer d'une valeur en degrés à la valeur en
radians : rad = deg × π / 180. Ainsi
30° = 0.523599 rad, 35.26° = 0.615472 rad
(l'angle magique de l'isométrique : atan(1/√2)).
10 // Compilation & tests
Makefile
Le Makefile compile les 8 modules sources, lie la libft et la miniLibX, et produit
l'exécutable fdf. Les flags -Wall -Wextra -Werror
garantissent un code strict.
NAME = fdf
CC = gcc
CFLAGS = -Wall -Wextra -Werror
SRC_DIR = src
OBJ_DIR = obj
INC_DIR = includes
MLX_DIR = minilibx-linux
LIBFT_DIR = libft
LIBFT_LIB = $(LIBFT_DIR)/libft.a
SRCS = $(SRC_DIR)/main.c \
$(SRC_DIR)/parse.c \
$(SRC_DIR)/draw.c \
$(SRC_DIR)/pixel.c \
$(SRC_DIR)/events.c \
$(SRC_DIR)/mouse.c \
$(SRC_DIR)/utils.c \
$(SRC_DIR)/map_utils.c
OBJS = $(SRCS:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
INC_FLAGS = -I$(INC_DIR) -I$(LIBFT_DIR) -I$(MLX_DIR)
LIB_FLAGS = -L$(MLX_DIR) -lmlx -L$(LIBFT_DIR) -lft \
-lXext -lX11 -lm
all: $(NAME)
$(NAME): $(LIBFT_LIB) $(OBJS)
$(CC) $(CFLAGS) $(OBJS) $(LIB_FLAGS) -o $(NAME)
$(LIBFT_LIB):
make -C $(LIBFT_DIR)
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR)
$(CC) $(CFLAGS) $(INC_FLAGS) -c $< -o $@
$(OBJ_DIR):
mkdir -p $(OBJ_DIR)
clean:
rm -rf $(OBJ_DIR)
make -C $(LIBFT_DIR) clean 2>/dev/null || true
fclean: clean
rm -f $(NAME)
make -C $(LIBFT_DIR) fclean 2>/dev/null || true
re: fclean all
.PHONY: all clean fclean re
La miniLibX Linux repose sur X11. Sur Debian/Ubuntu : apt install libxext-dev libx11-dev.
Sur Mac, la miniLibX est préinstallée et n'a pas besoin de ces flags. Le flag
-lm est requis pour cos / sin de <math.h>.
Compilation
# 1. Compilation complète (libft + mlx + fdf) $ make # 2. Recompilation propre $ make re # 3. Lancer le programme $ ./fdf test_maps/42.fdf # 4. Avec une map colorée $ ./fdf test_maps/elem-col.fdf # 5. Avec une grande map $ ./fdf test_maps/mars.fdf # 6. Vérifier les fuites mémoire $ valgrind --leak-check=full ./fdf test_maps/42.fdf
Maps de test fournies
| Fichier | Dimensions | Intérêt |
|---|---|---|
42.fdf | 19 × 11 | Le logo « 42 » en relief |
basictest.fdf | 10 × 9 | Plan incliné régulier (test du gradient) |
elem.fdf | — | Map pédagogique simple |
elem-col.fdf | 10 × 10 | Map avec couleurs custom (0xFF0000) |
elem-fract.fdf | — | Relief fractal |
pyramide.fdf | — | Pyramide parfaite |
pyra.fdf | — | Pyramide alternée |
pylone.fdf | — | Pylône électrique |
julia.fdf | — | Fractal de Julia en relief |
mars.fdf | — | Grande map — test performance |
plat.fdf | — | Map totalement plate (test max==0) |
pnp_flat.fdf | — | Positif/négatif sur fond plat |
pentenegpos.fdf | — | Pentes négatives et positives |
10-2.fdf · 10-70.fdf · 20-60.fdf · 50-4.fdf · 100-6.fdf | variables | Maps générées largeur-hauteur |
t1.fdf · t2.fdf | — | Maps de test rapides |
Contrôles complets en jeu
Checklist de validation
makecompile sans warning ni erreur../fdfsans argument afficheusage:et retourne 1../fdf nonexistent.fdfafficheerror: could not parse mapet retourne 1../fdf test_maps/42.fdfouvre une fenêtre avec le logo 42.ESCet la croix ferment proprement (pas deSegmentation fault).- Les flèches,
+/-,SPACE,Rrépondent instantanément. - La molette zoome, le drag gauche translate, le drag droit pivote.
valgrind --leak-check=full ./fdf test_maps/42.fdfne rapporte aucune fuite.- Une map plate (
plat.fdf) ne crash pas (gestionmax == 0). - Les couleurs custom (
elem-col.fdf) sont affichées correctement.
main.c · entry point + init
parse.c · lecture .fdf, dimensions, couleurs, min/max
draw.c · matrices de rotation + projection + boucle de tracé
pixel.c · Bresenham, écriture pixel, gradient, HUD
events.c · hooks clavier, fermeture, toggle projection
mouse.c · hooks souris : zoom, translation, rotation
utils.c · helpers : ft_abs, ft_min, center_map, reset_camera
map_utils.c · free_map (libération propre des grilles 2D)
Guide généré pour le projet FdF — École 42. Thème visuel YoRHa // Unit 42A.
Tous les extraits de code proviennent de l'implémentation réelle située dans
/home/z/fdf/.