La 3D des jeux vidéo : vecteurs et produit scalaire Grand Oral · mathématiques, NSI, SI, physique-chimie Un support de connaissances pour construire sa propre question

← Tous les textes du Grand Oral

MathsNSISIPC

Dans un jeu vidéo, le personnage court dans une forêt, la caméra tourne, le soleil éclaire un côté des arbres et laisse l'autre dans l'ombre. Soixante fois par seconde, la console ou l'ordinateur recalcule l'image entière. Derrière ce spectacle, il n'y a que des points dans un repère de l'espace, des vecteurs et un très grand nombre de produits scalaires.

Le cours de terminale contient presque tout ce qu'il faut pour comprendre les quatre étapes de base : décrire un objet par des triangles, savoir quelles faces sont cachées, calculer leur éclairage, puis projeter la scène sur l'écran. Ce texte les reprend une par une, avec des exemples faits à la main et un petit moteur 3D à manipuler.

Ce texte rassemble des connaissances, avec les mathématiques de terminale : vecteurs et repères de l'espace, produit scalaire, orthogonalité, équations de plans et représentations paramétriques de droites, trigonométrie, algorithmique. Il ne propose pas de Grand Oral tout fait : à chacun d'y choisir une direction et d'y construire sa question.

Sommaire
  1. Un monde fait de triangles
  2. Le vecteur normal d'un triangle
  3. Les faces cachées et le signe du produit scalaire
  4. L'éclairage de Lambert
  5. La perspective : projeter sur l'écran
  6. Faire tourner un objet
  7. Un petit moteur 3D
  8. Le calcul complet en Python
  9. De Wolfenstein à Toy Story
  10. Des directions pour construire sa question
Un monde fait de triangles

Un objet de jeu vidéo est décrit par une liste de sommets, c'est-à-dire de points donnés par leurs coordonnées $(x\,;y\,;z)$ dans un repère orthonormé de l'espace, et par une liste de faces, presque toujours des triangles dont on donne les trois sommets. Cet ensemble s'appelle un maillage.

Pourquoi des triangles ? Parce que trois points non alignés définissent toujours un plan : un triangle est forcément plat, alors que quatre points pris au hasard ne sont en général pas coplanaires. Une surface courbe, comme le dos d'un dauphin, est approchée par beaucoup de petits triangles plats, comme un cercle est approché par un polygone. Un personnage de jeu actuel en compte des dizaines de milliers.

Un dauphin dessiné par un maillage de triangles Un dauphin décrit par un maillage de triangles : chaque triangle est plat, mais leur ensemble donne l'illusion d'une surface courbe. Chrschn, domaine public, Wikimedia Commons.

Dans tout le texte, on suivra un objet très simple : le tétraèdre de sommets $O(0\,;0\,;0)$, $A(1\,;0\,;0)$, $B(0\,;1\,;0)$ et $C(0\,;0\,;1)$. Il a quatre faces triangulaires : $ABC$, $OAB$, $OBC$ et $OAC$. Les trois dernières sont dans les plans de coordonnées ; la face $ABC$ est « penchée ».

Le vecteur normal d'un triangle

Pour savoir comment un triangle est orienté (vers la caméra ou non, vers le soleil ou non), on lui associe un vecteur normal $\vec n$ : un vecteur non nul orthogonal au plan du triangle. D'après le cours, il suffit qu'il soit orthogonal à deux vecteurs non colinéaires de ce plan, par exemple $\overrightarrow{AB}$ et $\overrightarrow{AC}$.

Exemple à la main : la face $ABC$. On a $\overrightarrow{AB}(-1\,;1\,;0)$ et $\overrightarrow{AC}(-1\,;0\,;1)$. On cherche $\vec n(a\,;b\,;c)$ tel que :

$$\begin{aligned} \vec n \cdot \overrightarrow{AB} &= -a + b = 0, \\ \vec n \cdot \overrightarrow{AC} &= -a + c = 0. \end{aligned}$$

On obtient $b = a$ et $c = a$ ; en prenant $a = 1$, le vecteur $\vec n(1\,;1\,;1)$ est normal à la face $ABC$. On en déduit d'ailleurs une équation du plan $(ABC)$ : $x + y + z - 1 = 0$, puisque $A$ vérifie $1 + 0 + 0 - 1 = 0$.

