Caméra & projection — 03 — Le pipeline de projection

Le voyage complet d'un point 3D jusqu'au pixel : espace monde, espace caméra, la division perspective, l'espace écran, les coordonnées normalisées (NDC) et enfin l'espace raster. Avec les formules de chaque étape.

03 — Le pipeline de projection

Ce que tu vas apprendre

  • Les cinq espaces traversés par un point, du monde au pixel
  • La division perspective, le cœur de l'effet de profondeur
  • Pourquoi on passe par des coordonnées normalisées (NDC)
  • Comment on retombe enfin sur des coordonnées pixel

Prérequis


On a une caméra réglée. Reste à faire voyager un point 3D jusqu'à son pixel. Ce voyage traverse une suite d'espaces, chacun avec sa transformation. C'est mécanique une fois qu'on connaît les étapes — et c'est exactement ce que fait une carte graphique des millions de fois par image.

Les cinq espaces

  monde  →  caméra  →  écran  →  NDC  →  raster
 (world)   (camera)  (screen)         (pixel i,j)
   M⁻¹      ÷(-z)      normalise     ×résolution

Suivons un point P à chaque étape.

Étape 1 — Monde → caméra

P est défini en espace monde. Pour le projeter, il faut d'abord l'exprimer du point de vue de la caméra. On le multiplie par la matrice monde-vers-caméra, qui est l'inverse de la matrice caméra-vers-monde (celle de LookAt). Comme cette matrice est orthogonale, son inverse est sa transposée — gratuit, comme vu en série maths.

Après cette étape, la caméra est à l'origine et regarde vers les z négatifs. La profondeur d'un point est donnée par son z (négatif s'il est devant la caméra).

Étape 2 — Caméra → écran : la division perspective

Voici le cœur de la perspective. Pour projeter un point sur le plan image, on divise ses coordonnées x et y par sa profondeur :

P'.x = P.x / -P.z
P'.y = P.y / -P.z

(Le signe moins vient de la caméra qui regarde vers les z négatifs.) C'est tout ! Cette simple division crée l'effet de profondeur : plus un point est loin (|z| grand), plus on divise par un grand nombre, donc plus il se rapproche du centre de l'image et paraît petit. Deux objets identiques à des distances différentes se projettent à des tailles différentes. C'est la traduction mathématique de « les rails se rejoignent à l'horizon ».

Le résultat P' vit sur le plan image, dans l'espace écran (screen space) — des coordonnées 2D exprimées dans l'unité du canvas, centrées sur l'axe optique.

Étape 3 — Écran → NDC

Les coordonnées écran dépendent de la taille du canvas. On les normalise dans un intervalle propre et indépendant de la résolution : les coordonnées normalisées (Normalized Device Coordinates, NDC). Selon la convention, NDC va de [0, 1] ou de [-1, 1].

P_ndc.x = (P'.x + largeurCanvas/2) / largeurCanvas      // → [0, 1]
P_ndc.y = (P'.y + hauteurCanvas/2) / hauteurCanvas

L'intérêt des NDC : à ce stade, tout est exprimé en proportions de l'image (« à 30 % de la largeur, 70 % de la hauteur »), sans savoir si l'image finale fera 800 ou 4000 pixels de large. C'est un espace pivot, propre et réutilisable.

Étape 4 — NDC → raster

Dernière marche : convertir les proportions en vraies coordonnées pixel (raster space). On multiplie par la résolution, on prend la partie entière, et on retourne l'axe y (en NDC, y monte ; en raster, y descend, le pixel (0,0) étant en haut à gauche) :

i = floor( P_ndc.x × largeurImagePx )
j = floor( (1 − P_ndc.y) × hauteurImagePx )   // y inversé

(i, j) est enfin l'adresse du pixel. Le voyage est terminé.

En une fonction

Mis bout à bout, voici le squelette du calcul (rasterization) :

cppbool computePixelCoordinates(const Vec3f& pWorld, const Matrix44& worldToCamera,
                             /* canvas, résolution... */ int& i, int& j)
{
  Vec3f pCamera = pWorld * worldToCamera;            // 1. monde → caméra
  Vec2f pScreen;
  pScreen.x = pCamera.x / -pCamera.z;                // 2. division perspective
  pScreen.y = pCamera.y / -pCamera.z;
  Vec2f pNDC;                                         // 3. écran → NDC
  pNDC.x = (pScreen.x + canvasW/2) / canvasW;
  pNDC.y = (pScreen.y + canvasH/2) / canvasH;
  i = floor(pNDC.x * imageW);                         // 4. NDC → raster
  j = floor((1 - pNDC.y) * imageH);
  return /* le point est-il dans le canvas ? (visible) */;
}

Tout ce pipeline suppose que le point est dans le volume visible. Définir ce volume — le frustum — et écarter ce qui en sort, c'est le sujet de l'article suivant.


Sources

  • Scratchapixel. Computing the Pixel Coordinates of a 3D Point — Mathematics of Computing the 2D Coordinates. scratchapixel.com
  • Scratchapixel. Perspective Projection. scratchapixel.com
  • Marschner, S., & Shirley, P. (2021). Fundamentals of Computer Graphics (5ᵉ éd.), chap. « Viewing ». CRC Press.

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.