Tutoriel 13 - Espace caméra

Ogldev

Traducteur

Sylvain DOREMUS

Dans ce tutoriel, nous allons voir comment placer une caméra n'importe où dans le monde 3D.

Commentez

- Titre : Tutoriel 13 - Espace caméra

- Auteur : Ogldev

- Traducteur : Sylvain DOREMUS

- Parution : 14 août 2014

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2014 Etay Meiri. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Navigation

- I. Contexte

- II. Explication du code

- III. Sources

- IV. Remerciements

- Navigation

Navigation

Tutoriel précédent : projection en perspective

Sommaire

Tutoriel suivant : contrôle de la caméra

I - Contexte

Dans les derniers tutoriels, nous avons vu deux types de transformations. Les transformations du premier type sont celles qui changent la position (déplacement), l'orientation (rotation) ou la taille (mise à l'échelle) d'un objet. Ces transformations nous permettent de positionner un objet n'importe où dans un monde en 3D. Les transformations du second type sont les transformations de projection en perspective, qui prennent la position d'un sommet dans le monde en 3D et le projettent dans un monde en 2D (c'est-à-dire : un plan). Une fois que les coordonnées sont en 2D, il est très facile de les mapper directement en coordonnées de l'espace écran. Ces coordonnées sont utilisées pour transformer les primitives dont l'objet est composé (que ce soit des points, lignes ou triangles).

La pièce manquante au puzzle est la position de la caméra. Dans tous les tutoriels précédents, nous avons implicitement considéré que la caméra était simplement positionnée à l'origine de notre espace 3D. En réalité nous voulons avoir la liberté de placer la caméra n'importe où dans le monde et projeter les sommets sur un plan 2D en face d'elle. Cela reflétera la relation réelle entre la caméra et un objet sur l'écran.

Dans l'image suivante nous voyons la caméra positionnée quelque part, nous tournant le dos. On trouve aussi un plan 2D virtuel devant elle et une balle est projetée sur le plan. La caméra est inclinée et le plan suit cette inclinaison. Comme la vue de la caméra est limitée par son angle de vue, la partie visible du plan 2D (infini) est le rectangle. Tout ce qui se trouve hors de ce rectangle sera découpé. Notre objectif est de rendre le rectangle sur l'écran.

Théoriquement, il est possible de générer les transformations qui projetteraient un objet du monde 3D sur un plan 2D face à une caméra placée à une position arbitraire du monde. Cependant les mathématiques associées seraient bien plus complexes que ce que nous avons vu précédemment. Il est beaucoup plus simple de faire cela lorsque la caméra est placée à l'origine du monde 3D, regardant le long de l'axe Z. Par exemple, un objet est placé en (0, 0, 5) et la caméra est en (0, 0, 1), regardant le long de l'axe Z (donc directement vers l'objet). Si nous déplaçons en même temps la caméra et l'objet d'une unité vers l'origine, alors la distance et l'orientation relatives (en termes de direction de la caméra) restent les mêmes, mais la caméra est alors placée à l'origine. Déplacer tous les objets de la scène de cette manière va nous permettre de rendre la scène correctement en utilisant les méthodes préalablement apprises.

L'exemple ci-dessus était simple car la caméra regardait déjà le long de l'axe X et était alignée sur les axes du système de coordonnées. Mais qu'arrive-t-il lorsque la caméra regarde ailleurs ? Regardons l'image suivante. Par simplicité, c'est un système de coordonnées à deux dimensions et nous regardons la caméra du dessus.

À l'origine, la caméra regardait le long de l'axe X mais elle a tourné de 45° dans le sens des aiguilles d'une montre. Comme vous pouvez le voir, la caméra définit son propre système de coordonnées qui peut être identique à celui du monde (image du haut) ou être différente (image du bas). Il y a donc deux systèmes de coordonnées simultanément. Il y a le « système de coordonnées monde » dans lequel les objets sont spécifiés, et le système de coordonnées de la caméra qui est aligné sur les axes de la caméra (cible, haut et droite). Ces deux systèmes de coordonnées sont connus sous le nom de « espace monde » et « espace caméra/vue ».

