FdF // Fil de Fer
UNIT YORHA-42A  ·  GUIDE TECHNIQUE
// Projet 42 — Graphique
FIL DE FER
Une wireframe-terrain engine, écrite en C natif sur miniLibX.
LANGAGE: C (C99) API: miniLibX MODULES: 8 LIGNES: ~700

« 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 ESC et croix fenêtre.
  • Makefile avec règles all / clean / fclean / re.
  • Libft autorisée ; get_next_line et ft_strsplit fortement 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 :

test_maps/42.fdffdf map
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
Couleurs personnalisées

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

test_maps/elem-col.fdffdf map
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

PIPELINE MINILIBX ┌──────────┐ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │ mlx_init │──▶│ mlx_new_win │──▶│ mlx_new_image│──▶│ mlx_get_data_addr│ └──────────┘ └─────────────┘ └──────────────┘ └────────┬─────────┘ │ ┌──────────────────────────────────────┘ ▼ écrire pixels dans img_data ◀── img_pixel_put() │ ▼ mlx_put_image_to_window() │ ▼ mlx_hook() ──▶ callbacks key/mouse ──▶ draw_map() │ ▼ mlx_loop() ◀── boucle événementielle infinie

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.

src/main.c — init_fdfc
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

src/main.c — mainc
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

FonctionRôleUtilisation FdF
mlx_initÉtablit la connexion au serveur X11Une fois au démarrage
mlx_new_windowCrée une fenêtre (largeur, hauteur, titre)1600×900 "FdF"
mlx_new_imageAlloue un buffer image en mémoireDouble buffer pour le rendu
mlx_get_data_addrRetourne le pointeur char* vers les pixelsÉcriture directe par img_pixel_put
mlx_put_image_to_windowCopie l'image buffer vers la fenêtreUne fois par draw_map
mlx_hookEnregistre un callback pour un événement X11Touches, souris, fermeture
mlx_loopBoucle événementielle bloquanteToujours la dernière instruction
mlx_string_putDessine du texte dans la fenêtreHUD (contrôles affichés)
mlx_destroy_windowFerme la fenêtre et libère les ressourcesSur ESC ou croix
Pourquoi l'image buffer plutôt que mlx_pixel_put ?

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

includes/fdf.hc
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

src/parse.c — parse_mapc
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);
}
Pourquoi deux passes ?

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 »).

src/parse.c — parse_colorc
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);
}
Anatomie d'un hex 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).

src/parse.c — find_min_maxc
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

t_point
// sommet projeté
  • int x
  • int y
  • int z
  • int color
t_map
// grille d'altitude
  • int** grid
  • int** colors
  • int width
  • int height
  • int min_z
  • int max_z
t_camera
// point de vue
  • int zoom
  • double x_angle
  • double y_angle
  • double z_angle
  • float z_height
  • int offset_x
  • int offset_y
  • int iso
t_mouse
// état souris
  • int button
  • int x
  • int y
  • int prev_x
  • int prev_y
t_fdf // contexte racine
// passé à tous les hooks mlx
  • 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

CONTEXT GRAPH ┌───────────────────────────────────────────┐t_fdf │ │ mlx_ptr win_ptr img_ptr img_data │ │ bpp size_line endian │ └─────────┬───────────────────┬──────────────────┬─────────┘ │ │ │ ┌──────▼──────┐ ┌───────▼──────┐ ┌──────▼──────┐ │ t_map │ │ t_camera │ │ t_mouse │ │ grid[][] │ │ zoom │ │ button │ │ colors[][] │ │ x/y/z_angle │ │ x / y │ │ w / h │ │ z_height │ │ prev_x/y │ │ min/max_z │ │ offset_x/y │ └─────────────┘ └─────────────┘ │ iso (bool) │ └──────────────┘ génère un t_point par sommet (x, y, z, color) passé à draw_line(t_fdf, t_point s, t_point e)
Pourquoi passer un 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

includes/fdf.hc
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 :

ISOMETRIC PROJECTION — 3D vers 2D Y Z (altitude) ▲ ▲ │ / │ │ / │ │ / ───▶ │ ─── ▲ │/ │ ╱ ╲│ ┼──────▶ X │ ╱ ╲ └── ╲ écran (x', y') ╱ axe X ╲ rotation 30° X + 45° Z ╲ chaque axe = 120° sur é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èreIsométriqueParallèle
AnglesFixes : X=−35.26°, Y=−30°, Z=+35.26°Ajustables : X=−30°, Y=−15°, Z=0°
SensationVue 3D « classique » (jeux stratégie)Vue de dessus légèrement inclinée
Variable iso10
BasculeTouche SPACEtoggle_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).