Le cas général. Les programmes ne résolvent pas un système à chaque fois : ils utilisent une formule toute prête. Si $\vec u(u_1\,;u_2\,;u_3)$ et $\vec v(v_1\,;v_2\,;v_3)$ sont deux vecteurs du plan, le vecteur $\vec n$ de coordonnées

$$\begin{aligned} n_1 &= u_2 v_3 - u_3 v_2, \\ n_2 &= u_3 v_1 - u_1 v_3, \\ n_3 &= u_1 v_2 - u_2 v_1 \end{aligned}$$

leur est orthogonal. Ce vecteur s'appelle le produit vectoriel de $\vec u$ et $\vec v$ ; il n'est pas au programme, mais on peut vérifier la propriété avec le produit scalaire du cours.

Le vecteur $\vec n$ ci-dessus est orthogonal à $\vec u$ (et de même à $\vec v$)

Étape 1. Dans le repère orthonormé, $\vec n \cdot \vec u = n_1 u_1 + n_2 u_2 + n_3 u_3$. On développe chacun des trois produits :

$$\begin{aligned} n_1 u_1 &= u_1 u_2 v_3 - u_1 u_3 v_2, \\ n_2 u_2 &= u_2 u_3 v_1 - u_1 u_2 v_3, \\ n_3 u_3 &= u_1 u_3 v_2 - u_2 u_3 v_1. \end{aligned}$$

Étape 2. En additionnant ces trois lignes, les six termes s'annulent deux à deux : $u_1 u_2 v_3$ avec $-u_1 u_2 v_3$, $-u_1 u_3 v_2$ avec $u_1 u_3 v_2$, $u_2 u_3 v_1$ avec $-u_2 u_3 v_1$. Donc $\vec n \cdot \vec u = 0$.

Étape 3. Le même calcul avec $\vec v$ donne $\vec n \cdot \vec v = 0$. Si $\vec u$ et $\vec v$ ne sont pas colinéaires, $\vec n$ n'est pas nul et c'est donc un vecteur normal au plan qu'ils dirigent.

L'ordre des sommets compte. Avec $\vec u = \overrightarrow{AB}$ et $\vec v = \overrightarrow{AC}$, la formule donne bien $(1\,;1\,;1)$ pour la face $ABC$. En échangeant $B$ et $C$, on obtiendrait l'opposé $(-1\,;-1\,;-1)$. Les concepteurs de jeux adoptent donc une convention : les sommets de chaque triangle sont rangés dans le sens inverse des aiguilles d'une montre vu de l'extérieur de l'objet. Avec cette convention, la formule donne toujours la normale sortante, qui pointe vers l'extérieur.

Pour le tétraèdre, en rangeant les faces dans l'ordre $(A, B, C)$, $(O, B, A)$, $(O, C, B)$ et $(O, A, C)$, on obtient les quatre normales sortantes :

FaceNormale sortante
$ABC$$(1\,;1\,;1)$
$OBA$$(0\,;0\,;-1)$
$OCB$$(-1\,;0\,;0)$
$OAC$$(0\,;-1\,;0)$

On le vérifie sur la face $OBA$, posée sur le plan $z = 0$ : l'objet est au-dessus, la normale sortante doit donc pointer vers le bas, ce que fait $(0\,;0\,;-1)$.

Les faces cachées et le signe du produit scalaire

Quand on regarde un cube, on ne voit jamais plus de trois faces : les autres sont derrière. Dessiner les faces cachées serait du travail perdu, et surtout donnerait une image fausse si elles étaient dessinées par-dessus les autres. Le test qui les élimine (en anglais, back-face culling) tient en un produit scalaire.

Soit $E$ la position de l'œil (la caméra), $A$ un sommet d'une face et $\vec n$ sa normale sortante. Le plan de la face partage l'espace en deux demi-espaces : celui vers lequel pointe $\vec n$ (l'extérieur de la face) et l'autre. D'après le cours, un point $M$ est du côté de $\vec n$ exactement quand $\vec n \cdot \overrightarrow{AM} > 0$.

Le test des faces cachées

Pour un objet convexe (sans creux, comme un cube ou un tétraèdre) dont les normales sont sortantes, une face est visible depuis $E$ si $\vec n \cdot \overrightarrow{AE} > 0$, et cachée si $\vec n \cdot \overrightarrow{AE} < 0$.