La balle verte est située en (0, y, z) dans l'espace monde. Dans l'espace caméra elle est située quelque part dans le quart en haut à gauche du système de coordonnées (c'est-à-dire qu'elle a un X négatif et un Z positif). Nous devons déterminer la position de la balle dans l'espace caméra. Nous pourrons alors oublier l'espace monde et utiliser uniquement l'espace caméra. Dans l'espace caméra, la caméra est placée à l'origine et regarde le long de l'axe Z. Les objets sont exprimés de façon relative à la caméra et peuvent être rendus en utilisant les outils que nous avons mis en place.

Dire que la caméra a tourné de 45° dans le sens des aiguilles d'une montre est la même chose que dire que la balle verte a tourné de 45° dans le sens inverse des aiguilles d'une montre. Le mouvement des objets est toujours l'opposé du mouvement de la caméra. Donc plus généralement, nous devons ajouter deux nouvelles transformations et les brancher au pipeline de transformation que nous avions jusqu'à maintenant. Nous devons déplacer les objets de telle manière qu'ils maintiennent leur distance à la caméra pendant son déplacement à l'origine, et nous devons les tourner dans la direction opposée à la direction que la caméra prend.

Déplacer la caméra est très simple si elle est située en (x, y, z), alors la transformation de déplacement est (-x, -y, -z). La raison est directe : la caméra était placée dans le monde en utilisant une transformation de déplacement basée sur le vecteur (x, y, z), donc pour la replacer à l'origine nous devons utiliser la transformation de déplacement opposée. C'est pourquoi la matrice de transformation ressemble à ça :

L'étape suivante est de tourner la caméra vers une cible dont les coordonnées sont exprimées dans l'espace monde. Nous voulons trouver la position des sommets dans le nouveau système de coordonnées défini par la caméra. La vraie question est donc : comment transformer des coordonnées d'un système à un autre ?

Regardons une fois encore l'image ci-dessus. Nous pouvons dire que le système de coordonnées monde est défini par trois vecteurs linéairement indépendants (1, 0, 0), (0, 1, 0) et (0, 0, 1). Linéairement indépendants signifie qu'il n'existe pas de coordonnées x, y et z différentes de zéro telles que x(1, 0, 0) + y(0, 1, 0) + z(0, 0, 1) = (0, 0, 0). En termes géométriques, cela signifie que chaque paire de vecteurs parmi ces trois, définit un plan qui est orthogonal au troisième vecteur (le plan XY est orthogonal à l'axe Z, etc.). Il est facile de voir que le système de coordonnées de la caméra est défini par les vecteurs (1, 0, -1), (0, 1, 0) et (1, 0, 1). Après normalisation de ces vecteurs, on obtient (0.7071, 0, -0.7071), (0, 1, 0) et (0.7071, 0, 0.7071).

L'image suivante montre comment la position d'un vecteur est exprimée dans deux systèmes de coordonnées différents :