Pourquoi des matrices cos/sin ?

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)

src/draw.c — rotate_xc
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)

src/draw.c — rotate_yc
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)

src/draw.c — rotate_zc
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.

src/draw.c — projectc
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

À quoi sert 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

src/main.c — init_camerac
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

BRESENHAM — segment de (1,1) à (8,5) dx = 7, dy = 4, sx = +1, sy = +1, err = dx − dy = 3 y\x 1 2 3 4 5 6 7 8 ┌───┬───┬───┬───┬───┬───┬───┬───┐ 1 │ │ │ │ │ │ │ │ │ err=3, 2·3=6 > −4 → X++ ├───┼───┼───┼───┼───┼───┼───┼───┤ 2 │ │ │ │ │ │ │ │ │ err=−1, 2·−1=−2 > −4 → X++, 2·−1<7 → Y++ ├───┼───┼───┼───┼───┼───┼───┼───┤ 3 │ │ │ │ │ │ │ │ │ err=5, 2·5=10 > −4 → X++ ├───┼───┼───┼───┼───┼───┼───┼───┤ 4 │ │ │ │ │ │ │ │ │ err=1, 2·1=2 > −4 → X++, 2·1<7 → Y++ ├───┼───┼───┼───┼───┼───┼───┼───┤ 5 │ │ │ │ │ │ err=−3, avance X,X,X,X jusqu'au bout └───┴───┴───┴───┴───┴───┴───┴───┘ → Le segment "colle" au pixel le plus proche de la droite idéale.

Code complet — draw_line

src/pixel.c — draw_linec
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

VariableRôle
dx, dyDistance absolue en X et Y
sx, sySigne du pas (+1 ou −1) selon la direction S→E
errErreur accumulée, initialisée à dx − dy
e2Erreur doublée (évite les flottants pour la comparaison au demi-pixel)
colorCouleur du segment (couleur de S si S et E diffèrent)

Écriture d'un pixel — img_pixel_put

src/pixel.c — img_pixel_putc
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;
}
Décryptage de l'arithmétique de pointeur

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.

src/draw.c — draw_mapc
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 :

0–20% · #432371
20–40% · #714674
40–60% · #9F6976
60–80% · #CC8B79
80–100% · #FAAE7B
Palier (%)CouleurHexSensation
0 – 20% Violet profond0x432371Fonds / creux
20 – 40% Mauve0x714674Pied de pente
40 – 60% Rose taupe0x9F6976Pente moyenne
60 – 80% Saumon0xCC8B79Haut de pente
80 – 100% Orange clair0xFAAE7BSommets

Code — get_color

src/pixel.c — get_colorc
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);
}
Sentinelle -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 :

syntaxe .fdffdf map
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 :

src/pixel.c — extrait draw_linec
        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.

src/pixel.c — draw_hudc
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ériqueEntréeCodeAction
ClavierESC65307Quitter (libère la map, détruit la fenêtre)
Clavier65361Décaler la map à gauche (offset_x − 10)
Clavier65363Décaler la map à droite (offset_x + 10)
Clavier65362Décaler la map vers le haut (offset_y − 10)
Clavier65364Décaler la map vers le bas (offset_y + 10)
Clavier+65451Exagérer le relief (z_height − 0.1)
Clavier65453Aplatir le relief (z_height + 0.1)
ClavierSPACE32Basculer projection isométrique ↔ parallèle
ClavierR114Réinitialiser la caméra (zoom, angles, offset)
SourisMolette hautbtn 4Zoom avant (zoom + 2)
SourisMolette basbtn 5Zoom arrière (zoom − 2, min 1)
SourisClic gauche + dragbtn 1Translater la map (offset_x/y)
SourisClic droit + dragbtn 3Rotation X/Y (selon dx/dy du drag)
SourisClic milieu + dragbtn 2Rotation Z
FenêtreCroix de fermetureevt 17Quitter proprement

Hook clavier — key_hook

src/events.c — key_hookc
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

src/events.c — mod_heightc
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

src/mouse.c — mouse_down / mouse_movec
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