Si $\vec n \cdot \overrightarrow{AE} < 0$, la face est cachée par l'objet lui-même

Étape 1 : l'objet est du côté opposé à $\vec n$. L'objet est convexe : il est tout entier d'un même côté du plan de chacune de ses faces, et la normale sortante pointe vers l'autre côté. Pour tout point $M$ de l'objet, $\vec n \cdot \overrightarrow{AM} \leqslant 0$.

Étape 2 : l'œil est du même côté que l'objet. L'hypothèse $\vec n \cdot \overrightarrow{AE} < 0$ place $E$ dans le demi-espace qui contient l'objet.

Étape 3 : le rayon lumineux traverse l'objet. Soit $P$ un point intérieur à la face. Pour voir $P$, il faut que le segment $[PE]$ ne rencontre pas l'objet. Or les points $P + t\,\overrightarrow{PE}$ avec $t > 0$ petit sont du même côté que $E$, donc du côté de l'objet par rapport au plan de la face, et ils sont tout près de $P$, donc loin des autres faces : ils sont à l'intérieur de l'objet. Le segment $[PE]$ entre dans l'objet dès qu'il quitte $P$ : la lumière venue de $P$ est arrêtée, la face est cachée.

Étape 4. Si au contraire $\vec n \cdot \overrightarrow{AE} > 0$, les points de $[PE]$ proches de $P$ sont du côté extérieur ; comme l'objet convexe est entièrement de l'autre côté du plan, aucun point de $]PE]$ n'est dans l'objet : la face est visible (s'il n'y a pas d'autre objet devant).

Exemple à la main. L'œil est placé en $E(3\,;-2\,;4)$. Pour la face $ABC$ : $\overrightarrow{AE}(2\,;-2\,;4)$ et $\vec n \cdot \overrightarrow{AE} = 2 - 2 + 4 = 4 > 0$ : elle est visible. Pour les trois autres faces, $O$ est un sommet et $\overrightarrow{OE}(3\,;-2\,;4)$ :

FaceProduit scalaireConclusion
$ABC$$4$visible
$OBA$$-4$cachée
$OCB$$-3$cachée
$OAC$$2$visible

Depuis ce point de vue, on voit deux faces sur quatre. Le gain est considérable : pour un objet fermé, environ la moitié des triangles sont éliminés par un seul produit scalaire chacun, avant tout dessin. Pour les objets non convexes (un personnage, une maison avec un toit en L), le test élimine toujours les faces tournées vers l'arrière, mais ne suffit plus : une face tournée vers l'œil peut être masquée par une autre partie de l'objet. Les cartes graphiques ajoutent alors un second outil, le tampon de profondeur, qui garde pour chaque pixel la distance du point le plus proche déjà dessiné.

L'éclairage de Lambert

En 1760, dans sa Photometria, Johann Heinrich Lambert énonce une loi simple pour une surface mate (du plâtre, du papier, un mur) : la lumière qu'elle reçoit est proportionnelle au cosinus de l'angle entre sa normale et la direction de la lumière. Un faisceau qui arrive de face se répartit sur une petite surface ; s'il arrive en biais, le même faisceau s'étale sur une surface plus grande et l'éclaire moins.

Pourquoi un cosinus

Étape 1. Un faisceau de lumière parallèle de section $S$ transporte une puissance $P$ fixée.

Étape 2. Ce faisceau tombe sur une surface plane dont la normale fait un angle $\theta$ avec la direction de la lumière. La surface éclairée est la section $S$ « inclinée » : dans le plan qui contient la normale et le rayon, une largeur $\ell$ du faisceau devient sur la surface une longueur $\dfrac{\ell}{\cos\theta}$ (hypoténuse d'un triangle rectangle), et la largeur perpendiculaire ne change pas. L'aire éclairée vaut donc $\dfrac{S}{\cos\theta}$.

Étape 3. La puissance reçue par unité d'aire vaut $P \div \dfrac{S}{\cos\theta} = \dfrac{P}{S}\cos\theta$ : elle est proportionnelle à $\cos\theta$, maximale quand la lumière arrive de face ($\theta = 0$), nulle quand elle arrive en rasant ($\theta = 90^\circ$).

Or le cosinus de l'angle entre deux vecteurs, c'est exactement ce que donne le produit scalaire : si $\vec n$ et $\vec L$ sont unitaires (de norme $1$), avec $\vec L$ dirigé de la surface vers la lumière,

$$\vec n \cdot \vec L = \|\vec n\| \times \|\vec L\| \times \cos\theta = \cos\theta.$$

Les jeux calculent donc pour chaque face une intensité $I = \max(0\,;\vec n \cdot \vec L)$ : le maximum avec $0$ traduit qu'une face tournée dos à la lumière est dans l'ombre, et ne peut pas recevoir une lumière « négative ». On y ajoute souvent une petite lumière ambiante, pour que les faces à l'ombre ne soient pas complètement noires.

Exemple à la main. Le soleil est dans la direction $(1\,;2\,;2)$, de norme $\sqrt{1 + 4 + 4} = 3$ ; le vecteur unitaire est $\vec L\left(\frac13\,;\frac23\,;\frac23\right)$. La normale de $ABC$, de norme $\sqrt3$, devient $\vec n\left(\frac{1}{\sqrt3}\,;\frac{1}{\sqrt3}\,;\frac{1}{\sqrt3}\right)$.

$$\vec n \cdot \vec L = \frac{1 + 2 + 2}{3\sqrt3} = \frac{5}{3\sqrt3} \approx 0{,}962.$$

La face $ABC$ reçoit $96\,\%$ de l'éclairage maximal. Pour la face $OBA$, de normale $(0\,;0\,;-1)$, on trouve $-\frac23 < 0$ : elle est à l'ombre, de même que $OCB$ ($-\frac13$) et $OAC$ ($-\frac23$).

var opt = {fontSize: 12, useKatex: false, useMathJax: false, parse: false, fixed: true, highlight: false}; board.create('segment', [[-2.2, 0], [2.2, 0]], {strokeColor: '#6d4c41', strokeWidth: 5, fixed: true, highlight: false}); board.create('arrow', [[0, 0], [0, 2.35]], {strokeColor: '#455a64', strokeWidth: 2, fixed: true, highlight: false}); board.create('text', [-0.37, 2.62, 'normale'], {fontSize: 12, useKatex: false, parse: false, fixed: true, highlight: false, color: '#455a64'}); var sa = board.create('slider', [[-2.2, -1.55], [0.6, -1.55], [-90, 30, 90]], {withLabel: false, snapWidth: 1}); function rad() { return sa.Value() * Math.PI / 180; } board.create('arrow', [function () { return [2.2 * Math.sin(rad()), 2.2 * Math.cos(rad())]; }, function () { return [0.25 * Math.sin(rad()), 0.25 * Math.cos(rad())]; }], {strokeColor: '#f9a825', strokeWidth: 3, highlight: false}); board.create('text', [0.75, -1.55, function () { return 'angle = ' + Math.round(sa.Value()) + '°'; }], opt); board.create('text', [-2.45, -0.45, function () { return 'cos = ' + Math.cos(rad()).toFixed(2).replace('.', ','); }], opt); board.create('polygon', [[1.6, -0.25], [2.3, -0.25], [2.3, -1.15], [1.6, -1.15]], {fillColor: '#90a4ae', fillOpacity: 0.15, borders: {strokeColor: '#90a4ae'}, vertices: {visible: false}, fixed: true, highlight: false}); board.create('polygon', [[1.6, -1.15], [2.3, -1.15], [2.3, function () { return -1.15 + 0.9 * Math.max(0, Math.cos(rad())); }], [1.6, function () { return -1.15 + 0.9 * Math.max(0, Math.cos(rad())); }]], {fillColor: '#f9a825', fillOpacity: 0.9, borders: {visible: false}, vertices: {visible: false}, fixed: true, highlight: false}); board.create('text', [0.55, -0.45, 'éclairement'], {fontSize: 11, useKatex: false, parse: false, fixed: true, highlight: false}); Une surface vue de profil, sa normale et un rayon de soleil (en jaune) dont le curseur règle l'angle avec la normale. La jauge à droite donne l'éclairement relatif $\max(0\,;\cos\theta)$ : maximal quand la lumière arrive de face, nul quand elle arrive en rasant.

Ce modèle simple donne des faces de couleur uniforme, d'où l'aspect « à facettes » des premiers jeux en 3D. En 1971, Henri Gouraud propose de calculer l'éclairage aux sommets puis de le faire varier progressivement à l'intérieur de chaque triangle ; en 1973-1975, Bui Tuong Phong ajoute les reflets brillants. Les surfaces paraissent alors lisses, alors qu'elles sont toujours faites de triangles plats.

La perspective : projeter sur l'écran

Il reste à dessiner les triangles visibles sur un écran plat. On place l'œil à l'origine $O$ d'un repère, on le fait regarder dans la direction de l'axe $(Oz)$, et on imagine l'écran comme une fenêtre posée dans le plan d'équation $z = d$, où $d > 0$ est la distance de l'œil à l'écran. Un point $M(x\,;y\,;z)$ de la scène, avec $z > 0$, est vu à l'endroit où la droite $(OM)$ traverse l'écran.

Le point $M(x\,;y\,;z)$ se projette sur l'écran en $M'\left(\dfrac{d\,x}{z}\,;\dfrac{d\,y}{z}\,;d\right)$

Étape 1. La droite $(OM)$ passe par $O$ et a pour vecteur directeur $\overrightarrow{OM}(x\,;y\,;z)$. Une représentation paramétrique est $\big(t\,x\,;t\,y\,;t\,z\big)$, avec $t$ réel.

Étape 2. Le point de la droite qui est sur l'écran vérifie $t\,z = d$, soit $t = \dfrac{d}{z}$ (possible car $z \neq 0$).

Étape 3. En remplaçant, on obtient $M'\left(\dfrac{d\,x}{z}\,;\dfrac{d\,y}{z}\,;d\right)$. On peut retrouver ce résultat par le théorème de Thalès dans le triangle formé par $O$, $M$ et le pied de $M$ sur l'axe $(Oz)$.

Exemple à la main. Avec $d = 1$, le point $M(2\,;1\,;4)$ se projette en $\left(\frac24\,;\frac14\right) = (0{,}5\,;0{,}25)$. Le point $N(2\,;1\,;8)$, deux fois plus loin, se projette en $(0{,}25\,;0{,}125)$ : deux fois plus près du centre. C'est la division par $z$ qui fait que les objets lointains paraissent plus petits et que les rails parallèles semblent se rejoindre à l'horizon.

var opt = {fontSize: 12, useKatex: false, useMathJax: false, parse: false, fixed: true, highlight: false}; board.create('segment', [[0, 0], [10.3, 0]], {strokeColor: '#455a64', strokeWidth: 1, lastArrow: true, fixed: true, highlight: false}); board.create('text', [9.9, -0.35, 'z'], opt); board.create('point', [0, 0], {name: '', size: 3, color: '#263238', fixed: true}); board.create('text', [-0.7, 0.35, 'œil'], opt); board.create('segment', [[2, -0.6], [2, 2.6]], {strokeColor: '#1565c0', strokeWidth: 3, fixed: true, highlight: false}); board.create('text', [1.4, 2.85, 'écran z = d'], {fontSize: 12, useKatex: false, parse: false, fixed: true, highlight: false, color: '#1565c0'}); var sz = board.create('slider', [[0.3, -1.25], [5.5, -1.25], [3, 6, 10]], {withLabel: false, snapWidth: 0.5}); board.create('text', [5.9, -1.25, function () { return 'distance z = ' + sz.Value().toFixed(1).replace('.', ','); }], opt); board.create('segment', [function () { return [sz.Value(), 0]; }, function () { return [sz.Value(), 2]; }], {strokeColor: '#c62828', strokeWidth: 4, highlight: false}); board.create('segment', [[0, 0], function () { return [sz.Value(), 2]; }], {strokeColor: '#c62828', strokeWidth: 1, dash: 2, highlight: false}); board.create('segment', [[2, 0], function () { return [2, 4 / sz.Value()]; }], {strokeColor: '#f9a825', strokeWidth: 6, highlight: false}); board.create('text', [2.3, -0.4, function () { return 'image : ' + (4 / sz.Value()).toFixed(2).replace('.', ','); }], opt); Vue de côté : un poteau rouge de hauteur $2$ placé à la distance $z$ de l'œil, et son image (en jaune) sur l'écran placé à $d = 2$. La hauteur de l'image vaut $\dfrac{2 \times 2}{z}$ : quand le poteau s'éloigne, son image rétrécit.

Les peintres de la Renaissance ont découvert les mêmes règles avec des fils tendus et des fenêtres quadrillées : le texte La perspective en peinture raconte cette histoire.

Faire tourner un objet

Quand le joueur tourne la caméra, ou quand un objet pivote, tous les sommets doivent être déplacés. Pour une rotation d'angle $\theta$ autour de l'axe vertical $(Oy)$, la coordonnée $y$ ne change pas, et le point $(x\,;z)$ tourne dans le plan horizontal comme sur un cercle trigonométrique.

La rotation d'angle $\theta$ autour de $(Oy)$ envoie $(x\,;y\,;z)$ sur $(x\cos\theta - z\sin\theta\,;\,y\,;\,x\sin\theta + z\cos\theta)$

Étape 1. Dans le plan horizontal, on écrit le point en coordonnées polaires : $x = r\cos\varphi$ et $z = r\sin\varphi$, où $r$ est sa distance à l'axe.

Étape 2. Tourner d'un angle $\theta$, c'est garder $r$ et remplacer $\varphi$ par $\varphi + \theta$.

Étape 3. Avec les formules d'addition :

$$\begin{aligned} x' &= r\cos(\varphi + \theta) \\ &= r\cos\varphi\cos\theta - r\sin\varphi\sin\theta \\ &= x\cos\theta - z\sin\theta, \end{aligned}$$ $$\begin{aligned} z' &= r\sin(\varphi + \theta) \\ &= r\sin\varphi\cos\theta + r\cos\varphi\sin\theta \\ &= x\sin\theta + z\cos\theta. \end{aligned}$$

On vérifie qu'une telle rotation conserve les longueurs, car $x'^2 + z'^2 = x^2 + z^2$ grâce à $\cos^2\theta + \sin^2\theta = 1$, et qu'elle conserve aussi les produits scalaires. Conséquence pratique : on peut faire tourner les normales avec les mêmes formules, sans les recalculer. Pour l'exemple, la rotation d'angle $90^\circ$ envoie $A(1\,;0\,;0)$ sur $(0\,;0\,;1)$, c'est-à-dire sur $C$.

Un moteur de jeu enchaîne ces calculs pour chaque sommet, à chaque image : rotations, translations, projection. Ils sont rangés dans des tableaux de nombres (des matrices, étudiées en maths expertes), ce qui permet aux cartes graphiques de les effectuer par milliers en même temps.

Un petit moteur 3D

Voici un moteur 3D réduit à l'essentiel, écrit pour ce texte avec exactement les calculs précédents : rotation des sommets, normales sortantes, test des faces cachées, éclairage de Lambert et projection en perspective. On peut faire tourner le cube ou le tétraèdre, déplacer le soleil, et désactiver le test des faces cachées pour voir ce qui se passe sans lui.

30° 25° 60°

Sans le test des faces cachées, les faces sont dessinées dans l'ordre de la liste, et une face arrière peut recouvrir une face avant : l'image devient incohérente dès que l'objet tourne. Avec le test, chaque face n'est dessinée que si $\vec n \cdot \overrightarrow{AE} > 0$, et l'image est juste pour ces objets convexes. L'intensité de chaque face est $0{,}2 + 0{,}8 \times \max(0\,;\vec n \cdot \vec L)$ : un peu de lumière ambiante, plus la loi de Lambert.

Le calcul complet en Python

Le programme suivant reprend le tétraèdre, l'œil $E(3\,;-2\,;4)$ et le soleil dans la direction $(1\,;2\,;2)$. Pour chaque face, il calcule la normale avec la formule du produit vectoriel, la visibilité par le signe de $\vec n \cdot \overrightarrow{AE}$ et l'éclairement de Lambert. On retrouve les tableaux faits à la main.

from math import sqrt O, A, B, C = (0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1) faces = {"ABC": (A, B, C), "OBA": (O, B, A), "OCB": (O, C, B), "OAC": (O, A, C)} oeil = (3, -2, 4) soleil = (1, 2, 2) def vecteur(P, Q): return (Q[0] - P[0], Q[1] - P[1], Q[2] - P[2]) def scalaire(u, v): return u[0] * v[0] + u[1] * v[1] + u[2] * v[2] def normale(P, Q, R): u, v = vecteur(P, Q), vecteur(P, R) return (u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]) def unitaire(u): n = sqrt(scalaire(u, u)) return (u[0] / n, u[1] / n, u[2] / n) L = unitaire(soleil) for nom in faces: P, Q, R = faces[nom] n = normale(P, Q, R) test = scalaire(n, vecteur(P, oeil)) eclairage = max(0, scalaire(unitaire(n), L)) if test > 0: etat = "visible" else: etat = "cachée" print(nom, "normale", n, "test", test, etat, "éclairage", round(eclairage, 3))

On peut modifier la position de l'œil ou du soleil et prévoir le résultat avant de lancer le programme : par exemple, depuis $E(-3\,;-3\,;-3)$, quelles faces sont visibles ?

De Wolfenstein à Toy Story

Les débuts. Dès les années 1960 et 1970, des laboratoires universitaires dessinent des objets en fil de fer, puis avec des faces pleines. L'université de l'Utah joue un rôle central : c'est là que travaillent Gouraud et Phong, et que Martin Newell modélise en 1975 une théière qui deviendra l'objet de test le plus célèbre de l'image de synthèse, la « théière de l'Utah ». Elle est décrite non par des triangles, mais par des morceaux de surfaces de Bézier, le pendant en trois dimensions des courbes présentées dans le texte Les courbes de Bézier.

La théière de l'Utah en images de synthèse La théière de l'Utah, modélisée par Martin Newell en 1975 avec des surfaces de Bézier, rendue ici en image de synthèse. Dhatfield, CC BY-SA 3.0, Wikimedia Commons.

Les jeux en 3D sur ordinateur personnel. Au début des années 1990, les ordinateurs sont trop lents pour dessiner une vraie scène 3D en temps réel. Les programmeurs trichent avec les mathématiques : dans Wolfenstein 3D (1992), le labyrinthe est une grille plate, et pour chaque colonne de l'écran le programme lance un rayon dans le plan jusqu'au premier mur rencontré (le lancer de rayons) ; la hauteur du mur dessinée est inversement proportionnelle à la distance trouvée, comme dans la formule $\dfrac{d\,y}{z}$. Doom (1993) range les murs dans un arbre qui permet de les trier du plus proche au plus lointain. Ces dates et techniques sont celles que l'on cite couramment ; elles méritent d'être vérifiées dans des sources sur l'histoire du jeu vidéo avant d'être reprises.

Les cartes graphiques. À partir du milieu des années 1990, des cartes spécialisées effectuent elles-mêmes les calculs de sommets, les tests de visibilité et le remplissage des triangles. Leur principe est de faire le même calcul simple sur des millions de données en parallèle : exactement le cas des produits scalaires de ce texte. Ces processeurs graphiques servent aujourd'hui aussi au calcul scientifique et à l'apprentissage automatique (voir le texte Le neurone).

Le cinéma. En 1995, Toy Story, du studio Pixar, est le premier long métrage entièrement réalisé en images de synthèse. Ses images ne sont pas calculées en temps réel : chacune demande plusieurs heures de calcul, avec des modèles d'éclairage bien plus fins que celui de Lambert. Mais le point de départ reste le même : des sommets, des normales et des produits scalaires.

Des directions pour construire sa question

Ce texte n'est pas un Grand Oral. Il rassemble des connaissances dans lesquelles chacun peut choisir une direction, la creuser et formuler sa propre question. Quelques directions possibles :

Pour l'échange avec le jury : les notions du cours à maîtriser

Le jury peut interroger sur les chapitres de spécialité mobilisés. Avec ce thème, il est utile de connaître parfaitement : les vecteurs et les repères de l'espace ; le produit scalaire dans l'espace, sa formule en coordonnées et avec le cosinus ; l'orthogonalité, le vecteur normal et l'équation cartésienne d'un plan ; les représentations paramétriques de droites et l'intersection d'une droite et d'un plan ; les formules trigonométriques d'addition ; les boucles et les fonctions en Python.

Pour réviser ces chapitres, l'application Maths FlashBac reprend tout le cours de spécialité en cartes.

Pour aller plus loin