Nous savons comment récupérer les vecteurs unitaires qui représentent les axes de la caméra dans l'espace monde et nous connaissons la position du vecteur dans l'espace monde (x, y, z). Nous recherchons le vecteur (x', y', z'). Nous allons maintenant profiter d'une propriété du produit scalaire connue comme « projection orthogonale ». La projection orthogonale est le résultat du produit scalaire entre un vecteur quelconque A et un vecteur unitaire B, dont le résultat est la longueur de A dans la direction de B. En d'autres termes, la projection du vecteur A sur le vecteur B. Dans l'exemple ci-dessus, si nous faisons un produit scalaire entre (x, y, z) et le vecteur unitaire qui représente l'axe X de la caméra, nous obtenons x'. En procédant de la même manière nous pouvons obtenir y' et z'.(x', y', z') est la position de (x, y, z) dans l'espace caméra.

Voyons comment organiser cette connaissance en solution complète pour orienter la caméra. La solution est appelée « UVN camera » et est un système parmi d'autres permettant d'exprimer l'orientation d'une caméra. L'idée est que la caméra est définie par trois vecteurs.

1. N : le vecteur de la caméra vers sa cible. Il est aussi connu sous le nom de « look at » dans certaines documentations 3D. Ce vecteur correspond à l'axe Z.

2. V : pour une personne debout, il s'agit du vecteur pointant de la tête vers le ciel. Si vous écrivez un simulateur de vol et que l'avion est à l'envers, ce vecteur peut aussi ponter vers le sol. Ce vecteur correspond à l'axe Y.

3. U : Ce vecteur pointe de la caméra vers son côté « droit ». Il correspond à l'axe X.

Afin de transformer une position de l'espace monde vers l'espace caméra défini par les vecteurs UVN, nous devons effectuer un produit scalaire entre la position et les vecteurs UVN. Une matrice représente mieux cela :

Dans le code qui accompagne ce tutoriel, vous remarquerez que la variable globale de shader « gWorld » a été renommée « gWVP ». Ce changement reflète la façon par laquelle la série de transformations est connue dans de nombreux livres. WVP signifie « World-View-Projection ».

II - Explication du code

Dans ce tutoriel j'ai décidé de modifier un peu la conception et ai déplacé le code bas niveau de manipulation des matrices de la classe Pipeline vers la classe Matrix4f. La classe Pipeline demande maintenant à la classe Matrix4f de s'initialiser elle-même de diverses manières et concatène les multiples matrices pour créer la transformation finale.

pipeline.h

1. struct {

2. Vector3f Pos;

3. Vector3f Target;

4. Vector3f Up;

5. } m_camera;

La classe Pipeline a quelques membres pour stocker les paramètres de la caméra. Notez que l'axe qui pointe de la caméra vers sa droite est manquant (l'axe U). Il est calculé à la volée en utilisant un produit vectoriel entre les axes cible et haut. De plus il y a une nouvelle fonction nommée SetCamera pour définir ces valeurs.

Math3d.h

1. Vector3f Vector3f::Cross(const Vector3f& v) const

2. {

3. const float _x = y * v.z - z * v.y;

4. const float _y = z * v.x - x * v.z;

5. const float _z = x * v.y - y * v.x;

7. return Vector3f(_x, _y, _z);

8. }

La classe Vector3f possède une nouvelle méthode permettant de calculer le produit vectoriel entre deux Vector3f. Un produit vectoriel entre deux vecteurs, produit un vecteur qui est orthogonal au plan défini par les vecteurs. Cela devient plus intuitif lorsque l'on se souvient que les vecteurs ont une direction et une longueur mais pas de position. Tous les vecteurs ayant les mêmes directions et longueurs sont considérés comme égaux, quelque soit l'emplacement où ils « commencent ». Donc vous pourriez aussi bien faire que ces deux vecteurs commencent à l'origine. Cela signifie que vous pouvez créer un triangle qui a un sommet à l'origine et deux sommets au bout des vecteurs. Ce triangle définit un plan et le produit vectoriel est un vecteur qui est orthogonal à ce plan.

Math3d.h

1. Vector3f& Vector3f::Normalize()

2. {

3. const float Length = sqrtf(x * x + y * y + z * z);

5. x /= Length;

6. y /= Length;

7. z /= Length;

9. return *this;

10. }

Pour générer la matrice UVN nous allons devoir mettre ces vecteurs à l'unité. Cette opération est connue comme « normalisation de vecteur » et est exécutée en divisant chaque composante du vecteur par la longueur du vecteur.

Math3d.h

1. void Matrix4f::InitCameraTransform(const Vector3f& Target, const Vector3f& Up)

2. {

3. Vector3f N = Target;

4. N.Normalize();

5. Vector3f U = Up;

6. U.Normalize();

7. U = U.Cross(Target);

8. Vector3f V = N.Cross(U);

10. m[0][0] = U.x; m[0][1] = U.y; m[0][2] = U.z; m[0][3] = 0.0f;

11. m[1][0] = V.x; m[1][1] = V.y; m[1][2] = V.z; m[1][3] = 0.0f;

12. m[2][0] = N.x; m[2][1] = N.y; m[2][2] = N.z; m[2][3] = 0.0f;

13. m[3][0] = 0.0f; m[3][1] = 0.0f; m[3][2] = 0.0f; m[3][3] = 1.0f;

14. }

Cette fonction génère la matrice de transformation de caméra, qui sera utilisée plus tard par la classe Pipeline. Les vecteurs U, V et N sont calculés et mis dans la matrice en tant que lignes. Comme la position du sommet va être multipliée en tant qu'opérande droit (et donc en tant que vecteur colonne), cela représente un produit scalaire entre U, V et N et la position. Cela génère les trois projections orthogonales qui deviennent les valeurs XYZ de la position en espace caméra.

La fonction est renseignée avec les vecteurs « cible » et « haut ». Le vecteur « droite » est calculé comme le produit vectoriel entre eux. Notez que nous ne faisons pas confiance à l'appelant pour le passage de vecteurs unitaires, donc nous les normalisons de toute façon. Après avoir généré le vecteur U, nous recalculons le vecteur « haut » comme le produit vectoriel entre le vecteur « cible » et le vecteur « droite ». La raison deviendra plus claire dans le futur, lorsque nous commencerons à déplacer la caméra. Il est plus simple de mettre à jour uniquement le vecteur « cible » et de laisser le vecteur « haut » non modifié. Cependant cela signifie que l'angle entre le vecteur « cible » et le vecteur « haut » ne sera pas de 90°, ce qui nous générera un système de coordonnées invalide. En recalculant le vecteur « droite » avec un produit vectoriel entre les vecteurs « cible » et « haut », puis en recalculant le vecteur « haut » avec un produit vectoriel entre les vecteurs « cible » et « droite », nous obtenons un système de coordonnées avec des angles droits entre chaque paire d'axes (un système orthonormé).

Pipeline.cpp

1. const Matrix4f* Pipeline::GetTrans()

2. {

3. Matrix4f ScaleTrans, RotateTrans, TranslationTrans, CameraTranslationTrans, CameraRotateTrans, PersProjTrans;

5. ScaleTrans.InitScaleTransform(m_scale.x, m_scale.y, m_scale.z);

6. RotateTrans.InitRotateTransform(m_rotateInfo.x, m_rotateInfo.y, m_rotateInfo.z);

7. TranslationTrans.InitTranslationTransform(m_worldPos.x, m_worldPos.y, m_worldPos.z);

8. CameraTranslationTrans.InitTranslationTransform(-m_camera.Pos.x, -m_camera.Pos.y, -m_camera.Pos.z);

9. CameraRotateTrans.InitCameraTransform(m_camera.Target, m_camera.Up);

10. PersProjTrans.InitPersProjTransform(m_persProj.FOV, m_persProj.Width, m_persProj.Height, m_persProj.zNear, m_persProj.zFar);

12. m_transformation = PersProjTrans * CameraRotateTrans * CameraTranslationTrans * TranslationTrans * RotateTrans * ScaleTrans;

13. return &m_transformation;

14. }

Mettons à jour la fonction qui génère la matrice de transformation complète pour un objet. Elle devient assez complexe avec deux nouvelles matrices qui fournissent la partie caméra. Après le calcul de la transformation monde (la combinaison du déplacement, de la rotation et de la mise à l'échelle de l'objet), nous commençons la transformation de la caméra, en « déplaçant » la caméra à l'origine. Cela est fait par le déplacement en utilisant le vecteur négatif de la position de la caméra. Donc si la caméra est positionnée en (1, 2, 3) nous devons déplacer l'objet de (-1, -2, -3) afin d'avoir la caméra à l'origine. Après cela, nous générons la matrice de rotation de la caméra basée sur les vecteurs « cible » et « haut » de la caméra. Cela termine la partie caméra. Enfin, nous projetons les coordonnées.

Tutorial13,cpp

1. Vector3f CameraPos(1.0f, 1.0f, -3.0f);

2. Vector3f CameraTarget(0.45f, 0.0f, 1.0f);

3. Vector3f CameraUp(0.0f, 1.0f, 0.0f);

4. p.SetCamera(CameraPos, CameraTarget, CameraUp);

Nous utilisons la nouvelle fonctionnalité dans la boucle de rendu. Pour placer la caméra nous reculons par rapport à l'origine sur l'axe Z négatif, puis nous la déplaçons sur la droite et vers le haut. Le vecteur « haut » est tout simplement l'axe Y positif. Nous définissons tout cela dans l'objet Pipeline, la classe Pipeline s'occupe du reste.

III - Sources

Vous pouvez télécharger les sources de ce projet en suivant ce lien :

Récupérer les sources

IV - Remerciements

Merci à Etay Meiri qui nous a permis de traduire son tutoriel.

Merci à LittleWhite pour ses corrections et à jacques_jean pour sa relecture orthographique.

Navigation

Tutoriel précédent : projection en perspective

Sommaire

Tutoriel suivant : contrôle de la caméra