# Moteur 3D DOS (TESTING) ## Vue d'ensemble Moteur 3D software en C ciblant le DOS 32-bit via l'extender **CauseWay** (ou DOS/4GW). Compilateur : **OpenWatcom wcc386** exécuté depuis WSL. Affichage : **VGA Mode 13h** — 320×200, 256 couleurs, rendu Gouraud en virgule fixe 16.16. * **Rendu 3D complet :** Filaire (Wireframe), Lignes cachées (Hidden), et Plein (Solid). * **Rasterisation :** Remplissage de triangles avec interpolation de Gouraud (16.16) et Z-Buffer 16 bits. * **Mathématiques :** Arithmétique en virgule fixe (16.16) et tables trigonométriques pré-calculées. * **Génération procédurale :** Création dynamique de géométries (Cubes, Sphères, Cylindres, Cônes, Plans, Tores, Surfaces mathématiques). * **Mode "Minimal Player" :** Capacité d'exporter la scène entière en un header statique C pour compiler un exécutable ultra-léger sans les fonctions de génération mathématique. --- ## Structure du projet ``` src/ main/main.c Point d'entrée, scène, boucle principale part3D/ defines.h Types, macros fixed-point, constantes écran, prototypes, externs engine.c Maths (sin/cos), matrices, pipeline de rendu geometry.c Génération de meshes (sphère, cylindre) + I/O mesh graph.c VGA init, palette, backbuffer/zbuffer raster.c Rastériseur triangle Gouraud, tracé de ligne Z-buffered build/ app.exe Binaire DOS final (CauseWay) link.lnk Script de link généré automatiquement Makefile Build via WSL + wcc386 Windows natif ``` --- ## Pipeline de rendu ``` init_engine_math() Précalcul sin/cos (tables entières 16.16) add_sphere / add_cylinder Génération mesh en espace local (0,0,0) ─── Boucle principale ─── kbhit / getch Gestion clavier (touches étendues consommées en deux appels) mat3_rotate_x/y Construction matrices de rotation mat3_mul (orbite) Composition de la matrice d'orbite (sûre contre l'aliasing) → obj.rot = orbit Application uniforme à tous les objets → obj.pos = orbit × base_pos + center Rotation 3D complète des positions clear_buffers Effacement backbuffer + zbuffer (0xFFFF) render_universe Transformation + projection + Gouraud fill flip memcpy backbuffer → 0xA0000 (VGA) ``` --- ## Arithmétique virgule fixe 16.16 | Macro/fonction | Opération | |---|---| | `int_to_f(a)` | `a << 16` | | `f_to_int(a)` | `a >> 16` | | `f_mul(a,b)` | `(int64_t)a * b >> 16` | | `f_div(a,b)` | `(int64_t)a << 16 / b` | --- ## Constantes écran Définies dans `defines.h` : ```c #define SCREEN_W 320 #define SCREEN_H 200 #define SCREEN_PIXELS (SCREEN_W * SCREEN_H) /* 64 000 */ ``` Utilisées partout dans `graph.c` et `raster.c` à la place du littéral `64000`. --- ## Palette VGA 256 entrées organisées en **16 couleurs × 16 niveaux d'intensité**. Index pixel = `(base_color << 4) | shade` avec shade ∈ [0..15]. Les 16 couleurs de base sont dans `my_palette[]` (`graph.c`). La palette est chargée dans le DAC VGA au démarrage via `setup_vga_palette()`. --- ## Projection perspective ```c v_cache[i].x = 160 + f_to_int(f_div(rx << 8, pz)); v_cache[i].y = 100 - f_to_int(f_div(ry << 8, pz)); ``` `<< 8` = distance focale implicite de 256 pixels. `pz` est clampé à `int_to_f(20)` minimum pour éviter la division par zéro. --- ## Back-face culling et convention de winding Le pipeline applique la **transposée** de la matrice de rotation aux sommets (convention colonne Watcom). Cette transposition inverse le handedness des triangles projetés. **Convention retenue :** winding CCW vu de l'extérieur → produit vectoriel 2D **> 0** en espace écran → face visible. ```c front = ((p2->x - p1->x) * (p3->y - p1->y) - (p2->y - p1->y) * (p3->x - p1->x)) > 0; ``` Toutes les géométries (sphère, corps cylindre, fonds cylindre) respectent cette convention. --- ## Shading Gouraud La lumière est assimilée à la direction de vue (−Z monde). Le dot product de la normale rotée avec (0,0,−1) est `−nz`. ```c nz = f_mul(n->x, rot.m[0][2]) + f_mul(n->y, rot.m[1][2]) + f_mul(n->z, rot.m[2][2]); intensity = (nz >= 0) ? int_to_f(1) : f_mul(-nz, int_to_f(14)) + int_to_f(1); ``` `f_mul(-nz, int_to_f(14))` est une multiplication en virgule fixe 16.16 (≠ multiplication entière). | nz | Signification | Intensité (shade) | |---|---|---| | −1.0 | Face directement vers caméra | 15 (max) | | 0 | Face tangentielle | 1 (ambiant) | | > 0 | Face opposée à la caméra | 1 (ambiant, face culléé) | L'intensité est interpolée en virgule fixe 16.16 entre les sommets du triangle. Le shade final est clampé dans [0, 15] — y compris vers le bas (intensité négative → 0). --- ## Génération des meshes ### Sphère (`add_sphere`) ``` Object3D *add_sphere(Universe *uni, fixed x, fixed y, fixed z, fixed r, int det, uint8_t col, uint8_t wire); ``` Paramètres : centre local, rayon `r`, détail `det` (nombre de subdivisions). Génère `(det+1) × det` sommets et `det × det × 2` faces. Les normales sont les vecteurs de position unitaires (normales sphériques exactes). Winding : `add_face(a, b, c)` et `add_face(b, d, c)` — CCW vu de l'extérieur. ### Cylindre (`add_cylinder`) ``` Object3D *add_cylinder(Universe *uni, fixed x, fixed y, fixed z, fixed r, fixed h, int det, uint8_t col, uint8_t wire); ``` Paramètres : centre local, rayon `r`, hauteur `h`, détail `det`. **Corps (barrel) :** `det` paires de sommets bas/haut. Normales radiales `(cos θ, 0, sin θ)`. `2×det` faces. Winding : `(bas_i, haut_i, bas_{i+1})` et `(haut_i, haut_{i+1}, bas_{i+1})`. **Fond bas :** 1 sommet centre + `det` sommets de rebord. Normale `(0, −1, 0)`. `det` faces en éventail depuis le centre. Winding : `(centre, rebord[i], rebord[i+1])`. **Fond haut :** 1 sommet centre + `det` sommets de rebord. Normale `(0, +1, 0)`. `det` faces en éventail depuis le centre. Winding inversé (normale opposée) : `(centre, rebord[i+1], rebord[i])`. **Total par cylindre (det=16) :** 2+4×det = 66 sommets, 4×det = 64 faces. ### Boîte / Cube (`add_box`) ``` Object3D *add_box(Universe *uni, fixed x, fixed y, fixed z, fixed dx, fixed dy, fixed dz, uint8_t col, uint8_t wire); ``` Paramètres : centre local, dimensions indépendantes `dx, dy, dz`. Génère 24 sommets (4 par face pour ne pas lisser les arêtes avec Gouraud) et 12 faces. Normales : alignées sur les axes orthogonaux (±X, ±Y, ±Z). ### Cône (`add_cone`) ``` Object3D *add_cone(Universe *uni, fixed x, fixed y, fixed z, fixed r, fixed h, int det, uint8_t col, uint8_t wire); ``` Paramètres : centre local, rayon `r`, hauteur `h`, détail `det`. Dérivé du cylindre, mais la boucle supérieure converge en un point unique avec des sommets dupliqués pour préserver l'interpolation radiale des normales du corps. Ne possède qu'un "fond bas". ### Plan / Grille (`add_plane`) ``` Object3D *add_plane(Universe *uni, fixed x, fixed y, fixed z, fixed w, fixed d, int seg_x, int seg_z, uint8_t col, uint8_t wire); ``` Paramètres : centre local, largeur `w`, profondeur `d`, segments `seg_x`, `seg_z`. Génère une surface plane subdivisée (idéal pour les sols et l'ombrage Gouraud par point). Normales : dirigées vers le haut (+Y). ### Tore (`add_torus`) ``` Object3D *add_torus(Universe *uni, fixed x, fixed y, fixed z, fixed r_main, fixed r_tube, int seg_main, int seg_tube, uint8_t col, uint8_t wire); ``` Paramètres : centre local, rayon principal `r_main`, rayon du tube `r_tube`, subdivisions `seg_main`, `seg_tube`. Génère un anneau ("donut") via deux boucles trigonométriques imbriquées. Excellente primitive de test pour l'éclairage. ### Surface Mathématique (`add_math_surface`) ``` Object3D *add_math_surface(Universe *uni, fixed x, fixed y, fixed z, fixed w, fixed d, int seg_x, int seg_z, uint8_t col, uint8_t wire, SurfaceFunc func); ``` Paramètres : similaires au plan, plus un pointeur de fonction `SurfaceFunc`. Cette primitive génère une grille dont la hauteur Y est calculée dynamiquement par la fonction utilisateur passée en paramètre. La fonction utilisateur doit respecter la signature : `fixed ma_fonction(fixed x, fixed z)`. ``` /* Exemple de formule : le "Sombrero" */ fixed mon_onde(fixed x, fixed z) { double dx = (double)x / 65536.0; double dz = (double)z / 65536.0; double dist = sqrt(dx * dx + dz * dz); return (fixed)(20.0 * sin(dist / 10.0) * 65536.0); } /* Appel dans le main */ add_math_surface(uni, 0, 0, 0, int_to_f(120), int_to_f(120), 30, 30, COLOR6, 0, mon_onde); ``` --- ## Manipulation des Mesh Une fois générés, les objets peuvent être positionnés, transformés ou recolorés à l'aide des fonctions suivantes (définies dans `engine.c` / `geometry.c`) : ### Placement de l'objet (`set_object_pos`) ``` void set_object_pos (Object3D *obj, fixed x, fixed y, fixed z); ``` Définit la position locale absolue de l'objet (`base_pos`) par rapport au centre de son espace d'orbite. ### Deplacement de l'objet (`move_object`) ``` void move_object (Object3D *obj, fixed dx, fixed dy, fixed dz); ``` Applique une translation relative à l'objet en ajoutant le décalage spécifié à sa position courante. ### Changement de la couleur de l'objet (`set_object_color`) ``` void set_object_color (Object3D *obj, uint8_t col); ``` Modifie l'index de couleur de base (0-15) utilisé pour le rendu de l'ensemble des faces de l'objet. ### Rotation de l'objet dans l'univers (`bake_object_rotation`) ``` void bake_object_rotation(Universe *uni, Object3D *obj, int ax, int ay, int az); ``` Applique une rotation *permanente* (Baking) aux sommets et aux normales de l'objet directement dans le pool de l'univers. Particulièrement utile pour orienter une primitive lors de l'initialisation (ex: coucher un cylindre sur le flanc) sans induire le moindre coût de calcul matriciel supplémentaire pendant la boucle de rendu en temps réel. --- ## Orbite de l'observateur (`main.c`) Les objets ont des positions de base fixes dans l'espace scène, définies par rapport au centre `(0, 0, 300)` : | Objet | Offset base | |---|---| | Sphère (universe[0]) | (+60, 0, 0) | | Cylindre (universe[1]) | (−60, 0, 0) | Chaque frame, la matrice d'orbite `orbit = rotX(anglex) × rotY(angley)` est calculée et appliquée uniformément à chaque objet (transform 3D complet sur `base_pos`) : ```c obj->rot = orbit; obj->pos.x = f_mul(bx, orbit.m[0][0]) + f_mul(by, orbit.m[1][0]) + f_mul(bz, orbit.m[2][0]); obj->pos.y = f_mul(bx, orbit.m[0][1]) + f_mul(by, orbit.m[1][1]) + f_mul(bz, orbit.m[2][1]); obj->pos.z = int_to_f(300) + f_mul(bx, orbit.m[0][2]) + f_mul(by, orbit.m[1][2]) + f_mul(bz, orbit.m[2][2]); ``` Les trois composantes de `base_pos` (bx, by, bz) sont toutes prises en compte — les objets à `base_pos.y != 0` ou `base_pos.z != 0` sont correctement transformés. La caméra reste à l'origine (+ translations clavier). L'effet visuel est celui d'un observateur qui orbite autour de la scène. --- ## Contrôles clavier | Touche | Action | |---|---| | Z / S | Caméra avance / recule (axe Z) | | Q / D | Caméra gauche / droite (axe X) | | A / E | Caméra haut / bas (axe Y) | | + / − | Orbite vertical (anglex ±3°) | | / / * | Orbite horizontal (angley ±3°) | | 1 / 2 / 3 | Mode fil-de-fer / caché / solide | | R | Reset caméra + angles | | Esc | Quitter | Les touches étendues (F1–F12, flèches…) retournent `0` depuis `getch()` ; le scan code suivant est consommé sans effet. --- ## Points de vigilance | Sujet | Note | |---|---| | Winding | Toutes les géométries doivent respecter la convention CCW (cross > 0). Vérifier à chaque ajout de primitive. | | Caméra sans rotation | La caméra n'a pas de matrice de vue — elle regarde toujours en +Z. L'orbite est simulée en faisant tourner la scène. Ajouter une matrice de vue si un vrai look-around est nécessaire. | | Clamp pz | pz clampé à int_to_f(20). Si un objet passe derrière la caméra (pz < 0 avant clamp), il sera projeté incorrectement. | | Modes fil-de-fer | MODE_WIRE, MODE_HIDDEN opérationnels. Le champ `face.force_wire` permet de forcer le fil-de-fer face par face en MODE_SOLID. | | Z-test | `scanline_gouraud_c` et `draw_line_z` utilisent tous les deux `zv < zbuf` (strict), garantissant un comportement cohérent. | | mat3_mul | Sûre contre l'aliasing (res peut être identique à a ou b). | | I/O mesh | `save_mesh` / `load_mesh` / `free_mesh` disponibles dans `geometry.c` mais non utilisées depuis `main.c`. | | Face.color | Le champ `face.color` a été supprimé : la couleur est portée par `obj->color`. `face.force_wire` reste le seul flag par-face. |