- C 94%
- Makefile 6%
| src | ||
| .gitignore | ||
| Makefile | ||
| README.md | ||
| WORKDOC.md | ||
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 :
#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
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.
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.
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) :
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. |