src/mouse.c — mouse_zoomc
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èremlx_key_hookmlx_hook
Événements couvertsClavier uniquement (KeyRelease)Tous : clavier, souris, expose, fermeture
Signature callbackint (*)(int keycode, void*)int (*)(...) — variable selon l'événement
Pression vs relâchementSeulement relâchement (Release)Pression (KeyPress) via event 2
Fermeture fenêtreImpossibleÉvénement 17 (ClientMessage) → croix
Boutons sourisImpossibleÉvénements 4 (ButtonPress), 5 (ButtonRelease), 6 (MotionNotify)
Pourquoi FdF utilise exclusivement 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

CodeNom X11SensCallback FdF
2KeyPressUne touche est presséekey_hook
4ButtonPressBouton souris pressémouse_down
5ButtonReleaseBouton souris relâchémouse_up
6MotionNotifyLa souris bougemouse_move
17ClientMessageFermeture fenêtre (croix)close_hook

Sortie propre — close_hook

src/events.c — close_hookc
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.

BonusStatutComment l'utiliserImplémentation
Rotation interactiveClic droit + drag (X/Y), clic milieu + drag (Z)mouse_move modifie x/y/z_angle
Zoom moletteMolette haut/basmouse_zoom ± 2 sur zoom
Échelle automatiqueAutomatique au chargementft_min(WIN_W/w/2, WIN_H/h/2)
Aplatissement Z+ / mod_height ajuste z_height
Translation clavierFlèches directionnelleskey_hook modifie offset_x/y
Translation sourisClic gauche + dragmouse_move bouton 1
Reset caméraRreset_camera réinit. tous les paramètres
HUD des contrôlesToujours visible (haut-gauche)draw_hud via mlx_string_put
Couleurs customSyntaxe z,0xRRGGBB dans le .fdfparse_color + sentinelle -1
Gradient par altitudeAutomatique si pas de couleur customget_color 5 paliers violet→orange
Projections multiplesSPACE bascule iso/parallèletoggle_projection change les angles
Sortie propre (croix + ESC)ESC ou croix fenêtreclose_hook + handler ESC_KEY

Reset caméra — reset_camera

src/utils.c — reset_camerac
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

src/events.c — toggle_projectionc
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;
}
Conversion degrés → radians

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.

Makefilemakefile
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
Dépendances système

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

shellbash
# 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

FichierDimensionsIntérêt
42.fdf19 × 11Le logo « 42 » en relief
basictest.fdf10 × 9Plan incliné régulier (test du gradient)
elem.fdfMap pédagogique simple
elem-col.fdf10 × 10Map avec couleurs custom (0xFF0000)
elem-fract.fdfRelief fractal
pyramide.fdfPyramide parfaite
pyra.fdfPyramide alternée
pylone.fdfPylône électrique
julia.fdfFractal de Julia en relief
mars.fdfGrande map — test performance
plat.fdfMap totalement plate (test max==0)
pnp_flat.fdfPositif/négatif sur fond plat
pentenegpos.fdfPentes négatives et positives
10-2.fdf · 10-70.fdf · 20-60.fdf · 50-4.fdf · 100-6.fdfvariablesMaps générées largeur-hauteur
t1.fdf · t2.fdfMaps de test rapides

Contrôles complets en jeu

RACCOURCIS — à l'écran ┌─────────────────────────────────────────────────────────────┐CLAVIER │ │ ESC quitter R reset caméra │ │ ← → ↑ ↓ déplacer la map SPACE iso ↔ parallèle │ │ + − exagérer/aplatir Z │ ├─────────────────────────────────────────────────────────────┤SOURIS │ │ molette zoom avant / arrière │ │ clic G glisser pour translater │ │ clic D glisser pour pivoter (X/Y) │ │ clic M glisser pour pivoter (Z) │ ├─────────────────────────────────────────────────────────────┤FENÊTRE croix rouge = quitter propre │ └─────────────────────────────────────────────────────────────┘

Checklist de validation

  1. make compile sans warning ni erreur.
  2. ./fdf sans argument affiche usage: et retourne 1.
  3. ./fdf nonexistent.fdf affiche error: could not parse map et retourne 1.
  4. ./fdf test_maps/42.fdf ouvre une fenêtre avec le logo 42.
  5. ESC et la croix ferment proprement (pas de Segmentation fault).
  6. Les flèches, +/-, SPACE, R répondent instantanément.
  7. La molette zoome, le drag gauche translate, le drag droit pivote.
  8. valgrind --leak-check=full ./fdf test_maps/42.fdf ne rapporte aucune fuite.
  9. Une map plate (plat.fdf) ne crash pas (gestion max == 0).
  10. Les couleurs custom (elem-col.fdf) sont affichées correctement.

Architecture finale — 8 modules, ~700 lignes

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/.