Vous êtes sur la page 1sur 430

Guide de programmation

Guide de programmation
Programmez
des jeux vidéo 3D
avec

C#5 et WPF

Avec comme exemple


le jeu du
Casse-Briques

Patrice REY
Tous les noms de produits ou marques cités dans ce livre sont
des marques déposées par leurs propriétaires respectifs.
La loi du 11 Mars 1957 n’autorisant aux termes des alinéas 2 et 3 de l’article 41, d’une
part, que les «copies ou reproductions strictement réservées à l’usage privé du copiste
et non destinées à une utilisation collective», et, d’autre part, que les analyses et les
courtes citations dans un but d’exemple et d’illustration, «toute représentation ou
reproduction intégrale, ou partielle, faite sans le consentement de l’auteur ou de ses
ayants droit ou ayant cause, est illicite» (alinéa 1er de l’article 40). Cette représentation
ou reproduction, par quelque procédé que ce soit, constituerait donc une contre-
façon sanctionnée par les articles 425 et suivants du Code Pénal.

Initiation au jeu 2D avec SILVERLIGHT 4


TITRES DISPONIBLES EN LIBRAIRIE

(ISBN : 978-2-8106-1168-3)
Les structures de données illustrées avec WPF et C#4
(ISBN : 978-2-8106-1387-8)
La programmation graphique 2D de WPF 4
(ISBN : 978-2-8106-1199-7)
La programmation graphique 3D de WPF 4
(ISBN : 978-2-8106-2086-9)
Guide pratique de la modélisation 3D avec WPF 4
(ISBN : 978-2-8106-1308-3)
Développez des applications internet avec SILVERLIGHT 5
(ISBN : 978-2-8106-2222-1)
La 3D avec SILVERLIGHT 5
(ISBN : 978-2-8106-2512-3)
Le traitement d’images avec SILVERLIGHT 5
(ISBN : 978-2-8106-2615-1)

Books on Demand GmbH,


12/14 rond point des Champs Élysées,
EDITEUR

75008 Paris, France


Impression :
Books on Demand GmbH, Norderstedt, Allemagne
ISBN : 978-2-322-03237-2
Dépôt légal : Mai 2013
Patrice REY
AUTEUR

85 rue de vincennes
App 23 B
33000 BORDEAUX
e-mail : reypatrice@orange.fr
Table des matières
Le code source de programmation
est téléchargeable gratuitement
à l’adresse internet
http://www.reypatrice.fr

Avant-propos ........................................................................................ 9

Première partie : Apprentissage de la modélisation 3D


Chapitre 1 - Point, vecteur et matrice dans l’espace 3D
1. Le repère des coordonnées 3D ............................................... 18
2. La structure Point3D .................................................................... 19
3. La notion de vecteur ................................................................. 25
4. La structure Vector3D ................................................................ 27
5. Coordonnées euclidiennes et homogènes ............................... 42
5.1 - Transformations en coordonnées euclidiennes .................. 42
5.2 - Définition des coordonnées homogènes ............................ 51
5.3 - Transformations en coordonnées homogènes ................... 52
5.4 - La structure Point4D .............................................................. 56
6. La structure Matrix3D ..................................................................... 57
6.1 - Opérations sur la matrice 3D ............................................... 59
6.2 - Transformations sur la matrice 3D ...................................... 63
Chapitre 2 - Les projections
1. Les types de projection ............................................................... 72
1.1 - Les projections perspectives .................................................. 73
1.2 - Les projections parallèles ....................................................... 75
2. La matrice de la projection perspective .................................. 77
3. La matrice de la projection parallèle ....................................... 85
6 Table des matières

Chapitre 3 - Les transformations 3D


1. Les classes des transformations 3D ............................................. 90
2. La transformation ScaleTransform3D ........................................... 90
3. La transformation TranslateTransform3D ..................................... 96
4. La transformation RotateTransform3D ........................................ 99
5. La transformation MatrixTransform3D ......................................... 104
6. Combinaison de transformations 3D ............................................ 109
Chapitre 4 - La scène 3D
1. Visualisation de la scène ............................................................... 112
2. Le contrôle Viewport3D ................................................................. 112
3. La création de modèles 3D .......................................................... 114
3.1 - La classe ModelVisual3D ......................................................... 114
3.2 - La classe UIElement3D ............................................................. 116
3.3 - La classe GeometryModel3D ................................................. 117
4. La géométrie Geometry3D .......................................................... 118
5. Matière et surface .......................................................................... 123
5.1 - La surface mate ....................................................................... 124
5.2 - La surface lumineuse .............................................................. 125
5.3 - Combinaison de matières ..................................................... 125
5.4 - La réflexion spéculaire ........................................................... 126
6. L’éclairage de la scène ................................................................ 127
7. Les caméras ................................................................................... 129
Chapitre 5 - Les modèles 3D basiques
1. La modélisation basique .............................................................. 134
1.1 - La facette triangulaire ............................................................ 134
1.2 - La facette rectangulaire ........................................................ 144
2. La modélisation du cube ............................................................ 150
2.1 - La géométrie du maillage ...................................................... 150
2.2 - Le placage de texture ............................................................ 156
2.3 - Visualisation des cubes ........................................................... 160
Table des matières 7

2.4 - Les axes de coordonnées et le plan de travail ................... 164


3. La modélisation du cylindre ......................................................... 172
3.1 - Les coordonnées sphériques ................................................. 172
3.2 - La géométrie du cylindre ...................................................... 173
3.3 - La facette cylindrique ........................................................... 175
3.4 - Les formes polygonales ......................................................... 185
4. La modélisation du cône .............................................................. 188
5. La modélisation de la sphère ...................................................... 195
6. La modélisation du tore ............................................................... 204
Chapitre 6 - L’interactivité des modèles 3D
1. Hériter de la classe UIElement3D .................................................. 212
2. Implémenter le test d’atteinte ..................................................... 221
3. Implémenter un Trackball ............................................................. 227
4. Se rapprocher et s’éloigner de la scène .................................... 235
5. Déplacer des objets sur la scène ................................................ 237
6. Tourner des objets sur la scène ................................................... 244

Deuxième partie : Animation 3D du jeu vidéo


Chapitre 7 - Construction du jeu du Casse-Briques
1. Visualisation avec Windows 7 et avec Windows 8 ...................... 252
2. Mise en place du jeu .................................................................... 255
2.1 - Les dimensions utiles ............................................................... 255
2.2 - Les éléments de la scène 3D ................................................. 258
2.3 - Le rendu d’animation ............................................................ 273
2.4 - Le déplacement de la raquette .......................................... 278
2.5 - L’animation de la balle ......................................................... 280
3. Les collisions ................................................................................... 288
3.1 - Collision entre la balle et la raquette .................................... 288
3.2 - Ajouter les briques .................................................................. 297
3.3 - Collision entre la balle et la brique ...................................... 302
4. Affichage du temps et du score .................................................. 306
5. La fin de jeu .................................................................................... 310
Chapitre 8 - Bonification du jeu par l’animation
1. Les règles de l’animation .............................................................. 316
2. La transition entre deux écrans .................................................... 322
2.1 - La réalisation en image ......................................................... 322
2.2 - Programmer un storyboard avec XAML .............................. 325
2.3 - Animer un composant par un storyboard ........................... 333
2.4 - Programmer un storyboard avec C# ................................. 341
3. Ajouter l’effet d’un éclair électrique .......................................... 345
4. Utilisation des animations réalistes ................................................ 349
4.1 - Principe des animations réalistes .......................................... 349
4.2 - Animation réaliste appliquée à une brique ........................ 358
5. Simuler le roulement de la balle .................................................. 367
6. Générer des surfaces de plateau ................................................ 378
7. Importer des objets 3D au format Wavefront OBJ ....................... 385
7.1 - Exporter depuis MAYA au format OBJ ................................. 386
7.2 - Le format Wavefront OBJ ..................................................... 387
7.3 - Importation au format OBJ .................................................. 390
Chapitre 9 - Sonorisation du jeu
1. La prise en charge de l’audio ....................................................... 400
1.1 - Le contrôle MediaElement .................................................... 400
1.2 - Manipulation d’un fichier audio ........................................... 402
2. Ajouter des séquences sonores au jeu .................................... 413
Index ....................................................................................................... 419
Avant-propos

WPF (Windows Presentation Foundation) est une technologie de développement


d’application riche pour Windows (Rich Desktop Application). La première version de
WPF est sortie avec .NET 3.0 en novembre 2006. Au moment où est rédigé ce livre, c’est
la version WPF 4.5 qui est utilisée avec C#5. Par rapport aux technologies graphiques
antérieures, WPF innove à la fois sur le plan architectural et sur le plan fonctionnel. WPF
constitue une synthèse et une unification des différentes branches de l’informatique
graphique (graphismes vectoriels 2D et 3D, typographie, impression, saisie, animation
et multimédia).
WPF est un framework managé qui exploite la puissance de Direct3D pour l’affichage.
Cette technologie de développement d’interface utilisateur permet de moderniser et
d’unifier les techniques de développement visuel, d’exploiter la puissance graphique
des machines modernes, d’intégrer l’infographie dans le processus de développement,
et de simplifier et de sécuriser le déploiement des applications.
En s’appuyant sur Direct3D, WPF exploite toute la puissance du GPU (Graphic Processing
Unit ou le processeur graphique), ce qui permet de libérer le CPU (Central Processing
Unit) et d’envisager des applications graphiques de plus en plus riches. WPF sépare
dans des couches distinctes la programmation des fonctionnalités (au moyen d’un
langage .NET tel que le C#) de la présentation visuelle par l’utilisation du langage XAML
(eXtensible Application Markup Language).
Ce livre s’adresse au développeur et au programmeur, débutant et confirmé, qui
souhaite découvrir et approfondir la modélisation 3D au travers de la réalisation d’un
jeu de Casse-Briques dans un environnement complet 3D (avec C#, XAML et WPF).
10 Avant-propos

Les logiciels requis pour le développement

Pour réaliser plus facilement des programmes en C# et XAML, il est préférable d’utiliser
un environnement de développement intégré ou IDE (Integrated Development
Environment). Cet IDE permet de développer, de compiler et d’exécuter un programme
dans un langage donné qui sera, dans ce livre, le C# (se prononce C Sharp) dans sa
version 5. Microsoft propose comme IDE soit Visual Studio 2012, dans sa version
payante avec 90 jours d’essai gratuit (figure A1), soit Visual Studio 2012 Express, dans
sa version allégée mais gratuite (figure A2).
Puissante interface IDE garantissant la production d’un code de qualité, Visual Studio
2012 intègre de nouvelles fonctionnalités qui simplifient le processus de développement
d’application, de la conception au déploiement.
Pour télécharger et installer l’environnement de développement intégré, ouvrez votre
navigateur et allez sur la page web suivante: http://msdn.microsoft.com/fr-fr/express/
aa975050.aspx

FIGURE A1

1
Avant-propos 11
FIGURE A2

Le contenu du livre

La première partie du livre est consacrée à l’apprentissage de la modélisation 3D


avec C#, XAML et WPF. Les différents chapitres de cette première partie permettent
d’apprendre à modéliser des objets 3D, de les insérer dans la scène 3D, et de pouvoir
visualiser la scène sous différents angles grâce aux manipulations 3D avec la souris.
La deuxième partie du livre est consacrée à la mise en place des modélisations 3D sur
une scène et à la création d’un rendu d’animation pour donner vie à un jeu de Casse-
Briques.
Les chapitres abordent les points suivants:
• le chapitre 1 traite des notions de point, de vecteur et de matrice dans l’espace 3D;
ces trois notions, en géométrie vectorielle, sont fondamentales et essentielles dans
le développement 3D.
• le chapitre 2 traite de la projection perspective et de la projection parallèle au travers
de leur définition et de leurs caractéristiques.
• le chapitre 3 traite de l’utilisation des transformations 3D (translation, rotation, mise
à l’échelle, matrice de transformation); les transformations 3D peuvent être utilisées
seules ou en combinaisons.
12 Avant-propos
• le chapitre 4 traite de la visualisation d’une scène 3D avec un contrôle WPF essentiel
qu’est le Viewport3D; une scène 3D est un assemblage d’un ensemble d’objets que
sont les modélisations 3D, la caméra, les lumières, etc.; les objets 3D peuvent être
texturés et leur rendu peut varier en fonction de leur surface mate, réfléchissante,
avec ou pas une lumière incidente.
• le chapitre 5 traite de la modélisation 3D; à partir de la modélisation 3D d’une surface
triangulaire, tous les modèles 3D du plus simple (cube, sphère, cylindre, cône, tore)
au plus compliqué peuvent être modélisés; la notion de coordonnée sphérique sera
abordée.
• le chapitre 6 traite de l’interactivité des modèles 3D; par héritage de la classe
UIElement3D, il est possible de concevoir des objets 3D dotés de fonctionnalités
de haut niveau; il sera abordé l’implémentation et l’utilisation d’un TrackBall
(déplacement d’objet par l’utilisation de la souris), d’un zoom (par l’intermédiaire
de la molette de la souris), et des déplacements 3D (rotation et translation).
• le chapitre 7 traite de la construction d’un jeu de Casse-Briques dans un
environnement 3D; il sera vu la façon de mettre en place des objets 3D modélisés
comme le plateau, les murs, la raquette, la balle et les briques; il sera vu la mise en
application du rendu d’animation avec la classe CompositionTarget; il sera vu la façon
de gérer le déplacement de la raquette et de la balle dans un environnement 3D; la
détection des collisions entre la balle et les autres objets 3D comme la raquette, les
briques et les murs, sera abordée; il sera vu aussi la mise en place de l’affichage du
temps et du score, tout comme la façon de gérer la fin de jeu.
• le chapitre 8 traite de la bonification du jeu par l’animation; les notions de
storyboard et d’animations réalistes seront abordées, tout comme la façon de les
utiliser ensemble; il sera vu la programmation d’un storyboard en C# et en XAML,
la programmation d’un composant animé par un storyboard, la programmation des
animations réalistes comme la simulation d’un effet de roulement d’une balle, la
génération des surfaces de plateau qui ne sont pas planes, et l’importation d’objet
3D au format Wavefront OBJ.
• le chapitre 9 traite de la prise en charge audio; il sera vu comment utiliser un contrôle
MediaElement pour écouter une piste sonore, comment manipuler un fichier audio
(lecture, pause, arrêt, positionnement de la tête de lecture), comment contrôler
le volume et la balance, et comment sonoriser le jeu en positionnant des pistes
sonores aux bons endroits et aux bons moments.
Toutes les notions abordées dans ce livre font l’objet d’une utilisation directe et pratique
que vous trouverez dans le code source en téléchargement. De cette façon, vous serez
capable de suivre et de programmer assez rapidement et assez facilement.
Avant-propos 13

Les liens de téléchargement

Tout le code source de cet ouvrage pourra être téléchargé gratuitement à l’adresse
web suivante:
http://www.reypatrice.fr

Bonne lecture à tous.


Première Partie

Apprentissage
de la
Modélisation
3D
1
Point, vecteur et matrice
dans l’espace 3D

Au sommaire de ce chapitre
• le repère des coordonnées 3D
• l’utilisation des structures Point3D et Vector3D
• l’utilisation des coordonnées euclidiennes et homogènes
• l’utilisation de la structure Point4D et les transformations en coordonnées
homogènes
• les opérations et les transformations sur la structure Matrix3D
18 Développez des applications 3D avec C#5 et WPF 4.5
L’espace de noms System.Windows.Media.Media3D (assembly PresentationCore.dll)
contient toutes les classes et toutes les structures qui supportent la représentation 3D.
Les fonctionnalités 3D permettent aux développeurs de dessiner, de transformer et
d’animer des graphiques 3D, à la fois en XAML et en code procédural. Les développeurs
peuvent aussi combiner de la 2D et des graphiques 3D pour créer des contrôles riches,
pour fournir des illustrations complexes de données et pour améliorer l’expérience
utilisateur de l’interface d’une application.
Dans ce chapitre, nous allons voir la définition et l’utilisation des structures Point3D,
Vector3D et Matrix3D, qui représentent les bases de la construction, en géométrie
analytique, des objets dans l’espace 3D.

1 - Le repère des coordonnées 3D

Dans un système à 3 dimensions, un point est représenté par ses coordonnées selon
les 3 axes X, Y et Z. Dans le repère 3D utilisé par WPF, depuis l’origine O du repère, l’axe
X des abscisses positives part vers la droite, l’axe Y des ordonnées positives part vers le
haut, et l’axe Z des coordonnées positives part vers l’extérieur et devant.
Ces 3 axes peuvent être représentés au moyen des doigts d’une main: le pouce
représente l’axe des X, l’index représente l’axe des Y et le majeur représente l’axe des Z.
L’axe des X est généralement dirigé vers la droite et l’axe des Y est généralement dirigé
vers le haut. Pour la direction de l’axe des Z, WPF utilise le système «main droite» dans
lequel l’axe des Z est dirigé vers l’avant (il sort de l’écran).

Y
plan (Ox,Oy)

plan (Oy,Oz)

Copyright 2013 Patrice REY

O
X

plan (Ox,Oz)

Z
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 19

2 - La structure Point3D

La localisation des points qui constituent une figure 3D dans un espace 3D, passe par
l’utilisation des coordonnées 3D. Le framework .NET 4.5 propose une structure nommée
Point3D. Cette structure Point3D représente un point de coordonnées x (abscisse), y
(ordonnée) et z dans l’espace 3D. La figure 1.1 énumère les propriétés et les méthodes
de cette structure. Cette structure Point3D expose les propriétés:
• X qui représente la coordonnée x du Point3D sur l’axe des X.
• Y qui représente la coordonnée y du Point3D sur l’axe des Y.
• Z qui représente la coordonnée z du Point3D sur l’axe des Z.

FIGURE 1.1
20 Développez des applications 3D avec C#5 et WPF 4.5
L’UserControl UtilisationPoint3d.xaml, dans le dossier chapitre_01, va nous permettre
de nous familiariser avec la pratique de la structure Point3D (figure 1.2).
FIGURE 1.2

Un point A (figure 1.3) de coordonnées (4,2,3), intitulé pt_a et de type Point3D, est
instancié en code procédural par Point3D pt_a = new Point3D(4,2,3). Les propriétés X,
Y et Z de la structure sont de type double.
FIGURE 1.3
Y

A(4,2,3)
4
X

3
Copyright 2013 Patrice REY

Z
Le premier choix du sélecteur x_select permet d’instancier 2 structures Point3D et
permet de tester leur égalité par la méthode héritée Equals ou bien par la surcharge de
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 21
l’opérateur «==». L’inégalité entre 2 structures Point3D est testée par la surcharge de
l’opérateur «!=». La figure 1.4 affiche les résultats obtenus.
FIGURE 1.4

private void TesterEgaliteInegalite() {


//test de 2 points: égalité et inégalité
Point3D pt_a = new Point3D(4, 2, 3);
Point3D pt_b = new Point3D(6, 3, 5);
if (pt_a == pt_b) {
AfficherCalcul(«=> donne true» + RC);
}
...
if (pt_a.Equals(pt_b)) {
AfficherCalcul(«=> donne true» + RC);
}
else {
AfficherCalcul(«=> donne false» + RC);
}
if (pt_a != pt_b) {
AfficherCalcul(«=> donne true» + RC);
}
else {
AfficherCalcul(«=> donne false» + RC);
}
if (pt_a.Equals(pt_b) == false) {
AfficherCalcul(«=> donne true» + RC);
}
else {
AfficherCalcul(«=> donne false» + RC);
}
}
22 Développez des applications 3D avec C#5 et WPF 4.5
La méthode Offset permet de déplacer un point dans l’espace. Le point A(4,2,3),
nommé pt_a, peut être déplacé d’une valeur de 2 selon X, d’une valeur de 1 suivant Y
et d’une valeur de 2 suivant Z par pt_a.Offset(2,1,2). Il possède alors comme nouvelles
coordonnées (6,3,5) comme le visualise la figure 1.5.
FIGURE 1.5

A(4,2,3)
4 6
X
A(6,3,5)

3
Copyright 2013 Patrice REY

Z
5

private void TesterDecalageEgalite() {


Point3D pt_a = new Point3D(4, 2, 3);
pt_a.Offset(2, 1, 2);
Point3D pt_b = new Point3D(6, 3, 5); }
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 23
La méthode statique Point3D.Parse permet de transformer une chaîne au format
texte, représentant les coordonnées d’un Point3D, en une structure Point3D (figure
1.6). L’écriture d’une méthode additionnelle MiseEnFormePoint3d permettra de bien
visualiser les valeurs des propriétés X, Y et Z d’un Point3D (notamment en différenciant
le point décimal de la virgule).
private void TransformerTexteEnPoint3D() {
Point3D pt = new Point3D();
pt = Point3D.Parse(«-0.5,3,6.2»);
AfficherTexte(«Point3D pt = new Point3D()» + RC);
AfficherTexte(«pt = Point3D.Parse(\»-0.5,3,6.2\»)» + RC);
string etiq = «-> pt(« + pt.X.ToString() + «,» + pt.Y.ToString() + «,» +
pt.Z.ToString() + «)» + RC;
AfficherCalcul(etiq);
AfficherTexte(«Mise en forme:» + RC);
AfficherCalcul(«-> pt»+MiseEnFormePoint3d(pt) + RC);
}
//mise en forme format texte pour un point3d arrondi à 2 décimales
private string MiseEnFormePoint3d(Point3D pt) {
string aff = «(«;
aff += pt.X.ToString(«0.00»).Replace(‘,’, ‘.’) + «,»;
aff += pt.Y.ToString(«0.00»).Replace(‘,’, ‘.’) + «,»;
aff += pt.Z.ToString(«0.00»).Replace(‘,’, ‘.’) + «)»;
return aff;
}

FIGURE 1.6

La classe Point3DCollection est une collection typée d’objets Point3D. Elle est très utile
quand l’on souhaite stocker un ensemble de points, de type Point3D, pour effectuer
par exemple une visualisation d’un ensemble de points à un instant donné.
Pour instancier une nouvelle collection collect, de type Point3DCollection, on écrira
Point3DCollection collect = new Point3DCollection(). Et pour ajouter le point A(4,2,3) à
24 Développez des applications 3D avec C#5 et WPF 4.5
cette collection, on écrira
• soit collect.Add(new Point3D(4,2,3)) pour ajouter un nouveau point A.
• soit collect.Add(pt_a) si le point A(4,2,3) a été instancié par Point3D pt_a=new
Point3D(4,2,3).
L’énumération d’une collection Point3DCollection se réalise facilement par l’emploi
d’une boucle foreach qui énumère les objets Point3D contenus dans la collection.
private void TesterCollectionPoint() {
Point3D pt_a = new Point3D(2, 3, 4);
Point3D pt_b = new Point3D(1, 2, 3);
Point3D pt_c = new Point3D(2, -1, -3);
Point3DCollection collect = new Point3DCollection();
collect.Add(pt_a);
collect.Add(pt_b);
collect.Add(pt_c);
...
AfficherTexte(«Point3DCollection collect = new Point3DCollection()» + RC);
AfficherTexte(«collect.Add(pt_a)» + RC);
AfficherTexte(«collect.Add(pt_b)» + RC);
AfficherTexte(«collect.Add(pt_c)» + RC);
AfficherCalcul(«Test -> énumération de la collection:» + RC);
foreach (Point3D pt in collect) {
string etiq = «-> pt(« + pt.X.ToString() + «,» + pt.Y.ToString() + «,» +
pt.Z.ToString() + «)» + RC;
AfficherCalcul(etiq);
}
}

FIGURE 1.7

Copyright 2013 Patrice REY


CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 25

3 - La notion de vecteur

René Descartes (1596-1650) est l’un des plus grands savants et philosophes de
son temps, fondateur de la géométrie analytique, de l’application de l’algèbre à la
géométrie, et d’une approche de la connaissance fondée sur la méthode rationnelle.
Avec les encouragements de ses amis scientifiques (Mersenne, Beeckman, Mydorge,
Hortensius, Huygens, van Schooten), René Descartes publia son monumental traité
«Discours de la Méthode, pour bien conduire sa raison et chercher la vérité dans les
sciences», fondement de la démarche scientifique moderne (figure 1.8). Cet ouvrage
comporte trois appendices qui illustrent l’application de sa méthode, ce sont La
Dioptrique, Les Météores et La Géométrie. Ce traité fut publié à Leyden en 1637.
C’est dans son traité de géométrie que Descartes fit un apport décisif : il posa le premier
les principes de l’invariance, où fut soulevée pour la première fois la question de
référentiel pour observer et décrire les phénomènes. Pour René Descartes, l’existence
même d’un raisonnement mathématique et d’une façon ordonnée de penser, prouve
que l’homme, en suivant méthodiquement certaines règles, peut se convaincre d’avoir
atteint la vérité. L’ordre qu’il établit en géométrie des courbes repose sur l’emploi des
coordonnées.
FIGURE 1.8
26 Développez des applications 3D avec C#5 et WPF 4.5
La géométrie analytique est un outil nouveau qui permet de résoudre par le calcul un
problème de géométrie. Il est forgé par Descartes alors que celui-ci cherche à résoudre
un problème posé par Pappus à la fin du IIIe siècle et non résolu jusqu’alors. Il apporte
l’idée de remplacer chaque point par deux nombres qui seront ses coordonnées afin
de résoudre par le calcul un problème de géométrie.
Par définition, un vecteur est un objet mathématique, représenté par deux points A et
B, et qui possède 3 caractéristiques:
• une direction: c’est la droite (AB)
• un sens: celui qui va de A vers B
• une longueur: c’est la distance entre A et B
droite AB

A
En mathématiques, les mots «sens» et «direction» ne sont pas synonymes. Une
direction est définie à l’aide d’une droite. Des droites parallèles ont la même direction.
Un sens n’est défini qu’associé à une direction. Pour une direction il n’y a que deux sens
possibles. A

sens de A vers B droite AB

A sens de B vers A

D1
D2 D3

les droites D1, D2 et D3 définissent sur la direction définie par (AB), il y a le sens «A
la même direction vers B» et le sens «B vers A»
D’un point de vue modélisation des données, un vecteur s’apparente à un déplacement
Copyright 2013 Patrice REY

selon l’axe des abscisses et à un déplacement selon l’axe des ordonnées. Un vecteur
dans l’espace 2D sera matérialisé par ces déplacements, par une valeur réelle x (pour
les abscisses) et par une valeur réelle y pour les ordonnées.
La connaissance de ces 2 valeurs suffit pour matérialiser n’importe quel vecteur. Et à
partir de ces 2 valeurs, nous pouvons calculer facilement la longueur du vecteur. Il en
est de même dans l’espace 3D en rajoutant une coordonnée z.
les droites
CHAPITRE 1 D1, D2 et D3 définissent Point, vecteur
sur la direction définie par
et matrice dans(AB),l’espace
il y a le sens3D
«A
la même direction vers B» et le sens «B vers A» 27

la longueur L du vecteur AB
est égale à sa norme
c’est-à-dire :

déplacement distance = b
selon Y
A

déplacement selon X

O
distance = a

4 - La structure Vector3D

Le framework .NET 4.5 possède une structure Vector3D qui gère l’utilisation et la
manipulation des vecteurs dans l’espace 3D.
Un vecteur dans l’espace 3D est représenté par une structure Vector3D. Un vecteur
dans l’espace 3D, de type Vector3D, par définition, représente un déplacement dans
l’espace 3D. Cette structure expose les propriétés suivantes:
• X qui représente le composant x de cette structure.
• Y qui représente le composant y de cette structure.
• Z qui représente le composant z de cette structure.
• Length qui obtient la longueur de cette structure.
• LengthSquared qui obtient le carré de la longueur de cette structure.
La figure 1.9 montre un vecteur V dont ses composantes sont (5,3,3). Ce vecteur
part de l’origine O vers le point A(5,3,3). Un vecteur indique un déplacement par ses
composantes. Depuis l’origine O, le déplacement selon X est de 5, le déplacement
selon Y est de 3 et le déplacement selon Z est de 3.
De ce fait, on dit que le point A est l’image du point O par la translation de vecteur
(5,3,3). Il est important de garder à l’esprit qu’un vecteur est un déplacement dans
l’espace 3D. Il existe une infinité de vecteurs identiques à celui-ci.
28 Développez des applications 3D avec C#5 et WPF 4.5
FIGURE 1.9 Y vecteur de l’origine O à A(5,3,3)

A(5,3,3)

O V
X

Les structures Point3D et Vector3D possèdent un ensemble de méthodes pour


effectuer différents calculs vectoriels. Il est important de bien connaître ces différents
mécanismes qui interviendront plus tard systématiquement dans la modélisation.
L’UserControl UtilisationVecteur3d.xaml, dans le dossier chapitre_01, va nous permettre
de nous familiariser avec la pratique des différents calculs vectoriels utilisant les
structures Point3D et Vector3D (figure 1.10).
FIGURE 1.10

Copyright 2013 Patrice REY


CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 29
Pour trouver les coordonnées d’un vecteur connaissant un point A et un point B, il faut
utiliser une surcharge de l’opérateur «-» de Point3D qui retourne un vecteur, de type
Vector3D, quand on soustrait un point à un autre point (par exemple avec l’écriture
vect = pt_a - pt_b ou bien avec l’écriture vect = pt_b - pt_a). Par exemple, sur la figure
1.11, on a les points A(1,1,2) et B(4,3,3), et le vecteur V(3,2,1) permet de passer de A
vers B. Trouver le vecteur résultant entre deux points c’est trouver un vecteur orienté.
Si vecteur = point B - point A, cela correspond au vecteur dont le point de départ est
le point A et le point d’arrivée est le point B, donc un vecteur orienté de A vers B. Si
vecteur = point A - point B, cela correspond au vecteur dont le point de départ est le
point B et le point d’arrivée est le point A, donc un vecteur orienté de B vers A.

FIGURE 1.11 Y

V(3,2,1)
B(4,3,3)

X
A(1,1,2)

Z
30 Développez des applications 3D avec C#5 et WPF 4.5
Avec les points A(1,1,2) et B(4,3,3), le vecteur V(3,2,1) permet de passer de A vers B et
le vecteur V(-3,-2,-1) permet de passer de B vers A.
private void TrouverVecteurEntre2Points() {
Point3D pt_a = new Point3D(1, 1, 2);
Point3D pt_b = new Point3D(4, 3, 3);
Vector3D vect_a_vers_b = pt_b - pt_a;
AfficherTexte(«Vector3D vect_a_vers_b = pt_b - pt_a» + RC);
Vector3D vect_b_vers_a = pt_a - pt_b;
AfficherTexte(«Vector3D vect_b_vers_a = pt_a - pt_b» + RC);
}
La méthode statique Point3D.Add permet d’additionner un vecteur à un point. Cela
permet de trouver les coordonnées d’un point B connaissant les coordonnées d’un
point A et celles d’un vecteur V. La figure 1.12 schématise géométriquement le résultat
recherché. La structure Vector3D possède une surcharge de l’opérateur «+», ce qui
permet d’écrire directement l’expression pt_b = pt_a + vect avec pt_a et pt_b qui sont
du type Point3D et vect qui est du type Vector3D. A noter qu’une deuxième surcharge
de l’opérateur permet d’écrire aussi pt_b = vect + pt_a.
FIGURE 1.12
Y

B(4,3,3) additionner le vecteur V au


point A pour donner le point B

V(3,2,1)
X
A(1,1,2) Copyright 2013 Patrice REY

Soient un point A, de type Point3D, de coordonnées (1,1,2) et un vecteur V, de type


Vector3D, de coordonnées (3,2,1), le point B recherché possède les coordonnées
(4,3,3). La figure 1.13 illustre le résultat attendu.
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 31
private void AdditionPointVecteur() {
Point3D pt_a = new Point3D(1, 1, 2);
Vector3D vect = new Vector3D(3, 2, 1);
Point3D pt_b = new Point3D();
pt_b = Point3D.Add(pt_a, vect);
AfficherTexte(«pt_b = Point3D.Add(pt_a, vect)» + RC);
pt_b = pt_a + vect;
AfficherTexte(«pt_b = pt_a + vect» + RC);
pt_b = vect + pt_a;
AfficherTexte(«pt_b = vect + pt_a» + RC);
}

FIGURE 1.13

Si, à un point A, on soustrait un vecteur V, on obtient un point B. La figure 1.14 schématise


géométriquement le résultat recherché. La méthode statique Point3D.Substract permet
de soustraire un vecteur à un point. Soit le point A(4,3,3) et le vecteur V(3,2,1), la
soustraction de V à A donne le point B aux coordonnées (1,1,2). Il existe ici aussi une
surcharge de l’opérateur «-» pour soustraire directement un vecteur à un point par
l’écriture pt_b = pt_a - vect (avec pt_a et pt_b de type Point3D, et vect de type Vector3D).
private void SoustraireVecteurPoint() {
Point3D pt_a = new Point3D(4, 3, 3);
Vector3D vect = new Vector3D(3, 2, 1);
Point3D pt_b = new Point3D();
pt_b = Point3D.Subtract(pt_a, vect);
AfficherTexte(«pt_b = Point3D.Subtract(pt_a, vect)» + RC);
pt_b = pt_a - vect;
AfficherTexte(«pt_b = pt_a - vect» + RC);
AfficherCalcul(«-> pt_b « + MiseEnFormePoint3d(pt_b) + RC);
}
32 Développez des applications 3D avec C#5 et WPF 4.5
FIGURE 1.14
Y

V(3,2,1)

A(4,3,3) soustraire le vecteur V au point


A pour donner le point B

X
B(1,1,2)

L’addition vectorielle permet de trouver le vecteur vect_u_v résultant de la somme


du vecteur U et du vecteur V (figure 1.15). Cette somme vectorielle s’effectue grâce
Copyright 2013 Patrice REY

à la surcharge de l’opérateur «+» de Vector3D, ou bien avec la méthode statique


Vector3D.Add.
private void Additionner2Vecteurs() {
Vector3D vect_u = new Vector3D(0, 2, -3);
Vector3D vect_v = new Vector3D(4, -2, 0);
Vector3D vect_u_plus_v = Vector3D.Add(vect_u, vect_v);
vect_u_plus_v = vect_u + vect_v;
}
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 33
FIGURE 1.15
Y

additionner le vecteur U au
vecteur V donne le vecteur
V(4,-2,0) vect_u_v

U(0,2,-3)
X

vect_u_v (4,0,-3)

Soient U et V deux vecteurs, le vecteur résultant vect_u_moins_v = U - V représente


la soustraction vectorielle qui consiste à ajouter à U le vecteur opposé à V. Ce qui
donne vect_u_moins_v = U - V = U + (-V). La méthode statique Vector3D.Substract
permet d’obtenir le vecteur résultant d’une soustraction vectorielle. Ce résultat peut
être obtenu aussi grâce à la surcharge de l’opérateur «-» de Vector3D.
En appliquant la soustraction vectorielle sur le vecteur U(0,2,-3) et sur le vecteur V(4,-
2,0), nous obtenons le vecteur résultant de cette soustraction vect_u_moins_v (figure
1.16).
34 Développez des applications 3D avec C#5 et WPF 4.5
private void Soustraire2Vecteurs() {
Vector3D vect_u = new Vector3D(0, 2, -3);
Vector3D vect_v = new Vector3D(4, -2, 0);
Vector3D vect_u_moins_v = Vector3D.Subtract(vect_u, vect_v);
vect_u_moins_v = vect_u - vect_v;
}
FIGURE 1.16

Le produit vectoriel (figure 1.18) consiste à multiplier un vecteur par un scalaire (une
valeur de type double). Le vecteur se trouve alors augmenté en longueur mais sa
direction reste la même. Son sens peut être opposé si le scalaire multiplicateur est
négatif. La surcharge de l’opérateur «*» permet de multiplier un vecteur par une valeur
de type double. L’autre façon est d’utiliser la méthode statique Vector3D.Multiply
(figure 1.17).
FIGURE 1.17

Copyright 2013 Patrice REY

private void MultiplierVecteurValeur() {


Vector3D vect_u = new Vector3D(2, 1, -1);
Vector3D vect_v = vect_u * 2;
vect_v = Vector3D.Multiply(2, vect_u);}
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 35
FIGURE 1.18
Y

multiplication d’un vecteur U


par un scalaire de valeur 2 qui
donne un vecteur V

X
V(4,2,-2)

U(2,1,-1)
Z

La longueur d’un vecteur est obtenue par la propriété Length et la longueur élevée au
carré est obtenue par la propriété LengthSquared. La figure 1.19 visualise le résultat
obtenu pour un vecteur U(3,3,3) et la figure 1.20 schématise ce vecteur U.
private void TrouverlongueurVecteur() {
Vector3D vect_u = new Vector3D(3, 3, 3);
AfficherTexte(«Vector3D vect_u = new Vector3D(3, 3, 3)» + RC);
AfficherCalcul(«-> longueur de vect_u = « + vect_u.Length.ToString() + RC);
AfficherCalcul(«-> longueur au carré de vect_u = « + vect_u.LengthSquared
.ToString() + RC);
}

FIGURE 1.19
36 Développez des applications 3D avec C#5 et WPF 4.5
FIGURE 1.20
Y

U(3,3,3)
3
X

La normalisation d’un vecteur consiste à trouver les coordonnées du vecteur ayant pour
longueur 1. La normalisation ne change ni la direction du vecteur, ni le sens du vecteur.
Elle ne fait que réduire le vecteur à une longueur de 1 en recalculant ses composantes
(x,y,z). Pour normaliser un vecteur, il suffit de lui appliquer la méthode Normalize. Par
exemple (figure 1.21), le vecteur U(3,3,3) a une longueur approximative de 5.19. Après
la normalisation, ses composantes sont (0.58,0.58,0.58) et sa longueur est égale à 1.
FIGURE 1.21

Copyright 2013 Patrice REY

private void NormaliserVecteur() {


CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 37
Vector3D vect_u = new Vector3D(3, 3, 3);
AfficherTexte(«Vector3D vect_u = new Vector3D(3, 3, 3)» + RC);
AfficherTexte(«vect_u.Length» + RC);
AfficherCalcul(«-> longueur de vect_u = « + vect_u.Length.ToString() + RC);
vect_u.Normalize();
AfficherTexte(«vect_u.Normalize()» + RC);
AfficherCalcul(«-> vect_u normaliser: « + MiseEnFormeVector3d(vect_u) + RC);
AfficherCalcul(«-> longueur de vect_u = « + vect_u.Length.ToString() + RC);
}
Le calcul de l’angle entre deux vecteurs se fait par la méthode statique Vector3D.
AngleBetween. La figure 1.22 visualise et schématise géométriquement le résultat
recherché de l’angle entre les vecteurs U(0,0,3) et V(2,0,0).
FIGURE 1.22
Y

U(0,0,3)

V(2,0,0)
X
angle

Z
38 Développez des applications 3D avec C#5 et WPF 4.5
private void AngleEntreVecteur() {
Vector3D vect_u = new Vector3D(0, 0, 3);
Vector3D vect_v = new Vector3D(2, 0, 0);
AfficherTexte(«Vector3D vect_u = new Vector3D(0, 0, 3)» + RC);
AfficherTexte(«Vector3D vect_v = new Vector3D(2, 0, 0)» + RC);
double angle = Vector3D.AngleBetween(vect_u, vect_v);
AfficherTexte(«double angle = Vector3D.AngleBetween(vect_u, vect_v)» + RC);
AfficherCalcul(«-> angle entre vect_u et vect_v = « + angle.ToString()
+ « degrés» + RC);
}
Les notions de produit scalaire et de produit vectoriel sont très importantes en algèbre
linéaire. La méthode statique Vector3D.DotProduct permet de calculer le produit
scalaire de deux vecteurs et la méthode statique Vector3D.CrossProduct permet de
calculer le produit vectoriel de deux vecteurs.
Par définition, on appelle produit scalaire (euclidien) de deux vecteurs (x1,y1,z1) et
(x2,y2,z2) le réel noté  et défini par  = x1*x2 + y1*y2 + z1*z2. Si on a les
vecteurs (1,2,3) et (2,3,6), alors le produit scalaire est égal au réel:
 = 1*2 + 2*3 + 3*6 = 26
Si les vecteurs sont de longueur non nulle, leur produit scalaire sera égal à 0 dans le
cas où ils sont perpendiculaires. Par exemple si on a (1,2,0) et (2,-1,0), le produit
scalaire sera nul.
Y

V(2,-1,0) le produit scalaire  = 0 donc


U(1,2,0) les vecteurs sont orthogonaux

X
Copyright 2013 Patrice REY

Z
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 39
Le produit scalaire est utilisé en 3D par exemple pour déterminer si une facette
rectangulaire dans l’axe de visée d’une caméra montre sa face avant ou bien sa face
arrière (face cachée). Supposons que l’axe de visée d’une caméra soit représenté par
le vecteur (-3,-3,-3) et qu’une normale à une facette rectangulaire soit représentée
par le vecteur (0,0,2), alors le produit scalaire sera négatif. Si la normale à la facette
rectangulaire est représentée par le vecteur (0,0,-2), alors le produit scalaire sera
positif.
Dans ce cas précis, un produit scalaire négatif permet de dire que la face regardée est
la face avant, et un produit scalaire positif permet de dire que la face regardée est la
face arrière (face cachée).

le produit scalaire le produit scalaire


 négatif  positif

face face (0,0,-2)


avant arrière

X
(0,0,2)

Z (-3,-3,-3)

Par définition, on appelle produit vectoriel de deux vecteurs (x1,y1,z1) et (x2,y2,z2)


le vecteur noté  et défini par
 (y1*z2 - z1*y2 , z1*x2 - x1*z2 , x1*y2 - y1*x2)
Le vecteur obtenu par le produit vectoriel est un vecteur perpendiculaire au plan formé
par les vecteurs  et . Une règle pratique, pour calculer les coordonnées du vecteur
résultant d’un produit vectoriel, consiste à écrire sur une ligne les coordonnées de 
en répétant à la suite la première et la deuxième coordonnée. On procède de même
avec  sur une seconde ligne. Les coordonnées de  sont obtenues à l’aide des
40 Développez des applications 3D avec C#5 et WPF 4.5
produits en croix suivants:
x1 y1 z1 x1 y1
x2 y2 z2 x2 y2

première troisième
coordonnée deuxième coordonnée
y1*z2 - y2*z1 coordonnée x1*y2 - x2*y1
z1*x2 - z2*x1
Avec un produit vectoriel, on peut définir par exemple la normale à une facette
rectangulaire. Une facette rectangulaire est composée par les points A, B, C et D. Le
vecteur (3,0,0) sert de support à AB, et le vecteur (0,2,0) sert de support à AB. Le
produit vectoriel  représente la normale à la face et ses composantes sont égales
à  (0,0,6).

D C

face
(0,2,0) avant

A
B
(3,0,0)
X

le produit vectoriel  donne


Copyright 2013 Patrice REY

le vecteur représentant la
normale (0,0,6)
Z

La figure 1.23 visualise les résultats obtenus lors des calculs de produit scalaire et de
produit vectoriel.
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 41
FIGURE 1.23

private void ProduitScalaireVectoriel() {


//explication produit scalaire general
double prod_scalaire = Vector3D.DotProduct(new Vector3D(1, 2, 3),
new Vector3D(2, 3, 6));
//explication produit scalaire nul
prod_scalaire = Vector3D.DotProduct(new Vector3D(1, 2, 0),
new Vector3D(2, -1, 0));
//explication face devant arriere
prod_scalaire = Vector3D.DotProduct(new Vector3D(0, 0, 2),
new Vector3D(-3, -3, -3));
prod_scalaire = Vector3D.DotProduct(new Vector3D(0, 0, -2),
new Vector3D(-3, -3, -3));
//explication produit vectoriel
Vector3D prod_vectoriel = Vector3D.CrossProduct(new Vector3D(2, -1, 3),
new Vector3D(-4, 2, -2));
AfficherTexte(«prod_vectoriel = Vector3D.CrossProduct(new Vector3D(2,-1,3),
new Vector3D(-4,2,-2))» + RC);
AfficherCalcul(«-> prod_vectoriel: vecteur « +
MiseEnFormeVector3d(prod_vectoriel) + RC);
//explication pour trouver une normale à une face
prod_vectoriel = Vector3D.CrossProduct(new Vector3D(3,0,0), new Vector3D(0,2,0));
AfficherTexte(«prod_vectoriel = Vector3D.CrossProduct(new Vector3D(3,0,0),
new Vector3D(0,2,0))» + RC);
AfficherCalcul(«-> prod_vectoriel: vecteur « +
MiseEnFormeVector3d(prod_vectoriel) + RC);
}
42 Développez des applications 3D avec C#5 et WPF 4.5

5 - Coordonnées euclidiennes et homogènes

Toute composition de rotation, de translation, de mise à l’échelle, de cisaillement, est


appelée une transformation affine. Soit deux points P1(x1,y1,z1) et P2(x2,y2,z2) de
l’espace 3D en coordonnées euclidiennes. Si P2 est l’image de P1 par une transformation,
alors cette transformation sera dite «affine» si elle respecte la condition suivante:

La notation P2=M.P1+T veut dire que P2 est obtenu en multipliant P1 par la matrice
M puis en lui ajoutant la matrice T. La transformation affine a pour particularité de
conserver le parallélisme des droites mais de ne pas préserver ni les longueurs, ni les
angles.

5.1 - Transformations en coordonnées euclidiennes

Nous allons voir comment les transformations géométriques s’expriment en


coordonnées euclidiennes avec la translation, la mise à l’échelle, la rotation et le
cisaillement.
La translation consiste à déplacer un point d’une distance Tx suivant l’axe des X, d’une
distance Ty suivant l’axe des Y et d’une distance Tz suivant l’axe des Z. Le passage du
point P1(x1,y1,z1) au point P2(x2,y2,z2) se fait par la transformation T tel que P2=P1+T.
Les coordonnées de P2(x2,y2,z2) sont obtenues à partir des coordonnées de P1(x1,y1,z1)
par la transformation de translation de la façon suivante:
Copyright 2013 Patrice REY

La figure 1.24 visualise le point P1(2,0,4) qui est transformé en P2(6,1,2) par une
translation T(Tx=4, Ty=1, Tz=-2).
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 43
FIGURE 1.24
Y
Tx=4
Ty=1
Tz=-2

X
P2(6,1,2)

Z P1(2,0,4)

La mise à l’échelle d’une figure consiste à l’agrandir ou à la réduire en fonction des


facteurs d’échelle Sx selon l’axe des X, Sy selon l’axe des Y et Sz selon l’axe des Z. Le
point P2(x2,y2,z2) est obtenu en appliquant au point P1(x1,y1,z1) une matrice de
transformation. L’équation correspondant à P2=Ms.P1 est la suivante:

Le calcul matriciel donne comme résultat x2=Sx*x1, y2=Sy*y1 et z2=Sz*z1. Les


coordonnées du point P2(x2,y2,z2) sont obtenues par le calcul matriciel des coordonnées
du point P1(x1,y1,z1) par la matrice de mise à l’échelle:

x2 = Sx*x1 + 0*y1 + 0*z1


donc x2 = Sx*x1
44 Développez des applications 3D avec C#5 et WPF 4.5

y2 = 0*x1 + Sy*y1 + 0*z1


donc y2 = Sy*y1

z2 = 0*x1 + 0*y1 + Sz*z1


donc z2 = Sz*z1

La figure 1.25 visualise une mise à l’échelle uniforme de valeur 2 avec S(Sx=2,Sy=2,Sz=2).
Le point P1(2,0,2) donne par cette mise à l’échelle le point P2(4,0,4).
FIGURE 1.25

Sx=2
Sy=2
Sz=2

P1(2,0,2)
P2(4,0,4)
Copyright 2013 Patrice REY

La réflexion, dit aussi effet miroir, est un cas particulier de la mise à l’échelle. La figure
suivante visualise la matrice pour un effet miroir selon l’axe des Y (on trouve x2=-x1,
y2=y1 et z2=z1).
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 45

x2 = -1*x1 + 0*y1 + 0*z1


donc x2 = -x1

y2 = 0*x1 + 1*y1 + 0*z1


donc y2 = y1

z2 = 0*x1 + 0*y1 + 1*z1


donc z2 = z1

La figure suivante visualise la matrice pour un effet miroir selon l’axe des Z (on trouve
x2=x1, y2=-y1 et z2=z1).

x2 = 1*x1 + 0*y1 + 0*z1


donc x2 = x1

y2 = 0*x1 + (-1*y1) + 0*z1


donc y2 = -y1

z2 = 0*x1 + 0*y1 + 1*z1


donc z2 = z1

La figure suivante visualise la matrice pour un effet miroir selon l’axe des X (on trouve
x2=x1, y2=y1 et z2=-z1).
46 Développez des applications 3D avec C#5 et WPF 4.5

x2 = 1*x1 + 0*y1 + 0*z1


donc x2 = x1

y2 = 0*x1 + 1*y1 + 0*z1


donc y2 = y1

z2 = 0*x1 + 0*y1 + (-1*z1)


donc z2 = -z1

La matrice de rotation pour faire pivoter les points d’un angle α autour de l’axe des X
est la suivante:

Les coordonnées du point P2(x2,y2,z2) sont obtenues par le calcul matriciel des
coordonnées du point P1(x1,y1,z1) par la matrice de rotation:

x2 = 1*x1 + 0*y1 + 0*z1


donc x2 = x1
Copyright 2013 Patrice REY

y2 = 0*x1 + cos(α)*y1 + (-sin(α)*z1)


donc y2 = y1*cos(α) - z1*sin(α)
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 47

z2 = 0*x1 + sin(α)*y1 + cos(α)*z1


donc z2 = y1*sin(α) + z1*cos(α)

Le calcul matriciel donne, pour un point P1(x1,y1,z1), un point P2(x2,y2,z2) avec x2=x1,
y2=y1*cos(α)-z1*sin(α) et z2=y1*sin(α)+z1*cos(α). La figure 1.26 visualise la rotation
d’un segment avec α=-90°. Le point A1(0,0,2) est transformé en A2(0,2,0) et le point
C1(6,0,2) est transformé en C2(6,2,0).
FIGURE 1.26

Y
B2(0,5,0) D2(6,5,0)

-90° A2(0,2,0) C2(6,2,0)

A1(0,0,2) C1(6,0,2)

Z B1(0,0,5) D1(6,0,5)

De la même façon, la matrice de rotation pour faire pivoter les points d’un angle α
autour de l’axe des Y est la suivante:
48 Développez des applications 3D avec C#5 et WPF 4.5
Les coordonnées du point P2(x2,y2,z2) sont obtenues par le calcul matriciel des
coordonnées du point P1(x1,y1,z1) par la matrice de rotation:

x2 = cos(α)*x1 + (-sin(α)*y1) + 0*z1


donc x2 = x1*cos(α) - y1*sin(α)

y2 = sin(α)*x1 + cos(α)*y1 + 0*z1


donc y2 = x1*sin(α) + y1*cos(α)

z2 = 0*x1 + 0*y1 + 1*z1


donc z2 = z1

Et la matrice de rotation pour faire pivoter les points d’un angle α autour de l’axe des
Z est la suivante:

Les coordonnées du point P2(x2,y2,z2) sont obtenues par le calcul matriciel des
coordonnées du point P1(x1,y1,z1) par la matrice de rotation:

x2 = cos(α)*x1 + (-sin(α)*y1) + 0*z1


Copyright 2013 Patrice REY

donc x2 = x1*cos(α) - y1*sin(α)

y2 = sin(α)*x1 + cos(α)*y1 + 0*z1


donc y2 = x1*sin(α) + y1*cos(α)
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 49

z2 = 0*x1 + 0*y1 + 1*z1


donc z2 = z1

Le cisaillement est une opération qui permet de déformer les objets. On distingue trois
types de cisaillement: Cx selon l’axe des X, Cy selon l’axe des Y et Cz selon l’axe des Z.
La matrice pour un cisaillement selon l’axe des X est la suivante:

Les coordonnées du point P2(x2,y2,z2) sont obtenues par le calcul matriciel des
coordonnées du point P1(x1,y1,z1) par la matrice de cisaillement:

x2 = 1*x1 +Cy*y1 + Cz*z1


donc x2 = x1 + y1*Cy + z1*Cz

y2 = 0*x1 + 1*y1 + 0*z1


donc y2 = y1

z2 = 0*x1 + 0*y1 + 1*z1


donc z2 = z1

La figure 1.27 visualise un cisaillement selon l’axe des X avec Cy=2 et Cz=2 pour une
figure représentant un carré. D’après l’équation matricielle, un point A1(1,2,0) donnera
un point A2(5,2,0) avec Cy=2 et Cz=2.
50 Développez des applications 3D avec C#5 et WPF 4.5
FIGURE 1.27
Y

Cy=2
Cz=2

A1(1,2,0) A2(5,2,0)

De la même façon, on aura la matrice pour un cisaillement selon l’axe des Y qui sera la
suivante:

Les coordonnées du point P2(x2,y2,z2) sont obtenues par le calcul matriciel des
coordonnées du point P1(x1,y1,z1) par la matrice de cisaillement:

x2 = 1*x1 + 0*y1 + 0*z1


Copyright 2013 Patrice REY

donc x2 = x1

y2 = Cx*x1 + 1*y1 + Cz*z1


donc y2 = x1*Cx + y1 + z1*Cz
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 51

z2 = 0*x1 + 0*y1 + 1*z1


donc z2 = z1

Et on aura la matrice pour un cisaillement selon l’axe des Z qui sera la suivante:

Les coordonnées du point P2(x2,y2,z2) sont obtenues par le calcul matriciel des
coordonnées du point P1(x1,y1,z1) par la matrice de cisaillement:

x2 = 1*x1 + 0*y1 + 0*z1


donc x2 = x1

y2 = 0*x1 + 1*y1 + 0*z1


donc y2 = y1

z2 = Cx*x1 + Cy*y1 + 1*z1


donc z2 = x1*Cx + y1*Cy +z1

5.2 - Définition des coordonnées homogènes

Les mathématiques des images par ordinateur sont intimement liées à la multiplication
matricielle. Comme nous venons de le voir dans le paragraphe précédent, la translation
d’un point ne correspond pas directement à une multiplication matricielle, parce que
52 Développez des applications 3D avec C#5 et WPF 4.5
la translation n’est pas une transformation linéaire. Pour contourner cette difficulté, il
est classique d’introduire ce que l’on appelle les coordonnées homogènes.
Dans le système des coordonnées homogènes, on introduit une quatrième coordonnée
w, différente de zéro. Un point P(x,y,z) en coordonnées euclidiennes s’écrira P(x/w,y/
w,z/w,w) en coordonnées homogènes.
Si w=1 par exemple, alors les coordonnées du point P seront P(x,y,z,1). Ce changement
consistant à passer d’un triplet à un quadruplet de coordonnées s’appelle une
homogénéisation. Le cas où w est égale à zéro représente les points à l’infini.
De ce fait, les transformations 3D seront représentées par des matrices de 4x4. Une
matrice unique de 4x4 pourra désormais effectuer toutes les transformations affines et
non affines (translation, rotation, mise à l’échelle, cisaillement). Il sera possible aussi de
rassembler sous une seule matrice, une composition de transformations simplement
en effectuant une multiplication des matrices.

5.3 - Transformations en coordonnées homogènes

Maintenant, un point P1(x1,y1,z1,1) donnera un point P2(x2,y2,z2,1) par une matrice de


transformation M de la façon suivante:

Cette matrice de 4x4 permet de réaliser des transformations affines ou non affines. Le
calcul des coordonnées (x2,y2,z2) en fonction des coordonnées (x1,y1,z1) se fera par le
calcul matriciel de la façon suivante:
Copyright 2013 Patrice REY

x2 = m11*x1 + m12*y1 + m13*z1


+ m14*1
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 53

y2 = m21*x1 + m22*y1 + m23*z1


+ m24*1

z2 = m31*x1 + m32*y1 + m33*z1


+ m34*1

1 = 0*x1 + 0*y1 + 0*z1 + 1*1


ce qui donne bien 1=1

Dans la matrice de transformation M, les composantes m11, m12, m13, m21, m22,
m23, m31, m32 et m33 seront utilisées pour effectuer des rotations, des mises à
l’échelle et des cisaillements, et les composantes m14, m24 et m34 seront utilisées
pour la translation.
composantes pour la rotation, la composantes pour la
mise à l’échelle et le cisaillement translation

La matrice de transformation concernant la translation donnera les coordonnées


(x2,y2,z2) suivantes en fonction du calcul matriciel: x2=x1+Tx, y2=y1+Ty et z2=z1+Tz.
54 Développez des applications 3D avec C#5 et WPF 4.5

En 3D, une rotation fait tourner un ensemble de points ou d’objets d’un angle α autour
d’un axe de rotation. Contrairement à la rotation dans le plan, l’axe autour duquel
s’effectue la rotation 3D peut être une droite quelconque dans l’espace. Il sera alors
nécessaire de décomposer la rotation en trois composantes: la rotation autour de l’axe
des X, la rotation autour de l’axe des Y et la rotation autour de l’axe des Z, ce qui
donnera lieu à trois matrices de rotation différentes. Comme dans le cas de la 2D, la
rotation s’effectue toujours par rapport à l’origine.
La matrice de rotation utilisée pour une rotation d’un angle α autour de l’axe des X
donnera x2=x1, y2=y1*cos(α)-z1*sin(α) et z2=y1*sin(α)+z1*cos(α).

La matrice de rotation utilisée pour une rotation d’un angle α autour de l’axe des Y
donnera x2=x1*cos(α)+z1*sin(α), y2=y1 et z2=-x1*sin(α)+z1*cos(α).

Copyright 2013 Patrice REY

La matrice de rotation utilisée pour une rotation d’un angle α autour de l’axe des Z
donnera x2=x1*cos(α)-y1*sin(α), y2=x1*sin(α)+y1*cos(α) et z2=z1.
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 55

La mise à l’échelle modifie la taille d’un objet par rapport à l’origine en l’agrandissant
ou en le réduisant. La matrice de transformation pour une mise à l’échelle donnera
x2=x1*Sx, y2=y1*Sy et z2=z1*Sz.

Le cisaillement qui permet de déformer les objets pourra se faire selon les trois axes.
D’où trois matrices de transformation concernant le cisaillement. Suivant l’axe des X, la
matrice de cisaillement donnera x2=x1+y1*Cy+z1*Cz, y2=y1 et z2=z1.

Suivant l’axe des Y, la matrice de cisaillement donnera x2=x1, y2=x1*Cx+y1+z1*Cz et


z2=z1.

Suivant l’axe des Z, la matrice de cisaillement donnera x2=x1, y2=y1 et


z2=x1*Cx+y1*Cy+z1.
56 Développez des applications 3D avec C#5 et WPF 4.5

5.4 - La structure Point4D

Dans WPF, la structure Point4D permet de gérer des points de type Point3D dans le
système des coordonnées homogènes. Elle est spécifiquement utilisée pour accomplir
des transformations avec des matrices 3D non affines. Elle expose les propriétés
suivantes:
• X qui définit la coordonnée suivant l’axe des X.
• Y qui définit la coordonnée suivant l’axe des Y.
• Z qui définit la coordonnée suivant l’axe des Z.
• W qui définit la quatrième coordonnée dans le système des coordonnées
homogènes.
La structure Point4D possède un ensemble de méthodes pour accomplir des
opérations mathématiques variées sur des objets Point4D. Les méthodes suivantes
sont généralement les plus utilisées:
• Point4D.Add qui est une méthode statique qui ajoute une structure Point4D à un
Point4D.
• Point4D.Substract qui est une méthode statique qui soustrait une structure Point4D
à une autre structure Point4D.
• Point4D.Multiply qui est une méthode statique qui transforme la structure Point4D
spécifiée par la structure Matrix3D indiquée.
• Offset qui déplace la structure Point4D de la valeur spécifiée.
• Point4D.Parse qui est une méthode statique qui convertit une représentation
Copyright 2013 Patrice REY

textuelle d’une structure Point4D en une structure Point4D équivalente.


• Point4D.Equals qui est une méthode statique qui compare l’égalité de deux
structures Point4D.
• Equals qui est une méthode qui compare l’égalité de deux structures Point4D.
La structure Point4D possède des opérateurs surchargés comme:
• + (addition) qui ajoute une structure Point4D à un Point4D.
• - (soustraction) qui soustrait une structure Point4D d’une autre structure Point4D
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 57
et retourne le résultat sous forme de Point4D.
• * (multiplication) qui transforme la structure Point4D spécifiée par la structure
Matrix3D indiquée.
• == (égalité) qui compare l’égalité de deux structures Point4D.
• != (inégalité) qui compare l’inégalité de deux structures Point4D.
Il est possible d’effectuer un cast d’un objet Point3D en un objet Point4D comme dans
Point4D pt4 = (Point4D)new Point3D(10, 15, 20). Il en est de même pour passer d’un
Vector3D à un Point4D comme dans Point4D pt4 = (Point4D)(Point3D)new Vector3D(10,
15, 20). A noter que lors de ces cast, la coordonnée homogène w est fixée à 1.

6 - La structure Matrix3D

La structure Matrix3D est une structure qui représente une matrice 4x4 dans le système
des coordonnées homogènes. Ses composantes sont les suivantes:

La structure Matrix3D permet de représenter des transformations 3D affines ou


non affines. Les transformations non affines avec des valeurs non nulles pour les
composantes M14, M24 et M34, représentent des transformations en projection
perspective. Ces 16 composantes sont des propriétés publiques de la structure. La
structure expose d’autres propriétés utiles pour la performance dans les calculs sur les
matrices qui sont:
• Determinant qui récupère le déterminant de cette structure Matrix3D.
• HasInverse qui obtient une valeur qui indique si la Matrix3D est réversible.
• Matrix3D.Identity qui est une propriété statique qui change une structure Matrix3D
en une matrice identité (matrice remplie de zéro sauf M11, M22, M33 et M44 qui
sont remplis de 1).
• IsAffine qui obtient une valeur qui indique si cette structure Matrix3D est affine.
• IsIdentity qui détermine si cette structure Matrix3D est une matrice identité.
Jusqu’à présent, la notation des matrices de transformation que nous avons utilisées,
est celle qui est employée usuellement en algèbre linéaire. Cependant WPF utilise
des matrices transposées, ce qui explique la présence des propriétés OffsetX, OffsetY
et OffsetZ (qui correspondent à la translation) en dernière ligne de la matrice. Donc
58 Développez des applications 3D avec C#5 et WPF 4.5
les colonnes d’une matrice dans WPF correspondent aux lignes d’une matrice dans
l’algèbre linéaire classique.
Prenons comme exemple un point P1 aux coordonnées (5,0,0). Si nous appliquons à P1
une matrice de rotation d’un angle de -90° autour de l’axe Y, nous obtiendrons un point
P2 aux coordonnées (0,0,5). En algèbre linéaire classique, nous avons donc la relation
suivante de positionnement des coordonnées (le calcul matriciel se fait par ligne):
x2 = cos(-90)*5 + 0*0 + sin(-90)*0 + 0*1
=0
y2 = 0*5 + 1*0 + 0*0 + 0*1 = 0
z2 = -sin(-90)*5 + 0*0 + cos(-90)*0 + 0*1
=5

Dans la matrice de WPF, les lignes deviennent des colonnes. Le calcul matriciel se fait
donc de la façon suivante:
x2 = cos(-90)*5 + 0*0 + sin(-90)*0 + 0*1
=0
y2 = 0*5 + 1*0 + 0*0 + 0*1 = 0
z2 = -sin(-90)*5 + 0*0 + cos(-90)*0 + 0*1
=5

On retrouve bien les coordonnées de P2 qui sont (0,0,5). Les propriétés M11, M12, M13,
M21, M22, M23, M31, M32 et M33 sont donc réservées pour les transformations de
rotation, mise à l’échelle et inclinaison (ou cisaillement). Les propriétés OffsetX, OffsetY
et OffsetZ sont réservées à la translation.
composantes pour la rotation, la
mise à l’échelle et le cisaillement

Copyright 2013 Patrice REY

composantes pour la translation


D’un point de vue du code, on écrira donc:
Point4D pt = new Point4D(5, 0, 0, 1);
double theta = -Math.PI / 2;
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 59
double sin = Math.Sin(theta);
double cos = Math.Cos(theta);
Matrix3D matrice = new Matrix3D(
cos, 0, -sin, 0,
0, 1, 0, 0,
sin, 0, cos, 0,
0, 0, 0, 1);
Point4D pt_resultat = pt * matrice;

6.1 - Opérations sur la matrice 3D

L’UserControl OperationSurMatrice.xaml, dans le dossier chapitre_01, va nous permettre


d’effectuer des calculs sur des structures Matrix3D (figure 1.28).
FIGURE 1.28

Nous définissons une matrice mat1 de type Matrix3D qui représente une translation
de Tx=4 suivant l’axe des X, de Ty=2 suivant l’axe des Y et de Tz=1 suivant l’axe des Z.
60 Développez des applications 3D avec C#5 et WPF 4.5
Matrix3D mat1 = new Matrix3D(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
4, 2, 1, 1);
La propriété HasInverse nous retourne un booléen pour signifier si la matrice est
inversible. Par définition, une matrice M est inversible si il existe une matrice M-1 tel
que M*M-1=Mid avec Mid qui est la matrice identité et M-1 qui est la matrice inversée.
Dans la littérature, une matrice non inversible est appelée une matrice singulière, et
une matrice inversible est appelée une matrice non singulière ou régulière.
Si notre matrice mat1 est inversible alors on instancie une matrice mat2 en lui affectant
les valeurs de mat1. Puis on lui applique la méthode Invert qui calcule la matrice inverse.
if (mat1.HasInverse == true) {
x_infos.Text += «matrice mat1 est inversible» + RC;
mat2 = mat1;
mat2.Invert();
...
}
Pour vérifier que la matrice mat2 est bien une matrice inversée de la matrice mat1, on
instancie une nouvelle matrice mat3 et on lui affecte le produit matriciel de mat1 par
mat2.
if (mat1.HasInverse == true) {
...
Matrix3D mat3 = new Matrix3D();
mat3 = mat1 * mat2;
...
}
Il est à noter que la structure Matrix3D possède une surcharge des opérateurs qui
sont:
• * (multiplication) qui multiplie les matrices spécifiées.
• == (égalité) qui compare l’égalité exacte de deux instances de Matrix3D.
• != (inégalité) qui compare l’inégalité exacte de deux instances de Matrix3D.
Le code procédural nous donne le calcul. Si nous effectuons le calcul à la main pour
Copyright 2013 Patrice REY

vérifier que le produit des matrices donne bien la matrice identité, ce calcul se fait de
la façon suivante: les deux matrices sont des matrices 4x4, le produit sera une matrice
4x4 et chacune des composantes résultera du calcul d’une ligne par une colonne.
La figure 1.29 visualise le détail complet du calcul des 16 composantes N1 à N16 dans
cette multiplication de matrices.
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 61
FIGURE 1.29 matrice matrice
matrice inversée identité

C1 C2 C3 C4 N1 = L1*C1 = 1*1+0*0+0*0+0*(-4)=1
N2 = L1*C2 = 1*0+0*1+0*0+0*(-2)=0
N3 = L1*C3 = 1*0+0*0+0*1+0*(-1)=0
N4 = L1*C4 = 0
N5 = L2*C1 = 0
N6 = L2*C2 = 1
N7 = L2*C3 = 0
N8 = L2*C4 = 0
N9 = L3*C1 = 0
L1 N10 = L3*C2 = 0
L2 N11 = L3*C3 = 1
N12 = L3*C4 = 0
L3
N13 = L4*C1 = 0
L4 N14 = L4*C2 = 0
N15 = L4*C3 = 0
N16 = L4*C4 = 1

Comme le visualise la figure 1.30, nous avons un triangle repéré par les points P1(3,0,1),
P2(3,0,5) et P3(3,1,5). Nous allons appliquer à ces trois points la matrice mat1 de façon
à définir le triangle après la transformation (repéré par les points P1t, P2t et P3t).
FIGURE 1.30 Y

P1t

P3t

P1
X

P2t
P3

Z
P2
62 Développez des applications 3D avec C#5 et WPF 4.5
On instancie des points p1, p2 et p3 de type Point4D, en coordonnées homogènes, en
leur affectant les valeurs des coordonnées euclidiennes de P1, P2 et P3. Pour obtenir
les coordonnées de P1t, on instancie une nouvelle structure p1t de type Point4D et on
lui affecte le produit de p1 par la matrice mat1 (opérateur * surchargé pour la structure
Point4D).
Point4D p1 = new Point4D(3, 0, 1, 1);
x_infos.Text += «P1: = « + p1.ToString() + RC;
Point4D p1_t = new Point4D();
p1_t = p1 * mat1;
x_infos.Text += «P1t = P1 * mat1 « + p1_t.ToString() + RC;
Point4D p2 = new Point4D(3, 0, 5, 1);
x_infos.Text += «P2: = « + p2.ToString() + RC;
Point4D p2_t = new Point4D();
p2_t = p2 * mat1;
x_infos.Text += «P2t = P2 * mat1 « + p2_t.ToString() + RC;
Point4D p3 = new Point4D(3, 1, 5, 1);
x_infos.Text += «P3: = « + p3.ToString() + RC;
Point4D p3_t = new Point4D();
p3_t = p3 * mat1;
x_infos.Text += «P3t = P3 * mat1 « + p3_t.ToString() + RC;
D’un point de vue mathématique, comme nous avons pu le voir, les matrices 3D de
WPF sont des matrices transposées par rapport à celles utilisées dans l’algèbre linéaire
classique. Le détail du calcul des positions de P1t en fonction de P1 et de la matrice
mat1 est le suivant:

calcul de x2 Copyright 2013 Patrice REY

calcul de y2
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 63

calcul de z2

calcul de w

Il est important d’effectuer le calcul à la main lors de l’apprentissage car cela permet
de s’entraîner au calcul matriciel, et dès fois, cela permet de corriger des idées reçues.

6.2 - Transformations sur la matrice 3D

La structure Matrix3D possède des méthodes pour la rotation, la translation et la mise


à l’échelle des matrices. Voici les méthodes les plus fréquemment utilisées pour les
opérations sur les matrices 3D:
• Scale qui ajoute le Vector3D spécifié de mise à l’échelle à cette structure Matrix3D.
• ScaleAt qui met à l’échelle cette structure Matrix3D avec le Vector3D spécifié sur
le Point3D spécifié.
• Translate qui ajoute une translation de l’offset spécifié à la structure Matrix3D
actuelle.
• Rotate qui ajoute une transformation de rotation au Matrix3D actuel.
• RotateAt qui fait pivoter la structure Matrix3D sur le Point3D spécifié.
• Invert qui inverse cette structure Matrix3D.
• Matrix3D.Multiply qui multiplie les matrices spécifiées (méthode statique).
• Transform qui transforme le Point3D spécifié par le Matrix3D et retourne le
résultat; qui transforme les objets Point3D spécifiés dans le tableau par Matrix3D;
qui transforme le Point4D spécifié par Matrix3D et retourne le résultat; qui
transforme les objets Point4D spécifiés dans le tableau par Matrix3D et retourne
le résultat; qui transforme le Vector3D spécifié par Matrix3D; qui transforme les
objets Vector3D spécifiés dans le tableau par Matrix3D.
A noter qu’il n’y a pas de méthode pour la transformation d’inclinaison (ou de
cisaillement) pour la structure. Pour effectuer cette transformation, il faut passer
directement par les valeurs de la matrice Matrix3D.
Il y a aussi des méthodes suffixées par Prepend pour les transformations de rotation, de
mise à l’échelle et de translation. Par exemple, il y a pour la translation les méthodes
64 Développez des applications 3D avec C#5 et WPF 4.5
Translate et TranslatePrepend. La méthode Append ajoute la matrice spécifiée à la
matrice actuelle. C’est le mode par défaut. La méthode Prepend ajoute initialement une
matrice spécifiée à la matrice actuelle. Les méthodes Append et Prepend définissent
donc l’ordre d’application des matrices. De ce fait, si la méthode Translate ajoute
une translation de l’offset spécifié à la structure Matrix3D actuelle, alors la méthode
TranslatePrepend ajoute initialement une translation de l’offset spécifié à la structure
Matrix3D actuelle. Append signifie que la nouvelle opération est appliquée après
l’opération précédente, et Prepend signifie que la nouvelle opération est appliquée
avant l’opération précédente lors des associations de transformations.
L’UserControl TransformationSurMatrice.xaml, dans le dossier chapitre_01, va nous
permettre d’effectuer des calculs de transformations sur des structures Matrix3D
(figure 1.31).
FIGURE 1.31

Copyright 2013 Patrice REY


CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 65
On instancie une matrice mat_orig de type Matrix3D avec ses 16 composantes.
//matrice originale
Matrix3D mat_orig = new Matrix3D(
1, 2, 3, 4,
2, 1, 0, 0,
0, 0, 1, 0,
1, 2, 3, 1);
AfficherMatriceTexte(mat_orig, «matrice originale mat_orig»);
La méthode Scale permet d’ajouter le Vector3D spécifié de mise à l’échelle (0.5 suivant
X, 1.5 suivant Y et 2.5 suivant Z) à la structure mat_scale de type Matrix3D (la matrice
mat_scale est affectée par la structure mat_orig). Nous obtenons comme résultat une
matrice dont les composantes sont 0.5,3,7.5,4,1,1.5,0,0,0,0,2.5,0,0.5,3,7.5,1.
//mise a l’echelle
Matrix3D mat_scale = mat_orig;
mat_scale.Scale(new Vector3D(0.5, 1.5, 2.5));
AfficherMatriceTexte(mat_scale,
«mat_scale: mat_scale.Scale(new Vector3D(0.5, 1.5, 2.5))»);
Si nous effectuons le calcul à la main pour vérification, nous obtenons bien le résultat
trouvé.

La méthode ScalePrepend permet d’ajouter initialement le Vector3D de mise à l’échelle


spécifié (0.5 suivant X, 1.5 suivant Y et 2.5 suivant Z) à la structure Matrix3D actuelle
mat_scale_prepend (la matrice mat_scale_prepend est affectée par la structure mat_
orig). Nous obtenons comme résultat une matrice dont les composantes sont 0.5,1,1.5
,2,3,1.5,0,0,0,0,2.5,0,1,2,3,1.
Matrix3D mat_scale_prepend = mat_orig;
mat_scale_prepend.ScalePrepend(new Vector3D(0.5, 1.5, 2.5));
AfficherMatriceTexte(mat_scale_prepend, «mat_scale_prepend:
mat_scale_prepend.ScalePrepend(new Vector3D(0.5, 1.5, 2.5))»);
Si nous effectuons le calcul à la main pour vérification, nous obtenons bien le résultat
trouvé.
66 Développez des applications 3D avec C#5 et WPF 4.5

La méthode Translate permet d’ajouter une translation (Vector3D de 100 suivant X,


150 suivant Y et 200 suivant Z) de l’offset spécifié à la structure Matrix3D actuelle
mat_trans (la matrice mat_trans est affectée par la structure mat_orig). Nous obtenons
comme résultat une matrice dont les composantes sont 401,602,803,4,2,1,0,0,0,0,1,0,1
01,152,203,1.
//translation
Matrix3D mat_trans = mat_orig;
mat_trans.Translate(new Vector3D(100,150,200));
AfficherMatriceTexte(mat_trans, «mat_trans:
mat_trans.Translate(new Vector3D(100,150,200))»);
Si nous effectuons le calcul à la main pour vérification, nous obtenons bien le résultat
trouvé.

La méthode TranslatePrepend permet d’ajouter initialement une translation (Vector3D


de 100 suivant X, 150 suivant Y et 200 suivant Z) de l’offset spécifié à la structure
Matrix3D actuelle mat_trans_prepend (la matrice mat_trans_prepend est affectée par la
Copyright 2013 Patrice REY

structure mat_orig). Nous obtenons comme résultat une matrice dont les composantes
sont 1,2,3,4,2,1,0,0,0,0,1,0,401,352,503,401.
Matrix3D mat_trans_prepend = mat_orig;
mat_trans_prepend.TranslatePrepend(new Vector3D(100, 150, 200));
AfficherMatriceTexte(mat_trans_prepend, «mat_trans_prepend:
mat_trans.TranslatePrepend(new Vector3D(100,150,200))»);
Si nous effectuons le calcul à la main pour vérification, nous obtenons bien le résultat
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 67
trouvé.

La méthode Rotate ajoute une transformation de rotation à la structure Matrix3D


actuelle. Elle prend en paramètre un objet Quaternion. Un Quaternion est une
structure qui représente une rotation en 3D. Cette structure expose principalement les
propriétés suivantes:
• Axis qui obtient l’axe du quaternion.
• Angle qui obtient l’angle du quaternion, en degrés.
• IsIdentity qui obtient une valeur booléenne qui indique si le quaternion spécifié est
un quaternion Identity.
• Quaternion.Identity qui obtient le quaternion Identity (propriété statique).
• IsNormalized qui obtient une valeur booléenne qui indique si le quaternion est
normalisé ou pas.
Un des constructeurs avec paramètres permet d’instancier un Quaternion avec un
Vector3D (qui représente l’axe de rotation) et une valeur de type double qui représente
l’angle de rotation autour de l’axe spécifié (angle en degrés).
A noter que la coordonnée homogène w du quaternion est calculée automatiquement.
Ici nous effectuons une rotation en instanciant une structure Quaternion. Cette
structure Quaternion est composé d’un axe de rotation spécifié par un Vector3D(1,2,3)
et un angle de rotation de 45 degrés autour de cette axe. La matrice mat_rotate est
affectée par la matrice mat_orig. Puis on effectue la transformation de rotation. On
obtient une matrice mat_rotate dont les composantes (arrondies à 3 chiffres après la
virgule) sont 1, 2, 3, 4, 0.931, 2.008, -0.316, 0, 0.441, -0.063, 0.895, 0, 1, 2, 3, 1.
//rotation
Matrix3D mat_rotate = mat_orig;
mat_rotate.Rotate(new Quaternion(new Vector3D(1, 2, 3), 45));
AfficherMatriceTexte(mat_rotate, 3, «mat_rotate:
mat_rotate.Rotate(new Quaternion(new Vector3D(1, 2, 3), 45))»);
Pour calculer le quaternion à partir du vecteur représentant l’axe de rotation et l’angle
de rotation, le vecteur représentant l’axe doit être normalisé. Vous pouvez calculer les
composantes du quaternion en une unité d’axe de rotation et d’angle de rotation.
68 Développez des applications 3D avec C#5 et WPF 4.5
Supposons que l’axe de rotation pour une rotation 3D soit représenté par un vecteur
unité, de type Vector3D, avec les composantes ax, ay et az, et avec β l’angle de rotation.
Le quaternion aura comme composantes (x,y,z,w) en coordonnées homogènes données
par la formule:
x=ax*sin(β/2) , y=ay*sin(β/2), z=az*sin(β/2) et w=cos(β/2).
Dans notre exemple, nous effectuons une rotation de la matrice mat_orig d’un angle
de 45 degrés autour d’un axe spécifié par Vector3D(1,2,3). La normalisation du vecteur
donne (avec 14=12+22+32)

Avec un angle de rotation de 45 degrés, les coordonnées homogènes du quaternion


sont donc de

Comme ce quaternion est normalisé, on peut construire la matrice de rotation qui est

Copyright 2013 Patrice REY

Maintenant si on applique la rotation à la matrice mat_orig, on obtient la matrice mat_


rotate avec ses composantes. On retrouve bien les mêmes valeurs que par le code
procédural.
CHAPITRE 1 Point, vecteur et matrice dans l’espace 3D 69

On effectue la même démarche avec la méthode RotatePrepend qui ajoute initialement


une rotation spécifiée par un quaternion à la structure Matrix3D actuelle.
Matrix3D mat_rotate_prepend = mat_orig;
mat_rotate_prepend.RotatePrepend(
new Quaternion(new Vector3D(1, 2, 3), 45));
AfficherMatriceTexte(mat_rotate_prepend, 3, «mat_rotate_prepend:
mat_rotate_prepend.RotatePrepend(
new Quaternion(new Vector3D(1, 2, 3), 45))»);
La méthode RotateAt fait pivoter une structure Matrix3D sur le Point3D spécifié. Elle
reçoit en paramètre une structure Quaternion et un Point3D qui représente le point
central sur lequel pivoter.
2
Les projections

Au sommaire de ce chapitre
• définition et caractéristique de la projection perspective
• définition et caractéristique de la projection parallèle
• utilisation pratique de la matrice de projection perspective
• utilisation pratique de la matrice de projection parallèle
72 Programmez des jeux vidéo 3D avec C#5 et WPF
En infographie, les projections transforment les points d’un système de coordonnées
de dimension 3 en des points d’un système de coordonnées de dimension 2. Les objets
3D sont projetés dans un plan 2D, permettant de visualiser à un moment donné le
rendu d’une scène 3D.
Nous allons aborder au cours de ce chapitre les types de projection (perspective et
parallèle) en les mettant en œuvre pour se rendre compte de leurs différences.

1 - Les types de projection

Le rendu d’une scène 3D consiste à obtenir une visualisation réaliste de la scène dans
un plan de visualisation 2D. La projection d’un objet 3D est définie par un ensemble
de rayons de projection droits, appelés des projecteurs, qui partent d’un centre de
projection, passent par chaque point de l’objet, et intersectent un plan de projection.
Les intersections des rayons avec le plan de projection forment la projection de
l’objet. Le centre de projection se situe généralement à une distance finie du plan de
projection. Pour certains types de projections, il est pourtant pratique de considérer
un centre de projection qui tend à être infiniment loin. Les figures 2.1 et 2.2 visualisent
deux projections différentes d’un segment de droite.
FIGURE 2.1 A

A’
B’ plan de projection
les projecteurs

Copyright 2013 Patrice REY

centre de projection

Comme la projection d’un segment de droite est elle-même un segment de droite,


il suffit de projeter les points extrêmes. La figure 2.1 visualise un segment AB et sa
projection perspective A’B’. La figure 2.2 visualise un segment AB et sa projection
parallèle A’B’ (les projecteurs AA’ et BB’ sont parallèles).
CHAPITRE 2 Les projections 73
FIGURE 2.2 A

A’
B’ plan de projection
les projecteurs

centre de projection
à l’infini
La classe de projections que nous étudions ici est connue sous le nom de projections
géométriques planes, puisque la projection se produit sur une surface plane et non
sur une surface courbée, et que les projecteurs sont des droites et non des courbes. A
contrario, les projections cartographiques ne sont ni planes ni géométriques.
Les projections (terme usuellement employé pour les projections géométriques) sont
essentiellement de deux types: perspective et parallèle. Leur distinction revient à la
relation du centre de projection avec le plan de projection.
Si la distance entre le centre et le plan est finie, la projection est alors une projection
perspective. Si la distance entre le centre et le plan est infinie, la projection est alors
une projection parallèle. Dans une projection perspective, le centre de projection est
explicitement spécifié. Dans une projection parallèle, la direction de projection est
explicitement spécifiée.

1.1 - Les projections perspectives

Les projections perspectives de tout ensemble de lignes parallèles qui ne sont pas
parallèles au plan de projection convergent vers un point de fuite. Le point de fuite
peut être considéré comme la projection d’un point à l’infini. Il existe donc un nombre
infini de points de fuite dont chacun représente une direction dans laquelle une droite
peut être orientée. Si l’ensemble des lignes est parallèle à l’un des axes principaux, le
point de fuite est dit point de fuite axial (ou principal). Il existe au plus trois points de
fuite axiaux correspondant au nombre d’axes que le plan de projection intersecte.
Par exemple, si le plan de projection n’intersecte que l’axe Z (et lui est donc
74 Programmez des jeux vidéo 3D avec C#5 et WPF
perpendiculaire), l’axe Z seulement possède un point de fuite axial car les droites
parallèles à l’axe X ou à l’axe Y sont aussi parallèles au plan de projection et n’ont donc
pas de point de fuite.
Les projections perspectives sont classées selon leur nombre de points de fuite
principaux, et donc selon le nombre d’axes qui traversent le plan de projection. La
figure 2.3 visualise une projection perspective d’un cube avec un point de fuite. Dans
cette projection, les lignes parallèles aux axes X et Y ne convergent pas alors que celles
parallèles à l’axe Z convergent.
FIGURE 2.3 point de fuite sur l’axe Z

Z
La figure 2.4 visualise la construction d’une perspective à deux points de fuite
principaux. Les lignes parallèles à l’axe Y ne convergent pas dans la projection. Ce type
de perspective est couramment employé dans les dessins d’architecture, d’ingénierie
et de conception industrielle.
FIGURE 2.4 Y Copyright 2013 Patrice REY

point de fuite
point de fuite sur l’axe Z
sur l’axe X

Z X
CHAPITRE 2 Les projections 75
La figure 2.5 visualise une perspective à trois points de fuite. Ce type de perspective
n’est pas très utilisé car il n’ajoute que peu de réalisme supplémentaire.
FIGURE 2.5 Y
point de fuite
sur l’axe Z
point de fuite
sur l’axe X

point de fuite sur l’axe Y

1.2 - Les projections parallèles

Les projections parallèles peuvent être classées selon deux types: suivant la relation
entre la direction de projection et suivant la normale du plan de projection. Pour les
projections parallèles orthonormales, ces directions sont soit identiques soit dans le
sens inverse, la direction de projection est donc perpendiculaire au plan de projection.
En projections parallèles obliques, ce n’est pas le cas.
Les projections orthonormales les plus connues sont la projection de façade, la
projection de plan, et la projection de côté. Pour toutes ces projections, le plan de
projection est perpendiculaire à un axe principal qui représente donc la direction de
projection.
La figure 2.6 montre la construction de ces trois types de projections qui sont souvent
utilisées dans les dessins d’ingénierie pour représenter des pièces, des assemblages et
des structures. Les projections orthonormales axonométriques utilisent des plans de
projection qui ne sont pas perpendiculaires à un axe principal. Elles représentent donc
plusieurs faces d’un même objet à la fois. Le parallélisme des droites est préservé mais
pas les angles.
La projection isométrique est une projection axonométrique couramment utilisée.
76 Programmez des jeux vidéo 3D avec C#5 et WPF
La normale du plan de projection, c’est-à-dire la direction de projection, a des angles
égaux avec chaque axe principal.
FIGURE 2.6 plan de
projection de
plan
plan de projection
de côté

plan de
projection de
façade

Pour les projections obliques (second type de projection parallèle), la normale de


leur plan de projection est différente de leur direction de projection, contrairement
aux projections orthonormales. Elles combinent les propriétés des projections
orthonormales de façade, de plan de côté et celles de la projection axonométrique.
La figure 2.7 représente les relations logiques entre les différents types de projections.
Le point commun de toutes les projections est qu’elles utilisent un plan de projection
et, soit un centre de projection pour les projections perspectives, soit une direction de
projection pour les projections parallèles.
FIGURE 2.7
projection géométrique plane

projection parallèle projection perspective

projection projection
projection projection avec 1 point de avec 3 points
orthographique oblique fuite de fuite
projection de
projection de cabinet projection
plan
Copyright 2013 Patrice REY

perspective avec 2 points


projection de cavalière de fuite
façade
autres
projection de projections
côté
projection projection isométrique
axonométrique
autres projections
CHAPITRE 2 Les projections 77

2 - La matrice de la projection perspective

Pour effectuer un rendu de la scène 3D, il faut positionner une caméra dans la scène.
La figure 2.8 visualise une caméra perspective avec son plan de projection et son
volume d’observation. Tous les points des objets 3D qui se trouvent dans le volume
d’observation, vont être projetés dans le plan de projection. Le point projeté sera à
l’intersection du plan de projection et de la droite qui passe par le centre de projection
et par le point de l’objet 3D.
FIGURE 2.8
plan de
projection

haut

Y gauche

Z
bas droit
X
centre de
projection
(caméra) plan de
près volume
d’observation
plan de loin

plan de
projection

hauteur

Y
largeur
Z
angle de
X vue
plan de loin
centre de
projection
(caméra) volume
d’observation
78 Programmez des jeux vidéo 3D avec C#5 et WPF
En fixant un angle de vue, une distance du plan de projection (distance entre le plan de
projection et le centre de projection) et une distance du plan de loin (distance entre le
centre de projection et le plan de loin), les points en 3D dans le volume d’observation
peuvent être projetés dans le plan de projection.
Le rendu de la scène 3D est donc une projection géométrique plane 2D. De plus, la
caméra (figure 2.9) est positionnée dans l’espace 3D. Elle vise un endroit de l’espace
par son axe Z (c’est la direction du regard ou Look Direction). Elle peut aussi pivoter
sur elle-même d’un angle dans le plan XY (c’est la direction du haut ou Up Direction).
Avec un plan de projection perpendiculaire à l’axe Z de la caméra, si la direction du
haut tourne, les points projetés sur le plan de projection seront à des emplacements
différents.
FIGURE 2.9

direction du
haut (up
direction) plan de
projection direction du
regard (look
Y direction)

Z
angle de
vue
centre de
projection
(caméra) X
volume
d’observation
plan de loin

L’UserControl ProjectionPerspective.xaml, dans le dossier chapitre_02, permet de mettre


en œuvre l’utilisation d’une projection perspective en fixant les différents paramètres
de la caméra pour déterminer le rendu en 2D (figures 2.10 et 2.11).
Copyright 2013 Patrice REY

Différents contrôles de type Slider permettent de régler, dans des plages de valeurs,
la position de la caméra dans l’espace 3D, la direction du regard de la caméra et
l’inclinaison (direction du haut) de la caméra.
Le cube possède six faces de couleurs différentes. Il y a six boutons qui permettent
d’effectuer un réglage prédéfini de façon à voir le cube de face, de droite, de gauche, de
dessus, de dessous et par l’arrière. Un dernier bouton permet de revenir à la position
CHAPITRE 2 Les projections 79
initiale. Les axes sont repérés par des couleurs: rouge pour l’axe des X, bleu pour l’axe
des Y et vert pour l’axe des Z.
FIGURE 2.10

La figure 2.11 montre le cube quand on clique sur le bouton face gauche (repère 1) et
quand on clique sur le bouton face arrière (repère 2).
80 Programmez des jeux vidéo 3D avec C#5 et WPF
FIGURE 2.11

Copyright 2013 Patrice REY

Pour ce qui est de la réalisation de la géométrie du cube proprement dit et de l’application


des textures colorées, nous verrons cela plus en détail dans un autre chapitre. Ce que
nous allons surtout voir ici c’est l’incidence des paramètres de la caméra sur le rendu
CHAPITRE 2 Les projections 81
de la scène. L’espace 3D est visualisé par un objet Viewport3D (une sorte de Canvas
pour un rendu en 3D). On ajoute une caméra x_camera_matrice de type MatrixCamera.
Un objet MatrixCamera représente une caméra qui spécifie les transformations de
projection et d’affichage comme objets Matrix3D.
<Viewport3D ClipToBounds=’True’ Width=’400’ Height=’400’>
<Viewport3D.Camera>
<MatrixCamera x:Name=’x_camera_matrice’ />
</Viewport3D.Camera>
...
</Viewport3D>
On définit la position de la caméra par m_pos_camera de type Point3D (la caméra
a une position donc des coordonnées 3D). La direction du regard de la caméra est
représentée par un vecteur m_look_direction de type Vector3D. La direction du haut
de la caméra est représentée par un vecteur m_up_direction de type Vector3D. L’angle
de vue est représenté par une valeur de type double (exprimant une valeur en degré).
Le plan de près m_plan_pres (plan de projection) et le plan de loin m_plan_loin sont
des valeurs de type double. Le ratio m-aspect_ratio de type double représente le ratio
concernant la largeur par rapport à la hauteur du plan de projection.
private Point3D m_pos_camera = new Point3D(3, 4, 5);
private Vector3D m_look_direction = new Vector3D(-3, -4, -5);
private Vector3D m_up_direction = new Vector3D(0, 1, 0);
private double m_angle_vue = 60;
private double m_plan_pres = 1;
private double m_plan_loin = 100;
private double m_aspect_ratio = 1;
La classe MatrixCamera expose principalement les propriétés suivantes:
• ViewMatrix qui définit une structure Matrix3D comme matrice de transformation
de l’affichage.
• ProjectionMatrix qui définit une structure Matrix3D comme matrice de
transformation de la projection.
• Transform qui définit la transformation Transform3D appliquée à la caméra.
A chaque changement d’une position sur les glissières de type Slider, la méthode
FixerLaMatriceDeLaCamera est appelée pour recalculer la matrice Matrix3D de
transformation de l’affichage (propriété ViewMatrix) et pour recalculer la matrice
Matrix3D de transformation de la projection (propriété ProjectionMatrix).
// matrice de la caméra
private void FixerLaMatriceDeLaCamera() {
x_camera_matrice.ViewMatrix = FixerMatriceVue(m_pos_camera, m_look_direction,
m_up_direction);
x_camera_matrice.ProjectionMatrix =
82 Programmez des jeux vidéo 3D avec C#5 et WPF
FixerParametrePerspective(m_angle_vue, m_aspect_ratio, m_plan_pres,
m_plan_loin);}
Pour calculer la matrice de transformation de l’affichage, on passe les paramètres m_pos_
camera, m_look_direction et m_up_direction à la méthode FixerMatriceVue. Pour calculer
la matrice de transformation de la projection, on passe les paramètres m_angle_vue,
m_aspect_ratio, m_plan_pres et m_plan_loin à la méthode FixerParametrePerspective.
Avant de poursuivre, nous allons introduire les notions mathématiques de base pour
les projections géométriques planes. Nous avons le plan de projection de la projection
perspective qui est perpendiculaire à l’axe Z de la caméra en z=d (figure 2.12).
FIGURE 2.12 Y
X P(x,y,z)

Pp(xp,yp,zp)

X Y
P(x,y,z) P(x,y,z)
Pp(xp,yp,zp) Pp(xp,yp,zp)

Z Z
d d
Voyons le cas de la projection d’un point P sur un plan de projection qui est parallèle
au plan XY, et se situe en z=d. Afin de calculer Pp=(xp,yp,zp), c’est-à-dire la projection
perspective de P(x,y,z) sur le plan de projection en z=d, nous utilisons les triangles
similaires représentés dans la figure 2.12 et nous trouvons les rapports:

Copyright 2013 Patrice REY

En multipliant chaque côté par d, on obtient:


CHAPITRE 2 Les projections 83
La distance d est simplement un facteur d’échelle appliquée à xp et à yp. La division par
z rend la projection perspective des objets plus éloignés et plus petites que celles des
objets plus proches. Toute valeur de z est permise sauf celle de z=0. La transformation
peut être représentée par la matrice

Multiplier le point P par la matrice donne un point général en coordonnées homogènes


(X,Y,Z,W):

En divisant les composantes par W, c’est-à-dire z/d, et en éliminant la quatrième


coordonnée pour retourner en 3D, nous avons:

On obtient la coordonnée z transformée, c’est-à-dire d, qui est la position du plan de


projection dans la direction de l’axe Z. A partir de là, on peut implémenter la méthode
FixerMatriceVue pour trouver la matrice de transformation de l’affichage.
public static Matrix3D FixerMatriceVue(Point3D camera_position,
Vector3D look_direction, Vector3D up_direction) {
//normalisation des vecteurs
look_direction.Normalize();
up_direction.Normalize();
84 Programmez des jeux vidéo 3D avec C#5 et WPF
//definir les vecteurs XScale, YScale, et ZScale:
double denom = Math.Sqrt(1 -
Math.Pow(Vector3D.DotProduct(look_direction, up_direction), 2));
Vector3D XScale = Vector3D.CrossProduct(look_direction,
up_direction) / denom;
Vector3D YScale = (up_direction - (Vector3D.DotProduct(
up_direction, look_direction)) * look_direction) / denom;
Vector3D ZScale = look_direction;
//construction de la matrice M
Matrix3D M = new Matrix3D();
M.M11 = XScale.X;
M.M21 = XScale.Y;
M.M31 = XScale.Z;
M.M12 = YScale.X;
M.M22 = YScale.Y;
M.M32 = YScale.Z;
M.M13 = ZScale.X;
M.M23 = ZScale.Y;
M.M33 = ZScale.Z;
// Translater la camera de sa position a l’origine
Matrix3D translateMatrix = new Matrix3D();
translateMatrix.Translate(new Vector3D(-camera_position.X,
-camera_position.Y, -camera_position.Z));
//definir la reflexion sur l’axe z
Matrix3D reflectMatrix = new Matrix3D();
reflectMatrix.M33 = -1;
//construction de la matrice d’affichage
Matrix3D viewMatrix = translateMatrix * M * reflectMatrix;
return viewMatrix;
}
Puis on peut implémenter la méthode FixerParametrePerspective pour trouver la
matrice de transformation de projection.
public static Matrix3D FixerParametrePerspective(double angle_vue,
double aspect_ratio, double plan_pres, double plan_loin) {
Matrix3D matrice_perspective = new Matrix3D();
double grandeur_y = 1.0 / Math.Tan(angle_vue * Math.PI / 180 / 2);
double grandeur_x = grandeur_y / aspect_ratio;
matrice_perspective.M11 = grandeur_x;
matrice_perspective.M22 = grandeur_y;
Copyright 2013 Patrice REY

matrice_perspective.M33 = plan_loin / (plan_pres - plan_loin);


matrice_perspective.M34 = -1.0;
matrice_perspective.OffsetZ =
plan_pres * plan_loin / (plan_pres - plan_loin);
matrice_perspective.M44 = 0;
return matrice_perspective;
}
Ainsi, en modifiant la position des Slider, on recalcule les matrices nécessaires à
CHAPITRE 2 Les projections 85
l’affichage et à la projection. On obtient visuellement le rendu pour en apprécier les
incidences.

3 - La matrice de la projection parallèle

Pour effectuer un rendu de la scène 3D, il faut positionner une caméra dans la scène.
La figure 2.13 visualise une caméra parallèle avec son plan de projection et son
volume d’observation. Tous les points des objets 3D qui se trouvent dans le volume
d’observation, vont être projetés dans le plan de projection. Le point projeté sera à
l’intersection du plan de projection et de la droite qui passe par le centre de projection
et par le point de l’objet 3D. Comme dans la projection parallèle, le centre de projection
de la caméra se trouve à l’infini, la droite qui sert à trouver le point projeté est parallèle
à l’axe Z de la caméra.
FIGURE 2.13 plan de
projection
haut

gauche
Z

X droit
centre de bas
projection à
l’infini (caméra)
plan de volume
près d’observation plan de
plan de
projection loin

hauteur

X largeur
centre de plan de
projection à près plan de
l’infini (caméra) volume loin
d’observation
86 Programmez des jeux vidéo 3D avec C#5 et WPF
De plus, la caméra (figure 2.13) est positionnée dans l’espace 3D. Elle vise un endroit
de l’espace par son axe Z (c’est la direction du regard ou Look Direction). Elle peut
aussi pivoter sur elle-même d’un angle dans le plan XY (c’est la direction du haut ou
Up Direction). Avec un plan de projection perpendiculaire à l’axe Z de la caméra, si la
direction du haut tourne, les points projetés sur le plan de projection seront à des
emplacements différents. Le plan de projection est caractérisé par sa largeur et sa
hauteur, et par la distance qui le sépare de la caméra.
L’UserControl ProjectionParallele.xaml, dans le dossier chapitre_02, permet de mettre
en œuvre l’utilisation d’une projection parallele en fixant les différents paramètres de
la caméra pour déterminer le rendu en 2D (figure 2.15).
Différents contrôles de type Slider permettent de régler, dans des plages de valeurs,
la position de la caméra dans l’espace 3D, la direction du regard de la caméra et
l’inclinaison (direction du haut) de la caméra. Le cube possède six faces de couleurs
différentes. Il y a six boutons qui permettent d’effectuer un réglage prédéfini de façon
à voir le cube de face, de droite, de gauche, de dessus, de dessous et par l’arrière. Un
dernier bouton permet de revenir à la position initiale. Les axes sont repérés par des
couleurs: rouge pour l’axe des X, bleu pour l’axe des Y et vert pour l’axe des Z.
D’un point de vue mathématique (figure 2.14), on a xp=d, yp=d et zp=d.
FIGURE 2.14 Y
X

Pp(xp,yp,zp) P(x,y,z)

X Y
Pp(xp,yp,zp) P(x,y,z) Pp(xp,yp,zp) P(x,y,z)

Z Z
d d
Copyright 2013 Patrice REY

Cette projection peut être représentée par la matrice:


CHAPITRE 2 Les projections 87
FIGURE 2.15

Le point projeté Pp a donc les coordonnées homogènes (x,y,d,1) par transformation


avec la matrice parallèle Mpar.
88 Programmez des jeux vidéo 3D avec C#5 et WPF

A chaque changement d’une position sur les glissières de type Slider, la méthode
FixerLaMatriceDeLaCamera est appelée pour recalculer la matrice Matrix3D de
transformation de l’affichage (propriété ViewMatrix) et pour recalculer la matrice
Matrix3D de transformation de la projection (propriété ProjectionMatrix).
private void FixerLaMatriceDeLaCamera() {
x_camera_matrice.ViewMatrix = FixerMatriceVue(
m_pos_camera, m_look_direction, m_up_direction);
x_camera_matrice.ProjectionMatrix = FixerParametreParallele(
m_largeur, m_largeur, m_plan_pres, m_plan_loin);
}
Pour calculer la matrice de transformation de l’affichage, on passe les paramètres m_
pos_camera, m_look_direction et m_up_direction à la méthode FixerMatriceVue. Pour
calculer la matrice de transformation de la projection, on passe les paramètres m_
largeur, m_plan_pres et m_plan_loin à la méthode FixerParametreParallele.
Dans la projection parallèle, le plan de projection a des dimensions qui sont fonction
de la largeur et de la hauteur que l’on fixe. Ici on définit la largeur a une valeur de 10 et
la hauteur a une valeur de 5. On peut implémenter la méthode FixerParametreParallele
pour trouver la matrice de transformation de projection.
public static Matrix3D FixerParametreParallele(double largeur,
double hauteur, double plan_pres, double plan_loin) {
Matrix3D matrice_parallele = new Matrix3D();
matrice_parallele.M11 = 2 / largeur;
matrice_parallele.M22 = 2 / hauteur;
Copyright 2013 Patrice REY

matrice_parallele.M33 = 1 / (plan_pres - plan_loin);


matrice_parallele.OffsetZ = plan_pres / (plan_pres - plan_loin);
return matrice_parallele;
}
3
Les transformations 3D

Au sommaire de ce chapitre
• définition des classes de transformations 3D
• utilisation pratique de la transformation ScaleTransform3D
• utilisation pratique de la transformation TranslateTransform3D
• utilisation pratique de la transformation RotateTransform3D
• utilisation pratique de la transformation MatrixTransform3D
• principe des combinaisons de transformations
90 Programmez des jeux vidéo 3D avec C#5 et WPF
Le principe des transformations 3D est identique à celui des transformations 2D. Il
consiste en l’application d’un système de coordonnées transformées mappé sur le
système de coordonnées initial en fonction d’une matrice. Il y a des transformations de
haut niveau et des transformations par application directe d’une matrice. Nous allons
voir dans ce chapitre les classes de haut niveau que WPF fournit pour une mise en
œuvre plus simple.

1 - Les classes des transformations 3D

Les classes de transformations 3D de haut niveau qui sont fournies par WPF héritent
de la classe abstraite Transform3D. La classe Transform3D se trouve dans l’espace
de noms System.Windows.Media.Media3D (assembly PresentationCore.dll). Comme le
visualise l’arbre d’héritage de cette classe (figure 3.1), ses classes dérivées sont:
• AffineTransform3D est une classe abstraite qui sert de classe de base de laquelle
dérivent toutes les transformations affines concrètes 3D (translation, rotation et
mise à l’échelle).
• MatrixTransform3D est une classe qui crée une transformation spécifiée par
une structure Matrix3D, utilisée pour manipuler des objets ou des systèmes de
coordonnées dans l’espace universel 3D.
• Transform3DGroup est une classe qui représente une transformation qui
correspond à un élément composite des enfants Transform3D de la collection
Transform3DCollection.
Les classes dérivées de la classe AffineTransform3D représentent les transformations
de haut niveau. Il y a:
• ScaleTransform3D est une transformation affine qui met à l’échelle (agrandissement
ou réduction) un objet dans le plan (XYZ) tridimensionnel, à partir d’un point
central défini; les facteurs d’échelle sont définis sur les axes X, Y et Z à partir d’un
point central.
• TranslateTransform3D est une transformation affine qui déplace un objet dans le
plan (XYZ) tridimensionnel.
Copyright 2013 Patrice REY

• RotateTransform3D est une transformation affine qui spécifie une transformation


de rotation dans l’espace 3D.

2 - La transformation ScaleTransform3D

La classe ScaleTransform3D représente une transformation affine qui met à l’échelle


(agrandissement ou réduction) un objet dans le plan (XYZ) tridimensionnel, à partir
CHAPITRE 3 Les transformations 3D 91
FIGURE 3.1

d’un point central défini. Les facteurs d’échelle sont définis sur les axes X, Y et Z à partir
d’un point central. Elle expose principalement les propriétés suivantes:
• CenterX qui définit la coordonnée x du point central de la transformation.
• CenterY qui définit la coordonnée y du point central de la transformation.
• CenterZ qui définit la coordonnée z du point central de la transformation.
• ScaleX qui définit le facteur d’échelle sur l’axe X.
• ScaleY qui définit le facteur d’échelle sur l’axe Y.
• ScaleZ qui définit le facteur d’échelle sur l’axe Z.
• Value qui obtient une représentation Matrix3D de cette transformation.
• IsAffine qui obtient une valeur qui indique si la transformation est affine ou pas.
• Inverse qui obtient la transformation inverse de cet objet si elle existe.
92 Programmez des jeux vidéo 3D avec C#5 et WPF
Le constructeur par défaut de cette classe est ScaleTransform3D(). Plusieurs
constructeurs avec paramètres sont disponibles avec notamment:
• ScaleTransform3D(Vector3D) qui initialise une nouvelle instance de la classe
ScaleTransform3D à l’aide du Vector3D spécifié.
• ScaleTransform3D(Vector3D, Point3D) qui initialise une nouvelle instance de la
classe ScaleTransform3D avec les Vector3D et Point3D spécifiés.
• ScaleTransform3D(Double, Double, Double) qui initialise une nouvelle instance de la
classe ScaleTransform3D à l’aide des facteurs d’échelle spécifiés.
• ScaleTransform3D(Double, Double, Double, Double, Double, Double) qui initialise
une nouvelle instance de la classe ScaleTransform3D à l’aide des coordonnées de
centre spécifiées et des facteurs d’échelle.
L’UserControl ScaleTransform3d.xaml (figures 3.2 et 3.3), dans le dossier chapitre_03,
permet de visualiser un cube coloré (avec six faces colorées différentes) pour lequel on
peut faire varier la mise à l’échelle suivant X, suivant Y et suivant Z. Les deux modes de
projection pour la visualisation du cube en 3D sont accessibles par des RadioButton
à cocher (projection perspective et projection parallèle). Un conteneur de type Grid
affiche les composantes de la matrice de transformation ScaleTransform3D en cours. On
ajoute à la définition du cube coloré en XAML, un groupe, de type Transform3DGroup,
contenant les transformations 3D à effectuer. Une transformation de mise à l’échelle
x_cub_echelle de type ScaleTransform3D est ajoutée, en lui fixant le centre de la
transformation à la coordonnée CenterX=0, CenterY=0 et CenterZ=0.
<Transform3DGroup>
<TranslateTransform3D OffsetZ=’1’></TranslateTransform3D>
<ScaleTransform3D
x:Name=’x_cube_echelle’
CenterX=’0’ CenterY=’0’
CenterZ=’0’></ScaleTransform3D>
</Transform3DGroup>
Lors du déplacement d’une glissière de type Slider, on appelle la méthode
TransformerCube. Elle permet de modifier les propriétés ScaleX, ScaleY et ScaleZ de la
transformation en fonction des valeurs relevées sur les glissières.
Copyright 2013 Patrice REY

//appliquer une transformation de mise a l’echelle


private void TransformerCube() {
if (x_slider_scale_x != null && x_slider_scale_y != null
&& x_slider_scale_z!=null) {
x_cube_echelle.ScaleX = x_slider_scale_x.Value;
x_cube_echelle.ScaleY = x_slider_scale_y.Value;
x_cube_echelle.ScaleZ = x_slider_scale_z.Value;
AfficherMatriceEchelle();
} }
CHAPITRE 3 Les transformations 3D 93
FIGURE 3.2
ScaleY = 2.52

Mise à l’échelle (avec une


projection perspective)
ScaleX = 1.61
ScaleZ = 1.29
94 Programmez des jeux vidéo 3D avec C#5 et WPF
FIGURE 3.3
ScaleY = 2.60

Mise à l’échelle (avec une


projection parallèle)
ScaleX = 1.61
ScaleZ = 1.15

Copyright 2013 Patrice REY


CHAPITRE 3 Les transformations 3D 95
La méthode AfficherMatriceEchelle permet d’afficher le contenu des composantes de la
matrice de transformation de mise à l’échelle. On récupère la matrice matrice_echelle
de type Matrix3D par la propriété Value de la transformation x_cube_echelle. Il suffit
alors de lire les 16 composantes et de les afficher au format texte.
private void AfficherMatriceEchelle() {
if (x_grid_matrice != null) {
Matrix3D matrice_echelle = x_cube_echelle.Value;
x_mat_m11.Text = Math.Round(matrice_echelle.M11, 2).ToString();
x_mat_m12.Text = Math.Round(matrice_echelle.M12, 2).ToString();
x_mat_m13.Text = Math.Round(matrice_echelle.M13, 2).ToString();
x_mat_m14.Text = Math.Round(matrice_echelle.M14, 2).ToString();
x_mat_m21.Text = Math.Round(matrice_echelle.M21, 2).ToString();
x_mat_m22.Text = Math.Round(matrice_echelle.M22, 2).ToString();
x_mat_m23.Text = Math.Round(matrice_echelle.M23, 2).ToString();
x_mat_m24.Text = Math.Round(matrice_echelle.M24, 2).ToString();
x_mat_m31.Text = Math.Round(matrice_echelle.M31, 2).ToString();
x_mat_m32.Text = Math.Round(matrice_echelle.M32, 2).ToString();
x_mat_m33.Text = Math.Round(matrice_echelle.M33, 2).ToString();
x_mat_m34.Text = Math.Round(matrice_echelle.M34, 2).ToString();
x_mat_m41.Text =
Math.Round(matrice_echelle.OffsetX, 2).ToString();
x_mat_m42.Text =
Math.Round(matrice_echelle.OffsetY, 2).ToString();
x_mat_m43.Text =
Math.Round(matrice_echelle.OffsetZ, 2).ToString();
x_mat_m44.Text = Math.Round(matrice_echelle.M44, 2).ToString();
}
}
Les contrôles RadioButton permettent de sélectionner un type de caméra lorsqu’ils
sont cochés (soit une caméra de type perspective, soit une caméra de type parallèle).
//
private void FixerLaMatriceDeLaCamera() {
if (x_radbtn_perspective.IsChecked == true) {
x_camera_matrice.ViewMatrix = FixerMatriceVue(
m_pos_camera, m_look_direction, m_up_direction);
x_camera_matrice.ProjectionMatrix = FixerParametrePerspective(
m_angle_vue, m_aspect_ratio, m_plan_pres, m_plan_loin);
}
if (x_radbtn_parallele.IsChecked == true) {
x_camera_matrice.ViewMatrix = FixerMatriceVue(
m_pos_camera, m_look_direction, m_up_direction);
x_camera_matrice.ProjectionMatrix = FixerParametreParallele(
m_largeur, m_hauteur, m_plan_pres, m_plan_loin);
}
}
96 Programmez des jeux vidéo 3D avec C#5 et WPF

3 - La transformation TranslateTransform3D

La classe TranslateTransform3D représente un déplacement d’objet (une translation)


d’une certaine distance selon les axes X, Y et Z. Elle expose principalement les propriétés
suivantes:
• OffsetX qui définit le décalage selon l’axe X.
• OffsetY qui définit le décalage selon l’axe Y.
• OffsetZ qui définit le décalage selon l’axe Z.
• Value qui obtient une représentation Matrix3D de cette transformation.
• IsAffine qui obtient une valeur qui indique si la transformation est affine ou pas.
• Inverse qui obtient la transformation inverse de cet objet si elle existe.
Le constructeur par défaut de cette classe est TranslateTransform3D(). Plusieurs
constructeurs avec paramètres sont disponibles avec notamment:
• TranslateTransform3D(Vector3D) qui initialise une nouvelle instance de la classe
TranslateTransform3D, à l’aide de l’offset Vector3D spécifié.
• TranslateTransform3D(Double, Double, Double) qui initialise une nouvelle instance
de la classe TranslateTransform3D à l’aide de l’offset spécifié.
L’UserControl TranslateTransform3d.xaml (figures 3.4 et 3.5), dans le dossier chapitre_03,
permet de visualiser un cube pour lequel on peut faire varier le déplacement suivant
X, suivant Y et suivant Z. Les deux modes de projection pour la visualisation du cube
en 3D sont accessibles par des RadioButton à cocher (projection perspective et
projection parallèle). Un conteneur de type Grid affiche les composantes de la matrice
de transformation TranslateTransform3D en cours.
On ajoute à la définition du cube coloré en XAML, un groupe, de type Transform3DGroup,
contenant les transformations 3D à effectuer. Une transformation de translation x_
cube_trans de type TranslateTransform3D est ajoutée, en lui fixant les propriétés à
OffsetX=0, OffsetY=0 et OffsetZ=0.
<Transform3DGroup>
<TranslateTransform3D
Copyright 2013 Patrice REY

x:Name=’x_cube_trans’
OffsetX=’0’ OffsetY=’0’
OffsetZ=’0’>
</TranslateTransform3D>
</Transform3DGroup>
Lors du déplacement d’une glissière de type Slider, on appelle la méthode
TransformerCube. Elle permet de modifier les propriétés OffsetX, OffsetY et OffsetZ de
la transformation en fonction des valeurs relevées sur les glissières.
CHAPITRE 3 Les transformations 3D 97
FIGURE 3.4

Translation (avec une


projection perspective)
OffsetX = 0.36
OffsetZ= 2.14
OffsetY = -0.44
98 Programmez des jeux vidéo 3D avec C#5 et WPF
FIGURE 3.5
OffsetY = 0.44

Translation (avec une


projection parallèle)
OffsetX = 1.77
OffsetZ= 3.23

Copyright 2013 Patrice REY


CHAPITRE 3 Les transformations 3D 99
//appliquer une transformation de translation
private void TransformerCube() {
if (x_slider_trans_x != null && x_slider_trans_y != null
&& x_slider_trans_z != null) {
x_cube_trans.OffsetX = x_slider_trans_x.Value;
x_cube_trans.OffsetY = x_slider_trans_y.Value;
x_cube_trans.OffsetZ = x_slider_trans_z.Value;
AfficherMatriceEchelle();
}
}

4 - La transformation RotateTransform3D

La classe RotateTransform3D représente une transformation affine qui spécifie une


transformation de rotation dans l’espace 3D. Elle expose principalement les propriétés
suivantes:
• CenterX qui définit la coordonnée x du Point3D autour duquel pivoter.
• CenterY qui définit la coordonnée y du Point3D autour duquel pivoter.
• CenterZ qui définit la coordonnée z du Point3D autour duquel pivoter.
• Rotation qui définit un objet Rotation3D qui spécifie la rotation.
• Value qui récupère un objet Matrix3D qui représente la rotation.
• IsAffine qui obtient une valeur qui indique si la transformation est affine ou pas.
• Inverse qui obtient la transformation inverse de cet objet si elle existe.
Le constructeur par défaut de cette classe est RotateTransform3D(). Plusieurs
constructeurs avec paramètres sont disponibles avec notamment:
• RotateTransform3D(Rotation3D) qui initialise une nouvelle instance de la classe
RotateTransform3D avec la rotation spécifiée.
• RotateTransform3D(Rotation3D, Point3D) qui initialise une nouvelle instance de la
classe RotateTransform3D avec la rotation et le centre spécifiés.
• RotateTransform3D(Rotation3D, Double, Double, Double) qui initialise une nouvelle
instance de la classe RotateTransform3D avec la rotation et les coordonnées du
centre spécifiées.
L’UserControl RotateTransform3d.xaml (figures 3.6, 3.7 et 3.8), dans le dossier
chapitre_03, permet de visualiser un cube pour lequel on peut le faire pivoter d’un
angle autour des axes X, Y et Z. Les deux modes de projection pour la visualisation du
cube en 3D sont accessibles par des RadioButton à cocher (projection perspective et
projection parallèle). Un conteneur de type Grid affiche les composantes de la matrice
de transformation RotateTransform3D en cours.
On ajoute à la définition du cube coloré en XAML, un groupe, de type Transform3DGroup,
100 Programmez des jeux vidéo 3D avec C#5 et WPF
FIGURE 3.6

Rotation (avec une angle autour


projection perspective) de X = 30.48

Copyright 2013 Patrice REY


CHAPITRE 3 Les transformations 3D 101
FIGURE 3.7

angle autour de Y = -103.06


Rotation (avec une
projection perspective)
102 Programmez des jeux vidéo 3D avec C#5 et WPF
FIGURE 3.8

Rotation (avec une


projection perspective)

angle autour de Z = 55.16

Copyright 2013 Patrice REY


CHAPITRE 3 Les transformations 3D 103

contenant les transformations 3D à effectuer. Une transformation de rotation x_cube_


rotate de type RotateTransform3D est ajoutée, en lui fixant les propriétés à CenterX=0,
CenterY=0 et CenterZ=0.
<Transform3DGroup>
<RotateTransform3D
x:Name=’x_cube_rotate’
CenterX=’0’ CenterY=’0’
CenterZ=’0’>
</RotateTransform3D>
</Transform3DGroup>
Les glissières font varier l’angle de rotation autour d’un axe dans une plage de valeurs
allant de -180 degrés à +180 degrés. La méthode FaireTournerCube permet de faire
tourner le cube autour d’un axe selon un angle de rotation. Elle prend comme
paramètres un vecteur normalisé pour indiquer l’axe choisi et une valeur d’angle. Pour
indiquer l’axe X, on choisira un Vector3D(1,0,0). Pour indiquer l’axe Y, on choisira un
Vector3D(0,1,0). Pour indiquer l’axe Z, on choisira un Vector3D(0,0,1).
//valeur qui change sur un slider
private void x_slider_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e) {
Slider recup = (Slider)sender;
switch (recup.Name) {
case «x_slider_x»:
x_text_slider_x.Text = «angle X = « +
Math.Round(x_slider_x.Value, 2).ToString();
FaireTournerCube(new Vector3D(1, 0, 0), x_slider_x.Value);
break;
case «x_slider_y»:
x_text_slider_y.Text = «angle Y = « +
Math.Round(x_slider_y.Value, 2).ToString();
FaireTournerCube(new Vector3D(0, 1, 0), x_slider_y.Value);
break;
case «x_slider_z»:
x_text_slider_z.Text = «angle Z = « +
Math.Round(x_slider_z.Value, 2).ToString();
FaireTournerCube(new Vector3D(0, 0, 1), x_slider_z.Value);
break;
}
AfficherMatriceEchelle();
}
Pour effectuer la rotation autour d’un axe, on instancie un objet AxisAngleRotation3D
en lui passant un vecteur représentant l’axe et une valeur représentant l’angle. Puis on
affecte cet objet à la propriété Rotation de la transformation RotateTransform3D.
104 Programmez des jeux vidéo 3D avec C#5 et WPF
//faire tourner d’un angle
private void FaireTournerCube(Vector3D axe, double angle) {
x_cube_rotate.Rotation = new AxisAngleRotation3D(axe, angle);
}
Pour exprimer en XAML une rotation d’un angle de 90 degrés autour de l’axe Y, on
écrira:
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D Axis= «0 1 0» Angle= «90»/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
On peut aussi exprimer en XAML la rotation par l’intermédiaire d’un objet
QuaternionRotation3D. Pour cela on écrira:
<RotateTransform3D>
<RotateTransform3D.Rotation>
<QuaternionRotation3D Quaternion= «0 0.707 0 0.707»/>
</RotateTransform3D.Rotation>
</RotateTransform3D>

5 - La transformation MatrixTransform3D

La classe MatrixTransform3D crée une transformation spécifiée par une structure


Matrix3D, utilisée pour manipuler des objets ou des systèmes de coordonnées dans
l’espace universel 3D. Elle expose principalement les propriétés:
• Matrix qui définit un Matrix3D qui spécifie une transformation 3D.
• Value qui obtient une représentation de la matrice de la transformation 3D.
• Inverse qui obtient la transformation inverse de cet objet, si elle existe.
• IsAffine qui obtient une valeur qui indique si la transformation est affine ou pas.
Le constructeur par défaut est MatrixTransform3D(). Le constructeur avec paramètres
est MatrixTransform3D(Matrix3D) qui initialise une nouvelle instance de la classe
MatrixTransform3D à l’aide du Matrix3D spécifié.
L’UserControl MatrixTransform3d.xaml (figures 3.9 et 3.10), dans le dossier chapitre_03,
Copyright 2013 Patrice REY

permet de visualiser un cube pour lequel on peut modifier les valeurs de la matrice de
transformation par l’intermédiaire des glissières. Il y a 16 glissières qui correspondent
aux 16 composantes de la matrice de transformation. Trois boutons supplémentaires
sont ajoutés avec un réglage prédéfini concernant les inclinaisons. Les deux modes de
projection pour la visualisation du cube en 3D sont accessibles par des RadioButton
à cocher (projection perspective et projection parallèle). Un conteneur de type Grid
affiche les composantes de la matrice de transformation MatrixTransform3D en cours.
CHAPITRE 3 Les transformations 3D 105
FIGURE 3.9

Transformation personnalisée avec une


matrice (avec une projection perspective)
106 Programmez des jeux vidéo 3D avec C#5 et WPF
FIGURE 3.10

Transformation personnalisée avec une


matrice (avec une projection parallèle)

Copyright 2013 Patrice REY


CHAPITRE 3 Les transformations 3D 107

Il est ainsi possible d’effectuer sur le cube toutes les transformations que l’on désire
seulement en agissant sur les glissières.
On ajoute à la définition du cube coloré, en XAML, un groupe, de type Transform3DGroup,
contenant les transformations 3D à effectuer. Une transformation par matrice x_cube_
matrice de type MatrixTransform3D est ajoutée. Pour initialiser la propriété Matrix de
la matrice de transformation, on ajoute un nœud <MatrixTransform3D.Matrix> et on
définit les 16 composantes de l’objet Matrix3D (propriétés M11, M12, M13, M14, M21,
M22, M23, M24, M31, M32, M33, M34, OffsetX, OffsetY, OffsetZ et M44).
<Transform3DGroup>
<MatrixTransform3D
x:Name=’x_cube_matrice’>
<MatrixTransform3D.Matrix>
<Matrix3D M11=’1’ M12=’0’
M13=’0’ M14=’0’ M21=’0’
M22=’1’ M23=’0’ M24=’0’
M31=’0’ M32=’0’ M33=’1’
M34=’0’ OffsetX=’0’
OffsetY=’0’ OffsetZ=’0’
M44=’1’></Matrix3D>
</MatrixTransform3D.Matrix>
</MatrixTransform3D>
</Transform3DGroup>
Dès que l’on modifie la position d’une glissière de type Slider, on affiche la valeur au format
texte au-dessus de la glissière et on appelle la méthode FixerMatriceTransformation.
//changement de valeur des slider
private void x_slider_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e) {
Slider recup = (Slider)sender;
switch (recup.Name) {
case «x_slider_m11»:
x_text_m11.Text = «M11 = « +
Math.Round(x_slider_m11.Value, 2).ToString();
break;
case «x_slider_m12»:
x_text_m12.Text = «M12 = « +
Math.Round(x_slider_m12.Value, 2).ToString();
break;
...
}
FixerMatriceTransformation();
}
La méthode FixerMatriceTransformation permet de modifier la matrice de la
transformation x_cube_matrice. Pour cela, on instancie une nouvelle matrice matrice de
type Matrix3D. On affecte à ses 16 composantes la valeur correspondante en fonction
108 Programmez des jeux vidéo 3D avec C#5 et WPF
de la propriété Value des glissières. Puis on affecte cette matrice à la propriété Matrix
de la matrice de transformation x_cube_matrice.
//definir la matrice de la transformation
private void FixerMatriceTransformation() {
...
Matrix3D matrice = new Matrix3D();
matrice.M11 = x_slider_m11.Value;
matrice.M12 = x_slider_m12.Value;
matrice.M13 = x_slider_m13.Value;
matrice.M14 = x_slider_m14.Value;
matrice.M21 = x_slider_m21.Value;
matrice.M22 = x_slider_m22.Value;
matrice.M23 = x_slider_m23.Value;
matrice.M24 = x_slider_m24.Value;
matrice.M31 = x_slider_m31.Value;
matrice.M32 = x_slider_m32.Value;
matrice.M33 = x_slider_m33.Value;
matrice.M34 = x_slider_m34.Value;
matrice.OffsetX = x_slider_m41.Value;
matrice.OffsetY = x_slider_m42.Value;
matrice.OffsetZ = x_slider_m43.Value;
matrice.M44 = x_slider_m44.Value;
x_cube_matrice.Matrix = matrice;
AfficherMatriceTransform();
...
}
Nous avons vu que WPF ne fournissait pas de transformation de haut niveau pour
exécuter une inclinaison en 3D. Pour cela, il faut agir directement sur la matrice. Il y
a trois boutons x_btn_incliner_x, x_btn_incliner_y et x_btn_incliner_z qui possèdent un
réglage prédéfini pour visualiser une inclinaison.
Par exemple, le bouton x_btn_incliner_x visualise une inclinaison du cube suivant l’axe
X (avec une valeur d’inclinaison égale à 2). Le bouton x_btn_incliner_y visualise une
inclinaison du cube suivant l’axe Y (avec une valeur d’inclinaison égale à -1). Le bouton
x_btn_incliner_z visualise une inclinaison du cube suivant l’axe Y (avec une valeur
d’inclinaison égale à 2).
//reglage predefini : inclinaison suivant x
Copyright 2013 Patrice REY

private void x_btn_incliner_x_Click(object sender,


RoutedEventArgs e) {
RemiseAZero();
x_slider_m21.Value = 2;
FixerMatriceTransformation();
}
//reglage predefini : inclinaison suivant y
private void x_btn_incliner_y_Click(object sender,
RoutedEventArgs e) {
CHAPITRE 3 Les transformations 3D 109

RemiseAZero();
x_slider_m32.Value = -1;
FixerMatriceTransformation();
}
//reglage predefini : inclinaison suivant z
private void x_btn_incliner_z_Click(object sender,
RoutedEventArgs e) {
RemiseAZero();
x_slider_m23.Value = 2;
FixerMatriceTransformation();
}

6 - Combinaison de transformations 3D

Il est possible d’effectuer une combinaison de transformations de haut niveau


simultanément. Pour cela, il suffit d’ajouter dans un objet TransformGroup3D les
transformations à effectuer et avec l’ordre que l’on souhaite. Pour une combinaison
d’une mise à l’échelle x_echelle et d’une rotation x_rotate, on aura:
<Transform3DGroup>
<ScaleTransform3D x:Name=ˈx_echelleˈ>
</ ScaleTransform3D>
<RotateTransform3D
x:Name=ˈx_rotateˈ
CenterX=ˈ0ˈ CenterY=ˈ0ˈ CenterZ=ˈ0ˈ>
</RotateTransform3D>
</Transform3DGroup>
Ensuite, par code procédural, on peut accéder à ces transformations puis modifier
leurs paramètres en fonction du contexte donné.
4
La scène 3D

Au sommaire de ce chapitre
• organisation d’une scène 3D
• visualisation d’une scène 3D avec le contrôle Viewport3D
• les maillages et la géométrie 3D
• les conteneurs recevant des objets 3D
• les différentes sources de lumière
• les différents types de caméra
112 Programmez des jeux vidéo 3D avec C#5 et WPF
La scène 3D est un objet complexe. C’est l’endroit où l’on positionne des éléments en
3D dans le but d’effectuer un rendu en 2D. Ce sont ces éléments qui permettent de
donner un réalisme à la scène 3D.
Dans ce chapitre nous allons voir comment s’organise une scène 3D au travers des
éléments nécessaires à sa composition avec notamment le Viewport3D, les maillages
et la géométrie 3D de base, les conteneurs qui reçoivent les objets 3D, les différentes
sources de lumière, les différentes caméras et leurs paramètres.

1 - Visualisation de la scène

La création d’une scène 3D avec l’objet le plus simple qu’est le triangle, ne peut se faire
qu’au travers d’un ensemble d’étapes qui sont:
• l’ajout d’un contrôle Viewport3D qui restitue le contenu 3D dans les limites de
disposition 2D de l’élément Viewport3D.
• l’ajout des objets 3D qui dérivent généralement des objets ModelVisual3D ou
ModelUIElement3D.
• l’ajout d’une source de lumière qui illumine la scène.
• l’application de textures à l’objet 3D qui déterminent l’apparence de sa surface.
• le positionnement de caméra, qui projette l’objet 3D dans une représentation 2D,
et à partir de laquelle on peut visualiser la scène.
Cette liste d’étapes est juste une liste basique mais incontournable pour réaliser une
scène 3D.
Une scène 3D peut être très complexe en pratique, en contenant de nombreux objets
3D, plusieurs sources d’illumination, de nombreuses caméras pour avoir différents
points de visualisation, ainsi que différents matériaux pour la composition des objets.
Tout cet ensemble concoure au réalisme d’une scène 3D mais oblige le programmeur
à effectuer la même démarche quel que soit l’objet 3D à visualiser.

2 - Le contrôle Viewport3D Copyright 2013 Patrice REY

Le contenu graphique 3D dans WPF est encapsulé dans un élément Viewport3D qui
peut participer à la structure du document à deux dimensions. Le système graphique
traite Viewport3D comme un élément visuel à deux dimensions semblable à bien
d’autres éléments dans WPF. Le Viewport3D fonctionne comme une fenêtre d’affichage
dans une scène tridimensionnelle. Plus précisément, c’est une surface sur laquelle une
scène 3D est projetée.
CHAPITRE 4 La scène 3D 113

La classe Viewport3D expose les propriétés suivantes:


• Camera qui définit un objet caméra qui projette le contenu en 3D de la fenêtre
Viewport3D sur la surface 2D de Viewport3D.
• Children qui est une collection des enfants Visual3D de Viewport3D.
Un Viewport3D est un contrôle (il hérite de FrameworkElement). Il contient donc
(figure 4.1) une collection de Visual3D dans sa propriété Children (propriété implicite
en XAML) c’est-à-dire tous les objets 3D qui composent la scène. Mais un Viewport3D
n’affiche rien sans caméra ni éclairage. Une caméra doit être définie dans la propriété
Camera.
FIGURE 4.1

En pratique, un Viewport3D sera ajouté dans une fenêtre de type Window ou dans
un contrôle de type UserControl. Généralement on l’encadre dans un contrôle Border
de façon à identifier la zone 3D. Ce Viewport3D doit avoir ses propriétés Width et
Height explicitement définies. De plus, sa propriété héritée ClipToBounds sera fixée
à true (la propriété UIElement.ClipToBounds définit une valeur qui indique s’il faut
découper le contenu de cet élément, ou le contenu qui provient d’éléments enfants
de cet élément, pour l’ajuster aux dimensions de l’élément contenant; il s’agit d’une
propriété de dépendance).
La structure XAML sera principalement de la forme suivante:
114 Programmez des jeux vidéo 3D avec C#5 et WPF
<Border BorderBrush=’Black’ BorderThickness=’1’ Width=’400’
Height=’400’ Canvas.Left=’12’ Canvas.Top=’68’
Background=’Black’>
<!-- le controle viewport3d -->
<Viewport3D ClipToBounds=’True’ Width=’400’ Height=’400’>
<!-- les ressources du viewport3d -->
<Viewport3D.Resources>
...
</Viewport3D.Resources>
<!-- les caméras du viewport3d -->
<Viewport3D.Camera>
...
</Viewport3D.Camera>
<!-- le conteneur des objets 3D du viewport3d -->
<ContainerUIElement3D>
...
</ContainerUIElement3D>
</Viewport3D>
</Border>

3 - La création de modèles 3D

La création d’objets 3D peut se faire de deux façons différentes: soit en utilisant la


classe ModelVisual3D, soit en utilisant la classe UIElement3D.

3.1 - La classe ModelVisual3D

La classe ModelVisual3D, issue du framework .NET 3.0, est une classe bivalente (figure
4.2). Elle permet d’afficher un modèle défini par un Model3D (GeometryModel3D ou
Model3DGroup) spécifié dans la propriété Content. Elle peut contenir d’autres objets
Visual3D spécifiés dans la propriété Children.
Les modèles composites peuvent être créés au moyen de la propriété Children, mais
également en définissant un contenu de type Model3DGroup dans la propriété Content
qui permet de regrouper plusieurs Model3D.
L’interactivité sur ModelVisual3D peut être obtenue au moyen du test d’atteinte (hit
Copyright 2013 Patrice REY

testing). Le test d’atteinte est une technique de bas niveau que nous ne détaillerons pas
car elle est avantageusement remplacée par les fonctionnalités de UIElement3D. Voici
un exemple de code représentant un modèle ModelVisual3D composite regroupant
plusieurs GeometryModel3D:
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
CHAPITRE 4 La scène 3D 115

<GeometryModel3D ...></GeometryModel3D>
<GeometryModel3D ...></GeometryModel3D>
<GeometryModel3D ...></GeometryModel3D>
<GeometryModel3D ...></GeometryModel3D>
...
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
FIGURE 4.2

Voici un exemple de code représentant un modèle composite regroupant plusieurs


ModelVisual3D dans la propriété Children d’un ModelVisual3D:
<ModelVisual3D>
<ModelVisual3D>
...
</ModelVisual3D>
<ModelVisual3D>
...
</ModelVisual3D>
<ModelVisual3D>
...
</ModelVisual3D>
...
</ModelVisual3D>
116 Programmez des jeux vidéo 3D avec C#5 et WPF

3.2 - La classe UIElement3D

La classe abstraite UIElement3D est apparue dans le framework .NET 3.5, accompagnée
de deux classe héritées (figure 4.3): ModelUIElement3D et ContainerUIElement3D.
Ces classes offrent une alternative intéressante à ModelVisual3D car elles sont plus
évoluées sur le plan fonctionnel et aussi car chacune est spécialisée:
• UIElement3D hérite de Visual3D et apporte une gestion de haut niveau de
l’interactivité (gestion du focus, support du clavier, du stylet, du toucher et de la
souris); son interface est proche de celle de UIElement.
• ModelUIElement3D permet d’afficher un modèle défini par un Model3D
(GeometryModel3D ou Model3DGroup) spécifié dans la propriété Model.
• ContainerUIElement3D peut contenir d’autres objets Visual3D spécifiés dans la
propriété Children.
FIGURE 4.3

Copyright 2013 Patrice REY

Il existe toujours deux techniques pour créer des modèles composites, mais
ici en exploitant deux classes différentes: assembler des Visual3D au sein d’un
ContainerUIElement3D; assembler des Model3D au sein d’un Model3DGroup assigné
CHAPITRE 4 La scène 3D 117

à la propriété Model d’un ModelUIElement3D.


Voici un exemple de modèle ModelUIElement3D composite regroupant plusieurs
GeometryModel3D:
<ModelUIElement3D>
<ModelUIElement3D.Model>
<Model3DGroup>
<GeometryModel3D></GeometryModel3D>
<GeometryModel3D></GeometryModel3D>
<GeometryModel3D></GeometryModel3D>
...
</Model3DGroup>
</ModelUIElement3D.Model>
</ModelUIElement3D>
Voici un exemple de modèle composite regroupant plusieurs ContainerUIElement3D
dans la propriété Children d’un ContainerUIElement3D:
<ContainerUIElement3D>
<ContainerUIElement3D>
...
</ContainerUIElement3D>
<ContainerUIElement3D>
...
</ContainerUIElement3D>
<ContainerUIElement3D>
...
</ContainerUIElement3D>
...
</ContainerUIElement3D>

3.3 - La classe GeometryModel3D

La classe abstraite Model3D représente une description d’objet 3D. Un objet Model3D
peut être utilisé pour définir le visuel d’un objet Visual3D. Comme le montre l’arbre
d’héritage de la figure 4.4, les classes concrètes qui dérivent de Model3D sont:
• GeometryModel3D qui définit un modèle basé sur une géométrie 3D.
• Model3DGroup qui permet l’utilisation de plusieurs modèles Model3D en tant
qu’unité.
Un objet GeometryModel3D définit un modèle basé sur une géométrie 3D. Il sert à
réaliser un modèle 3D au moyen de ses propriétés:
• Geometry qui représente une géométrie 3D.
• Material qui représente une matière pour la face avant spécifiée.
• BackMaterial qui représente une matière pour la face arrière spécifiée (propriété
facultative).
118 Programmez des jeux vidéo 3D avec C#5 et WPF
FIGURE 4.4

4 - La géométrie Geometry3D

La classe Geometry3D est une classe abstraite qui sert de classe de base pour la
géométrie 3D. Les classes qui dérivent de cette classe de base abstraite définissent des
formes géométriques 3D. La classe d’objets Geometry3D peut être utilisée pour le test
d’atteinte et pour le rendu des données graphiques 3D.
Copyright 2013 Patrice REY

Une géométrie 3D contient les données d’un modèle mais ne sait pas s’afficher
elle-même. Le seul type de géométrie hérité de la classe abstraite Geometry3D est
MeshGeometry3D. La figure 4.5 visualise l’arbre d’héritage de la classe Geometry3D.
La classe MeshGeometry3D définit le maillage d’un modèle 3D au moyen des propriétés
suivantes: la propriété Positions représente une collection des points formant les
sommets (vertex) du maillage (du type Point3D); la propriété TriangleIndices représente
une collection des indices des positions formant les triangles des faces du maillage (du
CHAPITRE 4 La scène 3D 119

type Int32); la propriété Normals représente une collection des vecteurs normaux qui
sont utilisés pour le calcul de l’angle de réflexion des rayons lumineux sur la surface; la
propriété TextureCoordinates représente une collection des points 2D de la texture (le
contenu dans le pinceau) correspondant aux positions des sommets du maillage.
FIGURE 4.5

Pour que la production d’un contenu soit visible, la géométrie doit au moins
définir les propriétés Positions et TriangleIndices. Une valeur par défaut est calculée
automatiquement pour la propriété Normals. L’initialisation de TextureCoordinates
n’est pas nécessaire tant qu’une matière sans texture est utilisée.
La base des maillages est le triangle. Un modèle 3D est un assemblage de facettes
triangulaires. Le triangle est la surface la plus simple qui puisse exister. En effet, trois
points suffisent pour définir un triangle.
Les triangles du modèle sont définis au moyen des points formant les sommets du
maillage. La propriété TriangleIndices contient des séries de trois valeurs correspondant
aux indices des sommets (qui sont indiqués dans la propriété Positions) formant chacune
un triangle. L’ordre dans lequel les indices d’un triangle sont spécifiés détermine le
côté, avant ou arrière, sur lequel la face est représentée par rapport à la position du
120 Programmez des jeux vidéo 3D avec C#5 et WPF
point de vue courant. Pour être face à l’observateur, les indices doivent être spécifiés
dans l’ordre inverse des aiguilles d’une montre.
La figure 4.6 visualise la réalisation d’un modèle MeshGeometry3D constitué d’un
simple triangle. Le triangle est composé des trois points A(0,3,0), B(4,0,0) et C(0,0,3).
On affecte à chaque point un numéro d’indice, avec par exemple 0 pour A, 1 pour B
et 2 pour C. Quand on observe le triangle, on veut que la face de devant soit celle qui
est vue (face qui comporterait une couleur ou une texture par exemple). La démarche
consiste à exprimer les points dans l’ordre des indices en écrivant:
Positions = «0 3 0, 4 0 0, 0 0 3»
(la notation suivante est acceptée aussi: Positions = «0,3,0 4,0,0 0,0,3»)
Puis il faut exprimer l’ordre des indices par:
TriangleIndices = «0 2 1»
(la notation suivante est acceptée aussi: TriangleIndices = «0,2,1»)
L’ordre «0 2 1» est dans le sens antihoraire, donc la face du triangle que nous voyons,
sera la face qui comportera la couleur ou la texture. La face arrière de ce triangle
comportera une couleur ou une texture différente.
Y
FIGURE 4.6

sens
antihoraire

indice 0
A(0,3,0)

indice 1
B(4,0,0)
X
Copyright 2013 Patrice REY

indice 2
C(0,0,3)

Z
CHAPITRE 4 La scène 3D 121

Pour afficher ce triangle, il faut l’intégrer à un Visual3D. En l’occurrence, nous


référençons l’objet MeshGeometry3D dans la propriété Content d’un ModelVisual3D.
Dans la propriété Geometry de GeometryModel3D, on affecte le MeshGeometry3D
(avec les propriétés Positions et TriangleIndices). Dans la propriété Material de
GeometryModel3D, on ajoute un objet DiffuseMaterial dont sa propriété Brush
est fixée à Red (la face avant du triangle sera colorée en rouge). Dans la propriété
BackMaterial de GeometryModel3D, on ajoute un objet DiffuseMaterial dont sa
propriété Brush est fixée à Yellow (la face arrière du triangle sera colorée en jaune).
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions=’0 3 0, 4 0 0, 0 0 3’
TriangleIndices=’0 2 1’></MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial Brush=’Red’></DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial Brush=’Yellow’></DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
La figure 4.7 visualise la réalisation d’un modèle MeshGeometry3D qui représente
un rectangle et qui est constitué par deux simples triangles. Le premier triangle est
composé des trois points A(4,2,0), B(4,0,0) et C(0,0,3). Le deuxième triangle est composé
des trois points C(0,0,3), D(0,2,3) et A(4,2,0). On affecte à chaque point un numéro
d’indice, avec par exemple 0 pour A, 1 pour B, 2 pour C et 3 pour D. Quand on observe
le rectangle, on veut que la face de devant soit celle qui est vue (face qui comporterait
une couleur ou une texture par exemple). La démarche consiste à exprimer les points
dans l’ordre des indices en écrivant:
Positions = «4 2 0, 4 0 0, 0 0 3, 0 2 3»
Puis il faut exprimer l’ordre des indices par:
TriangleIndices = «2 1 0, 0 3 2»
L’ordre «2 1 0» est dans le sens antihoraire, donc la face du triangle que nous voyons,
sera la face qui comportera la couleur ou la texture. La face arrière de ce triangle
comportera une couleur ou une texture différente. Il en est de même pour l’ordre «0
3 2».
Pour afficher ce rectangle, il faut l’intégrer à un Visual3D. En l’occurrence, nous
122 Programmez des jeux vidéo 3D avec C#5 et WPF
référençons l’objet MeshGeometry3D dans la propriété Content d’un ModelVisual3D.
Dans la propriété Geometry de GeometryModel3D, on affecte le MeshGeometry3D
(avec les propriétés Positions et TriangleIndices). Dans la propriété Material de
GeometryModel3D, on ajoute un objet DiffuseMaterial dont sa propriété Brush
est fixée à Red (la face avant du rectangle sera colorée en rouge). Dans la propriété
BackMaterial de GeometryModel3D, on ajoute un objet DiffuseMaterial dont sa
propriété Brush est fixée à Yellow (la face arrière du rectangle sera colorée en jaune).
FIGURE 4.7 Y

sens
antihoraire

indice 0
A(4,2,0)

indice 1
indice 3
D(0,2,3) B(4,0,0)
X

indice 2
C(0,0,3)

<ModelVisual3D>
Copyright 2013 Patrice REY

<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions=’4 2 0, 4 0 0, 0 0 3, 0 2 3’
TriangleIndices=’2 1 0, 0 3 2’></MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial Brush=’Red’></DiffuseMaterial>
CHAPITRE 4 La scène 3D 123

</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial Brush=’Yellow’></DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>

5 - Matière et surface

Une matière est un objet hérité de la classe Material qui définit l’apparence d’une
surface, au moyen d’un pinceau spécifié dans sa propriété Brush (propriété incluant
tous les pinceaux 2D). Les pinceaux évolués contenant une image ou un objet Visual
doivent être appliqués à des modèles gérant le placage de texture. La figure 4.8 visualise
l’arbre d’héritage de la classe de base abstraite Material.
FIGURE 4.8
124 Programmez des jeux vidéo 3D avec C#5 et WPF
Les classes dérivées de la classe Material sont:
• DiffuseMaterial qui représente une surface autorisant l’application d’un pinceau
2D, tel qu’un SolidColorBrush ou un TileBrush, à un modèle 3D éclairé de manière
diffuse.
• EmissiveMaterial qui représente une surface autorisant l’application d’un Brush
à un modèle 3D afin qu’il soit compris dans les calculs de l’éclairage comme si les
surfaces émettaient une lumière égale à la couleur du Brush.
• SpecularMaterial qui représente une surface autorisant l’application d’un pinceau
2D, tel qu’un SolidColorBrush ou un TileBrush, à un modèle 3D éclairé de manière
spéculaire.
• MaterialGroup qui représente un Material qui forme un élément composite de la
collection des matières.

5.1 - La surface mate

Une surface mate est définie par un objet DiffuseMaterial. La caractéristique d’une
surface mate est de diffuser la lumière qu’elle reçoit et dont le rendu est fonction de
l’éclairage. Les parties à l’ombre sont noires.
La classe DiffuseMaterial expose principalement les propriétés suivantes:
• Brush qui représente le pinceau utilisé.
• Color qui définit la couleur réfléchie pour la texture de la matière avec un éclairage
normal.
• AmbientColor qui définit la couleur réfléchie pour la texture de la matière avec un
éclairage de type AmbientLight.
Par exemple, la face d’un rectangle peinte en rouge s’exprimera en XAML par:
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions=’4 2 0, 4 0 0, 0 0 3, 0 2 3’
TriangleIndices=’2 1 0, 0 3 2’></MeshGeometry3D>
</GeometryModel3D.Geometry>
Copyright 2013 Patrice REY

<GeometryModel3D.Material>
<DiffuseMaterial Brush=’Red’></DiffuseMaterial>
</GeometryModel3D.Material>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
CHAPITRE 4 La scène 3D 125

5.2 - La surface lumineuse

Une surface lumineuse est définie par un objet EmissiveMaterial. Cette surface
lumineuse est indépendante de l’éclairage ambiant. Mais un objet avec une surface
lumineuse n’illumine pas les éléments 3D présents dans son entourage immédiat.
La classe EmissiveMaterial expose principalement les propriétés suivantes:
• Brush qui représente le pinceau utilisé.
• Color qui définit la couleur réfléchie pour la texture de la matière.
Par exemple, la face d’un rectangle peinte en jaune s’exprimera en XAML par:
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions=’4 2 0, 4 0 0, 0 0 3, 0 2 3’
TriangleIndices=’2 1 0, 0 3 2’></MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<EmissiveMaterial Brush=’Yellow’></EmissiveMaterial >
</GeometryModel3D.Material>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>

5.3 - Combinaison de matières

Les matières peuvent être combinées au sein d’un objet MaterialGroup, formant
alors une matière composite. La propriété Children de MaterialGroup contient une
collection d’objets Material enfants qui constituent la matière composite. Par exemple,
un MaterialGroup permet de définir une surface unie ornée de motifs lumineux
visibles même dans l’ombre. Un objet DiffuseMaterial définit la surface unie. Un objet
DrawingBrush, utilisé par un objet EmissiveMaterial, définit les motifs lumineux. Cela
s’exprimera en XAML par:
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions=’4 2 0, 4 0 0, 0 0 3, 0 2 3’
TriangleIndices=’2 1 0, 0 3 2’></MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<MaterialGroup>
126 Programmez des jeux vidéo 3D avec C#5 et WPF
<DiffuseMaterial Brush=’Yellow’></DiffuseMaterial >
<EmissiveMaterial>
<EmissiveMaterial.Brush>
<DrawingBrush>
<DrawingBrush.Drawing>
<GeometryDrawing Brush=’MediumBlue’>
...
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
</EmissiveMaterial.Brush>
</EmissiveMaterial>
</MaterialGroup>
</GeometryModel3D.Material>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>

5.4 - La réflexion spéculaire

Un objet SpecularMaterial permet l’application d’un pinceau 2D, tel qu’un


SolidColorBrush ou un TileBrush, à un modèle 3D éclairé de manière spéculaire.
La classe SpecularMaterial gère la réflexion spéculaire en ajoutant de la brillance à
une surface définie avec d’autres matières. Elle s’utilise toujours en association avec
d’autres objets Material au sein d’un MaterialGroup. La brillance est constituée de
reflets en fonction de l’éclairage et de la position du point de vue.
La classe SpecularMaterial expose principalement les propriétés suivantes:
• SpecularPower qui indique l’intensité des reflets.
• Color qui indique la couleur des reflets.
Cela s’exprimera par exemple en XAML par:
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions=’4 2 0, 4 0 0, 0 0 3, 0 2 3’
Copyright 2013 Patrice REY

TriangleIndices=’2 1 0, 0 3 2’></MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<MaterialGroup>
<DiffuseMaterial Brush=’Yellow’></DiffuseMaterial >
<SpecularMaterial SpecularPower=’100.0’ Brush=’White’/>
</MaterialGroup>
</GeometryModel3D.Material>
</GeometryModel3D>
CHAPITRE 4 La scène 3D 127

</ModelVisual3D.Content>
</ModelVisual3D>

6 - L’éclairage de la scène

L’éclairage est géré par des objets dérivés de la classe de base abstraite Light. La figure
4.9 visualise l’arbre d’héritage de la classe Light. WPF gère l’éclairage par un objet
Model3D d’un type particulier, invisible, utilisé pour calculer les couleurs des éléments
de la scène. C’est pour cette raison que l’objet Light hérite de la classe Model3D.
FIGURE 4.9

Les classes dérivées de la clase abstraite Light sont:


• DirectionLight qui représente une lumière qui projette son effet dans une direction
spécifiée par un Vector3D.
• AmbientLight qui représente une lumière qui applique uniformément de la lumière
sur les objets, indépendamment de leur forme.
• PointLightBase qui est une classe de base abstraite qui représente un objet lumière
comportant une position dans l’espace et projetant sa lumière dans toutes les
directions.
La classe PointLightBase possède comme classes dérivées les classes:
• PointLight qui représente une source de lumière qui a une position spécifiée dans
l’espace et qui projette sa lumière dans toutes les directions.
128 Programmez des jeux vidéo 3D avec C#5 et WPF
• SpotLight qui représente une lumière qui projette son effet dans une zone conique
selon une direction spécifiée.
Tous ces objets représentant un éclairage, expose la propriété Color qui définit la couleur
de l’éclairage. L’objet AmbientLight produit un éclairage uniforme, sans ombre, avec
une position indéterminée. L’objet DirectionalLight produit un éclairage similaire aux
rayons solaires. Les rayons sont parallèles comme s’ils provenaient d’une source très
éloignée. Sa position d’éclairage est indéterminée. Sa propriété Direction détermine
l’orientation des rayons lumineux au moyen d’un vecteur. Généralement la puissance
de l’éclairage directionnel est fixe. La seule façon de l’augmenter consiste à positionner
plusieurs objets DirectionLight. Les objets PointLight et SpotLight produisent un
éclairage similaire à une lampe d’intérieur qui rayonne autour d’une position donnée.
Ces deux objets exposent principalement les propriétés suivantes:
• Position qui définit l’emplacement de la source d’éclairage (une position de type
Point3D).
• Range qui représente la portée maximale de la source d’éclairage; la portée a une
valeur infinie par défaut; les zones en dehors de cette portée ne sont pas éclairées
(dans le cas d’une valeur finie pour la portée maximale).
• ConstantAttenuation qui représente une valeur de constante par laquelle l’intensité
de la lumière diminue sur la distance; en agissant comme un facteur de division, la
luminosité de la lampe peut être réduite.
• LinearAttenuation qui représente une valeur qui spécifie la diminution linéaire de
l’intensité de la lumière sur la distance; en agissant comme un facteur multiplicateur
de la distance, la lumière peut être réduite.
• QuadraticAttenuation qui représente une valeur qui spécifie la diminution de l’effet
de la lumière sur la distance, calculée par une opération quadratique; en agissant
comme facteur multiplicateur du carré de la distance, la lumière peut être réduite.
Si l’objet PointLight rayonne dans toutes les directions, l’objet SpotLight, quant à lui,
réduit son éclairage à un cône, en exposant des propriétés supplémentaires qui sont:
• Direction qui définit un Vector3D qui spécifie la direction dans laquelle l’objet
SpotLight projette sa lumière.
Copyright 2013 Patrice REY

• OuterConeAngle qui définit un angle qui spécifie la proportion d’une projection


conique d’un objet SpotLight en dehors de laquelle la lumière n’éclaire pas les
objets dans la scène.
• InnerConeAngle qui définit un angle qui spécifie la proportion d’une projection
conique d’un objet SpotLight dans laquelle la lumière éclaire entièrement les
objets dans la scène.
CHAPITRE 4 La scène 3D 129

7 - Les caméras

La représentation d’une scène 3D dépend de la position du point de vue, et de la


direction du regard. Ces paramètres sont gérés par un objet Camera. La projection
2D d’une scène 3D est définit par la caméra qui spécifie quelle partie de la scène 3D
devant être restituée par l’élément Viewport3DVisual ou Viewport3D.
La classe de base abstraite Camera gère les différents types de caméra. La figure 4.10
visualise son arbre d’héritage. Ses classes dérivées sont:
• ProjectionCamera qui est une classe de base abstraite pour les caméras à projection
perspective et à projection orthographique (ou parallèle).
• MatrixCamera qui définit une caméra qui spécifie les transformations de projection
et d’affichage comme des objets de type Matrix3D.
FIGURE 4.10
130 Programmez des jeux vidéo 3D avec C#5 et WPF
La classe abstraite ProjectionCamera possède deux classes dérivées qui sont:
• PerspectiveCamera qui représente une caméra avec une projection de type
perspective.
• OrthographicCamera qui représente une caméra avec une projection de type
orthographique (ou parallèle).
Un objet PerspectiveCamera définit une projection avec de la perspective c’est-à-
dire contenant un point vers lequel convergent toutes les lignes de fuite. Un objet
OrthographicCamera définit une projection orthographique sans perspective. Les
dimensions de la représentation d’un élément sont fixes et indépendantes de sa
position.
Les classes PerspectiveCamera et OrthographicCamera exposent principalement les
propriétés suivantes:
• Position qui définit la position de la caméra en coordonnées universelles.
• LookDirection qui définit un vecteur Vector3D qui représente la direction de visée
de la caméra en coordonnées universelles.
• UpDirection qui définit un vecteur Vector3D qui indique l’inclinaison de la caméra;
par défaut, sa valeur est (0,1,0) ce qui signifie que la verticale de l’image correspond
à l’axe Y, valeurs positives vers le haut.
• NearPlaneDistance et FarPlaneDistance qui indiquent les limites de la zone de rendu
des éléments visuels respectivement à l’avant-plan et à l’arrière-plan par rapport à
la caméra; les parties des éléments au-delà des limites sont invisibles; les valeurs
par défaut sont respectivement 0.125 et l’infini.
De plus, l’objet PerspectiveCamera expose la propriété FielOfView qui détermine le
champ de vision horizontal de la caméra. Une valeur élevée correspond à une vue de
type grand-angle, et une valeur faible correspond à une vue de type téléobjectif. La
figure 4.11 visualise les principales propriétés de la caméra perspective.
Quant à l’objet OrthographicCamera, il expose la propriété Width qui représente la
largeur de la zone d’affichage. La figure 4.12 visualise les principales propriétés de la
caméra orthographique. Copyright 2013 Patrice REY
CHAPITRE 4 La scène 3D 131
FIGURE 4.11
FarPlaneDistance

NearPlaneDistance

UpDirection

Y
LookDirection
Z

X FieldOfView
centre de
projection
(caméra)
plan de
projection volume
d’observation
plan de loin
FIGURE 4.12
FarPlaneDistance
NearPlaneDistance

plan de
projection
UpDirection Width

Y
LookDirection
Z

X
centre de
projection à
l’infini (caméra)
plan de volume
près d’observation plan de
loin
5
Les modèles 3D basiques

Au sommaire de ce chapitre
• la modélisation de la facette triangulaire et de la facette rectangulaire avec
des faces colorées et texturées
• la modélisation du cube, du plan de travail et des axes de coordonnées
• la définition et l’application des coordonnées sphériques
• la modélisation du cylindre et la géométrie polygonale
• la modélisation du cône
• la modélisation de la sphère et de l’ellipsoïde
• la modélisation du tore
134 Programmez des jeux vidéo 3D avec C#5 et WPF
Les modèles 3D sont composés par des assemblages dont l’assemblage de base est
le triangle. Deux triangles qui sont joints par leur hypoténuse forment une facette
rectangulaire. Nous allons voir dans ce chapitre comment réaliser des assemblages
basiques et comment les visualiser. A partir de ces assemblages, nous verrons la
réalisation des principaux modèles 3D basiques que sont le cube, la sphère, le cylindre,
le cône et le tore.

1 - La modélisation basique

La base des maillages est le triangle. Un modèle 3D est un assemblage de facettes


triangulaires. Le triangle est la surface la plus simple qui puisse exister. En effet, trois
points suffisent pour définir un triangle.

1.1 - La facette triangulaire

L’UserControl FacetteTriangulaire.xaml, dans le dossier chapitre_05, visualise un


ensemble de quatre triangles rectangles dont la face avant est soit colorée soit texturée
et dont la face arrière est colorée en jaune (figure 5.1). Une caméra est positionnée
dans l’espace 3D, elle vise l’origine du repère 3D. Une glissière permet de faire tourner
la caméra autour de l’axe des Y.
Nous allons employer la démarche qui a été vue au chapitre 4 pour la visualisation.
On commence par ajouter un contrôle de rendu 3D de type Viewport3D avec ses
propriétés With et Height fixées explicitement (791 et 422 pixels respectivement
dans notre exemple). La propriété ClipToBounds est fixée à true pour éviter tous les
débordements de la zone de rendu. La couleur du fond de la zone de rendu est celle du
contrôle Border (propriété Background fixée à White).
<!-- 3d -->
<Border BorderBrush=ˈBlackˈ BorderThickness=ˈ1ˈ Width=ˈ791ˈ Height=ˈ422ˈ
Canvas.Left=ˈ11ˈ Canvas.Top=ˈ99ˈ Background=ˈWhiteˈ>
<!-- zone de rendu 3d -->
<Viewport3D x:Name=ˈx_viewportˈ ClipToBounds=ˈTrueˈ Width=ˈ791ˈ Height=ˈ422ˈ>
Copyright 2013 Patrice REY

...
</Viewport3D>
</Border>
Pour que la zone de rendu affiche quelque chose, il faut ajouter une caméra (propriété
Camera du Viewport3D). Pour cela on ajoute une caméra x_camera à projection
perspective, de type PerspectiveCamera. On fixe sa position dans l’espace 3D (Position
= «0,0,5»), son axe de visée est représenté par un vecteur (LookDirection = «0,0,-5»), sa
CHAPITRE 5 Les modèles 3D basiques 135
FIGURE 5.1

direction vers le haut est représentée par un vecteur (UpDirection = «0 1 0») et l’angle
de vue est fixé à 60 (FieldOfView = «60»). Pour que la caméra fixe en permanence
l’origine du repère 3D, il suffit de fixer l’orientation de la visée par le vecteur qui va du
point (0,0,5) qui correspond à la position de la caméra, au point (0,0,0) qui est l’origine.
Cela se traduit donc par un Vector3D(0,0,-5) pour une position de caméra de (0,0,5) soit
le signe opposé aux trois coordonnées de la caméra.
Le UpDirection de la caméra est celui de l’axe Y pour signifier aucune inclinaison de la
caméra. Son inclinaison est donc représentée par le Vector3D(0,1,0). Le plan de près de
projection (propriété NearPlaneDistance) est fixé à sa valeur par défaut (0.125). Le plan
de loin (propriété FarPlaneDistance) est fixé à une valeur de 100.
<!-- zone de rendu 3d -->
<Viewport3D x:Name=ˈx_viewportˈ ClipToBounds=ˈTrueˈ Width=ˈ791ˈ Height=ˈ422ˈ>
<!-- cameras -->
<Viewport3D.Camera>
<PerspectiveCamera x:Name=ˈx_cameraˈ Position=ˈ0,0,5ˈ LookDirection=ˈ0,0,-5ˈ
FieldOfView=ˈ60ˈ UpDirection=ˈ0 1 0ˈ NearPlaneDistance=ˈ0.125ˈ
136 Programmez des jeux vidéo 3D avec C#5 et WPF
FarPlaneDistance=ˈ100ˈ></PerspectiveCamera>
</Viewport3D.Camera>
</Viewport3D>
Les objets 3D sont positionnés dans un objet ContainerUIElement3D qui est affecté
à la propriété Children de Viewport3D (propriété implicite en XAML donc elle n’a
pas besoin d’être mentionnée avec <Viewport3D.Children>). On choisit un objet
ModelUIElement3D pour restituer un modèle 3D qui prend en charge l’entrée, le focus
et les événements.
<!-- zone de rendu 3d -->
<Viewport3D x:Name=ˈx_viewportˈ ClipToBounds=ˈTrueˈ Width=ˈ791ˈ Height=ˈ422ˈ>
...
<!-- conteneur des modeles **************** -->
<ContainerUIElement3D>
<ModelUIElement3D>
...
</ModelUIElement3D>
<ModelUIElement3D>
...
</ModelUIElement3D>
...
</ContainerUIElement3D>
...
</Viewport3D>
La propriété implicite Model de ModelUIElement3D (<ModelUIElement3D.Model>) est
constituée des objets x_groupe_triangle_hd, x_groupe_triangle_hg, x_groupe_triangle_bd
et x_groupe_triangle_bg, de type Model3DGroup. Ces objets représentent les différents
maillages qui sont apportés à la scène 3D.
<!-- zone de rendu 3d -->
<Viewport3D x:Name=ˈx_viewportˈ ClipToBounds=ˈTrueˈ Width=ˈ791ˈ Height=ˈ422ˈ>
...
<!-- conteneur des modeles **************** -->
<ContainerUIElement3D>
<!-- triangle haut droit -->
<ModelUIElement3D x:Name=ˈx_modele_triangle_hdˈ>
<Model3DGroup x:Name=ˈx_groupe_triangle_hdˈ>
...
Copyright 2013 Patrice REY

</Model3DGroup>
</ModelUIElement3D>
<!-- triangle haut gauche -->
<ModelUIElement3D x:Name=ˈx_modele_triangle_hgˈ>
<Model3DGroup x:Name=ˈx_groupe_triangle_hgˈ>
...
</Model3DGroup>
</ModelUIElement3D>
<!-- triangle bas droit -->
CHAPITRE 5 Les modèles 3D basiques 137

<ModelUIElement3D x:Name=ˈx_modele_triangle_bdˈ>
<Model3DGroup x:Name=ˈx_groupe_triangle_bdˈ>
...
</Model3DGroup>
</ModelUIElement3D>
<!-- triangle bas gauche -->
<ModelUIElement3D x:Name=ˈx_modele_triangle_bgˈ>
<Model3DGroup x:Name=ˈx_groupe_triangle_bgˈ>
...
</Model3DGroup>
</ModelUIElement3D>
...
</ContainerUIElement3D>
...
</Viewport3D>
Un éclairage x_lumiere, de type AmbientLight, permet d’éclairer la scène. On fixe la
propriété Color de AmbientLight à White pour un éclairage blanc.
<!-- zone de rendu 3d -->
<Viewport3D x:Name=ˈx_viewportˈ ClipToBounds=ˈTrueˈ Width=ˈ791ˈ Height=ˈ422ˈ>
...
<ContainerUIElement3D>
<!-- lumiere -->
<ModelUIElement3D>
<Model3DGroup x:Name=ˈx_lumiereˈ>
<AmbientLight Color=ˈWhiteˈ></AmbientLight>
</Model3DGroup>
</ModelUIElement3D>
...
</ContainerUIElement3D>
...
</Viewport3D>
Les quatre triangles sont positionnés à l’origine lors de leur construction. Puis,
avec des transformations 3D, ils sont translatés (avec une transformation de type
TranslateTransform3D) et éventuellement, ils subissent une rotation (avec une
transformation de type RotateTransform3D). La figure 5.2 visualise la réalisation du
triangle avec 3 points et 3 indices lors de sa construction, et la figure 5.3 visualise la
position de la caméra vis-à-vis du triangle.
Le triangle en haut à droite est le premier triangle positionné (x_groupe_triangle_hd
de type Model3DGroup). Un objet GeometryModel3D, intitulé x_geo_triangle_hd,
constitue le modèle 3D du triangle. La propriété Geometry de GeometryModel3D
(<GeometryModel3D.Geometry>) contient la définition du maillage du triangle. Ce
maillage est un MeshGeometry3D avec sa propriété Positions fixée à «0 0 0, 1 0 0, 0 1
0», et sa propriété TriangleIndices fixée à «0 1 2».
138 Programmez des jeux vidéo 3D avec C#5 et WPF
FIGURE 5.2
Y

(0,2,0) indice 2

face de devant face arrière


gris clair gris foncé
(2,0,0) indice 1
(0,0,0) indice 0 X

angle 30°

(0,0,5) caméra
Z
Y
FIGURE 5.3

FarPlaneDistance
= infini par défaut
Copyright 2013 Patrice REY

X
LookDirection
(0,0,-5)

UpDirection angle 30°


(0,1,0)
NearPlaneDistance
= 0.125 par défaut

(0,0,5) caméra
Z
CHAPITRE 5 Les modèles 3D basiques 139

<!-- triangle haut droit -->


<ModelUIElement3D x:Name=ˈx_modele_triangle_hdˈ>
<Model3DGroup x:Name=ˈx_groupe_triangle_hdˈ>
<GeometryModel3D x:Name=ˈx_geo_triangle_hdˈ>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions=ˈ0 0 0, 1 0 0, 0 1 0ˈ TriangleIndices=ˈ0 1 2ˈ
TextureCoordinates=ˈ0,1 1,1 0,0ˈ></MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
...
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
...
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
<Model3DGroup.Transform>
<Transform3DGroup>
<TranslateTransform3D OffsetX=ˈ0.25ˈ OffsetY=ˈ0.25ˈ>
</TranslateTransform3D>
</Transform3DGroup>
</Model3DGroup.Transform>
</Model3DGroup>
</ModelUIElement3D>
On utilise le même maillage pour les triangles x_groupe_triangle_hg, x_groupe_triangle_
bd et x_groupe_triangle_bg. Ensuite, les triangles sont positionnés par l’intermédiaire
de transformations 3D, en ajoutant un groupe de transformations, de type
Transform3DGroup, à sa propriété Transform (<Model3DGroup.Transform>).
On aura la translation (de type TranslateTransform3D avec ses propriétés OffsetX
et OffsetY) et la rotation (de type RotateTransform3D avec sa propriété Rotation qui
reçoit un objet AxisAngleRotation3D pour une rotation autour d’un axe, sa propriété
Axis définit l’axe et sa propriété Angle définit l’angle de rotation). Ce qui donne en
XAML par exemple:
<Model3DGroup.Transform>
<Transform3DGroup>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D Axis=ˈ0,0,1ˈ Angle=ˈ180ˈ />
</RotateTransform3D.Rotation>
</RotateTransform3D>
<TranslateTransform3D OffsetX=ˈ-0.25ˈ OffsetY=ˈ-0.25ˈ></TranslateTransform3D>
</Transform3DGroup>
</Model3DGroup.Transform>
La propriété Material de GeometryModel3D (<GeometryModel3D.Material>)
contient la définition de la surface avant du triangle. La propriété BackMaterial de
140 Programmez des jeux vidéo 3D avec C#5 et WPF
GeometryModel3D (<GeometryModel3D.BackMaterial>) contient la définition de la
surface arrière du triangle.
Pour affecter une surface mate à la propriété Material et BackMaterial, on ajoute un
objet DiffuseMaterial. Si la face est de couleur unie, on affecte à la propriété Brush un
pinceau de couleur.
<GeometryModel3D x:Name=ˈx_geo_triangle_bgˈ>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions=ˈ0 0 0, 1 0 0, 0 1 0ˈ TriangleIndices=ˈ0 1 2ˈ>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial x:Name=ˈx_surface_triangle_bgˈ Brush=ˈPeruˈ></DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial Brush=ˈYellowˈ></DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
Pour peindre avec un pinceau en dégradé de couleurs, il faut ajouter un objet
LinearGradientBrush à la propriété Brush de l’objet DiffuseMaterial. Ne pas oublier
d’initialiser la propriété TextureCoordinates du MeshGeometry3D. Cela donnerait par
exemple ici:
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions=ˈ0 0 0, 1 0 0, 0 1 0ˈ TriangleIndices=ˈ0 1 2ˈ
TextureCoordinates=ˈ0,0 1,0 1,1ˈ></MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial x:Name=ˈx_surface_triangle_hgˈ>
<DiffuseMaterial.Brush>
<LinearGradientBrush x:Name=ˈmyGradientBrushˈ StartPoint=ˈ0,0.5ˈ
EndPoint=ˈ1,0.5ˈ>
<LinearGradientBrush.GradientStops>
<GradientStop Color=ˈYellowˈ Offset=ˈ0ˈ />
<GradientStop Color=ˈRedˈ Offset=ˈ0.25ˈ />
<GradientStop Color=ˈBlueˈ Offset=ˈ0.75ˈ />
<GradientStop Color=ˈLimeGreenˈ Offset=ˈ1ˈ />
</LinearGradientBrush.GradientStops>
Copyright 2013 Patrice REY

</LinearGradientBrush>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
Il en est de même si le pinceau peint, avec non pas un dégradé de couleurs, mais avec
une image de référence de type ImageBrush (la propriété ImageSource pointe vers la
ressource image servant pour la peinture à effectuer). Cela donnerait par exemple ici:
CHAPITRE 5 Les modèles 3D basiques 141

<GeometryModel3D x:Name=ˈx_geo_triangle_hdˈ>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions=ˈ0 0 0, 1 0 0, 0 1 0ˈ TriangleIndices=ˈ0 1 2ˈ
TextureCoordinates=ˈ0,1 1,1 0,0ˈ></MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial x:Name=ˈx_surface_triangle_hdˈ>
<DiffuseMaterial.Brush>
<ImageBrush ImageSource=ˈimage_texture/texture_triangle_1.pngˈ />
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
La propriété TextureCoordinates du MeshGeometry3D permet de mapper un placage
de texture sur une surface. Ce placage de texture ne peut être réalisé que si la propriété
TextureCoordinates est explicitement exprimée. La propriété TextureCoordinates de la
géométrie MeshGeometry3D permet d’établir la correspondance entre les sommets
du maillage et les points 2D du contenu d’un pinceau évolué (tel que VisualBrush
ou ImageBrush). Pour exécuter ce placage, il faut tenir compte du fait que l’axe Y en
2D est inversé par rapport à l’axe Y en 3D. Les coordonnées 2D sont exprimées en
relatif par une valeur entre 0 et 1. La figure 5.4 visualise les coordonnées de la texture
à appliquer, ce qui donne une propriété TextureCoordinates = «0 1, 1 1, 0 0» pour
appliquer un ImageBrush dont la propriété ImageSource pointe sur la ressource image
«image_texture/texture_triangle_1.png».
FIGURE 5.4

0,0 1,0

0,1
1,1
142 Programmez des jeux vidéo 3D avec C#5 et WPF
La glissière x_slider_camera_axe_y, de type Slider, permet de faire tourner la caméra
autour de l’axe Y par rotation d’angle de pas de 1 unité (figures 5.5 et 5.6). Cela s’effectue
en affectant à la propriété Transform de la caméra une rotation RotateTransform3D qui
reçoit en paramètre une rotation d’axe AxisAngleRotation3D.
FIGURE 5.5
Y

Copyright 2013 Patrice REY


CHAPITRE 5 Les modèles 3D basiques 143
FIGURE 5.6
Y

//glissiere pour faire tourner la caméra


private void x_slider_camera_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e) {
v_angle_rotation_h = x_slider_camera_axe_y.Value;
144 Programmez des jeux vidéo 3D avec C#5 et WPF
x_camera.Transform = new RotateTransform3D(new AxisAngleRotation3D(
new Vector3D(0, 1, 0), v_angle_rotation_h));
}

1.2 - La facette rectangulaire

L’UserControl FacetteRectangulaire.xaml, dans le dossier chapitre_05, visualise un


ensemble de quatre rectangles dont la face avant est soit colorée soit texturée et
dont la face arrière est colorée en jaune ou bien texturée (figure 5.7). Une caméra est
positionnée dans l’espace 3D, elle vise l’origine du repère 3D. Une glissière permet
de faire tourner la caméra autour de l’axe des Y. L’éclairage est réalisé par un spot qui
éclaire le devant, et une lumière directionnelle qui éclaire l’arrière.
FIGURE 5.7

Copyright 2013 Patrice REY


CHAPITRE 5 Les modèles 3D basiques 145

Comme le montre la figure 5.8, le maillage d’un rectangle est réalisé à partir de deux
triangles. Le premier triangle est composé des points (0,1,0), (0,0,0) et (1,0,0) avec les
indices respectifs 0, 1 et 2. Le deuxième triangle est composé des points (1,0,0), (1,1,0)
et (0,1,0) avec les indices respectifs 2, 3 et 0. Le maillage, de type MeshGeometry3D, a
la propriété Positions = «0 1 0, 0 0 0, 1 0 0, 1 1 0».
FIGURE 5.8 (0,1,0) (1,1,0)
indice 0 indice 3

triangle triangle
(0,1,0) indice 0 (1,0,0) indice 2
(0,0,0) indice 1 (1,1,0) indice 3
(1,0,0) indice 2 (0,1,0) indice 0

(0,0,0) (1,0,0)
indice 1 indice 2
Pour appliquer un placage de texture sur la face avant, il faut définir la propriété
Material de GeometryModel3D en lui affectant un objet DiffuseMaterial par exemple.
Puis il faut appliquer à la propriété Brush de DiffuseMaterial, un objet ImageBrush qui
correspond à la texture à appliquer. Cet ImageBrush a sa propriété ImageSource qui
référence une image (ici elle référence l’image image_texture/texture_triangle_1.png).
Le procédé est le même pour appliquer un placage de texture sur la face arrière en
définissant la propriété BackMaterial de GeometryModel3D.
Ce placage de texture ne peut être réalisé que si la propriété TextureCoordinates
est explicitement exprimée. La propriété TextureCoordinates de la géométrie
MeshGeometry3D permet d’établir la correspondance entre les sommets du maillage
et les points 2D du contenu d’un pinceau évolué (tel que VisualBrush ou ImageBrush).
Pour exécuter ce placage, il faut tenir compte du fait que l’axe Y en 2D est inversé par
rapport à l’axe Y en 3D. Les coordonnées 2D sont exprimées en relatif par une valeur
entre 0 et 1. La figure 5.9 visualise les coordonnées de la texture à appliquer, ce qui
donne une propriété TextureCoordinates = «0 0, 0 1, 1 1, 1 0».
On aura par exemple, pour le rectangle en haut et à droite, qui possède une propriété
Material recevant un placage de texture sous forme d’une ressource image et une
146 Programmez des jeux vidéo 3D avec C#5 et WPF
propriété BackMaterial recevant une couleur jaune, le code suivant:
<!-- rectangle haut droit -->
<ModelUIElement3D x:Name=ˈx_modele_hdˈ>
<Model3DGroup x:Name=ˈx_groupe_hdˈ>
<GeometryModel3D x:Name=ˈx_geo_hdˈ>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions=ˈ0 1 0, 0 0 0, 1 0 0, 1 1 0ˈ
TriangleIndices=ˈ0 1 2, 2 3 0ˈ
TextureCoordinates=ˈ0,0 0,1 1,1 1,0ˈ></MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<ImageBrush ImageSource=ˈimage_texture/texture_triangle_1.pngˈ />
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial Brush=ˈYellowˈ></DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
...
</ModelUIElement3D>

FIGURE 5.9
(0,1,0) (1,1,0)
indice 0 indice 3
(1,0)
(0,0)

texture face avant

Copyright 2013 Patrice REY

(0,1)
(1,1) (0,0,0) (1,0,0)
indice 1 indice 2

On aura par exemple (figure 5.10), pour le rectangle en haut et à gauche, qui possède
CHAPITRE 5 Les modèles 3D basiques 147

une propriété Material recevant un placage de texture sous forme d’un dégradé de
couleurs et une propriété BackMaterial recevant une couleur jaune, le code suivant:
<!-- rectangle haut gauche -->
<ModelUIElement3D x:Name=ˈx_modele_hgˈ>
<Model3DGroup x:Name=ˈx_groupe_hgˈ>
...
<GeometryModel3D.Material>
<DiffuseMaterial x:Name=ˈx_surface_triangle_hgˈ>
<DiffuseMaterial.Brush>
<LinearGradientBrush x:Name=ˈmyGradientBrushˈ StartPoint=ˈ0,0.5ˈ
EndPoint=ˈ1,0.5ˈ>
<LinearGradientBrush.GradientStops>
<GradientStop Color=ˈYellowˈ Offset=ˈ0ˈ />
<GradientStop Color=ˈRedˈ Offset=ˈ0.25ˈ />
<GradientStop Color=ˈBlueˈ Offset=ˈ0.75ˈ />
<GradientStop Color=ˈLimeGreenˈ Offset=ˈ1ˈ />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial Brush=ˈYellowˈ></DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
...
</ModelUIElement3D>

FIGURE 5.10

(0,1,0) (1,1,0)
indice 0 indice 3
(1,0)
(0,0)

texture face avant

(0,1)
(1,1) (0,0,0) (1,0,0)
indice 1 indice 2
148 Programmez des jeux vidéo 3D avec C#5 et WPF
La face avant des quatre rectangles est éclairée par une lumière de type SpotLight.
On lui fixe sa propriété Color à White. On fixe sa propriété Position par un Point3D
de coordonnées (0,0,6). On règle la direction de l’axe du cône de lumière en le
faisant pointer sur l’origine du repère. Il faut donc affecter un vecteur Vector3D dont
l’orientation va de la position du spot au repère d’origine (propriété Direction affectée
par un Vector3D(0,0,-5) représentant cette direction). On règle les angles du cône qui
éclaire la scène en fixant les propriétés InnerConeAngle et OuterConeAngle. La figure
5.11 visualise les propriétés du cône de lumière de type SpotLight et l’effet obtenu
dans l’exemple.
<ModelUIElement3D x:Name=ˈx_lumiereˈ>
<Model3DGroup>
<SpotLight Color=ˈWhiteˈ Position=ˈ0,0,6ˈ Direction=ˈ0,0,-5ˈ
InnerConeAngle=ˈ20ˈ OuterConeAngle=ˈ35ˈ></SpotLight>
<DirectionalLight Color=ˈWhiteˈ Direction=ˈ0,0,1ˈ></DirectionalLight>
</Model3DGroup>
</ModelUIElement3D>

FIGURE 5.11

propriété
OuterConeAngle
propriété InnerConeAngle propriété
Direction

propriété
OuterConeAngle
propriété InnerConeAngle
Copyright 2013 Patrice REY
CHAPITRE 5 Les modèles 3D basiques 149

La face arrière des quatre rectangles est éclairée par une lumière directionnelle de
type DirectionalLight. Sa propriété Color donne la couleur de la lumière et sa propriété
Direction indique le sens d’envoi de la lumière (sous forme d’un Vector3D).
<ModelUIElement3D x:Name=ˈx_lumiereˈ>
<Model3DGroup>
<SpotLight Color=ˈWhiteˈ Position=ˈ0,0,6ˈ Direction=ˈ0,0,-5ˈ
InnerConeAngle=ˈ20ˈ OuterConeAngle=ˈ35ˈ></SpotLight>
<DirectionalLight Color=ˈWhiteˈ Direction=ˈ0,0,1ˈ></DirectionalLight>
</Model3DGroup>
</ModelUIElement3D>

FIGURE 5.12 Y
Y

Y Y
150 Programmez des jeux vidéo 3D avec C#5 et WPF
La figure 5.12 visualise le résultat obtenu quand on fait tourner la caméra autour de
la scène. On voit bien l’incidence de l’éclairage du spot sur la face avant des quatre
rectangles et l’incidence de l’éclairage de la lumière directionnelle sur la face arrière
des quatre rectangles.

2 - La modélisation du cube

La modélisation d’un volume consiste à passer par la réalisation de son maillage, puis
consiste à appliquer sur ce maillage une texture (sous forme d’une couleur unie, sous
forme d’un dégradé de couleurs, ou sous forme d’un placage de texture en général par
application d’une image prédéfinie).
L’UserControl ModeleCube.xaml, dans le dossier chapitre_05, visualise un ensemble de
quatre cubes (figure 5.13) pour lesquels on a appliqué des placage de texture différents
(texture avec motif, texture avec écriture, texture avec photographies et texture avec
une imitation de bois). Une caméra est positionnée dans l’espace 3D, elle vise l’origine
du repère 3D. Trois glissières permettent de faire tourner la caméra autour des axes X,
Y et Z. La scène est illuminée au moyen d’un éclairage ambiant.

2.1 - La géométrie du maillage

Le maillage d’un volume consiste à trouver toutes les facettes triangulaires qui le
composent. Une facette triangulaire est composée de trois points dans l’espace 3D.
Une fois les facettes triangulaires localisées, on peut alors appliquer dessus un placage
de texture. Un cube est composé de six faces. Chaque face peut être réalisée au moyen
de deux triangles accolés par leur hypothénuse.
Y

1
Copyright 2013 Patrice REY

X
2

Z
CHAPITRE 5 Les modèles 3D basiques 151

FIGURE 5.13
Différents cubes pour lesquels on a appliqué
des placages de texture prédéfinis

Trois glissières permettent de faire tourner


la caméra autour des axes X, Y et Z
152 Programmez des jeux vidéo 3D avec C#5 et WPF
La figure 5.14 visualise un cube par ses six faces et ses huit sommets. On fait coïncider
l’origine du repère avec le sommet arrière gauche et bas du cube. Les huit sommets
sont aux coordonnées 0(x,0,y), 1(x,y,z), 2(0,y,z), 3(0,0,z), 4(x,0,0), 5(x,y,0), 6(0,y,0) et
7(0,0,0). La composition des faces rectangulaires est la suivante (en utilisant le sens
contraire des aiguilles d’une montre):
• face de gauche avec les points 3, 2, 6 et 7,
• face avant avec les points 0, 1, 2 et 3,
• face de droite avec les points 4, 5, 1 et 0,
• face arrière avec les points 7, 6, 5 et 4,
• face du dessus avec les points 1, 5, 6 et 2,
• face du dessous avec les points 4, 0, 3 et 7.
Si on place les six faces du cube les unes à côté des autres, on obtient une surface
plate qui constitue l’enveloppe du cube. La figure 5.15 visualise cette enveloppe avec
la notification des faces avec leurs sommets respectifs. Chaque face va être subdivisée
en deux triangles qui sont composés de trois points.
FIGURE 5.15
6 5

face
dessus

2 1

6 2 2 1 1 5 5 6

face face face face


gauche avant droite arrière

7 3 3 0 0 4 4 7
Copyright 2013 Patrice REY

3 0

face
dessous

7 4
CHAPITRE 5 Les modèles 3D basiques 153
FIGURE 5.14

6 (0,y,0) 5 (x,y,0)
face
gauche face
dessus
6 (0,y,0) 2 (0,y,z)
2 (0,y,z) 1 (x,y,z)

7 (0,0,0) 3 (0,0,z)

face
(0,y,0) (x,y,0) arrière
6 5
5 (x,y,0) 6 (0,y,0)

(0,y,z) (x,y,z)
2 1 4 (x,0,0) 7 (0,0,0)
7 4
(0,0,0) (x,0,0)
X

3
0 1 (x,y,z) 5 (x,y,0)
(0,0,z) (x,0,z)
face
droite

Z 0 (x,0,z) 4 (x,0,0)

2 (0,y,z) 1 (x,y,z) 3 (0,0,z) 0 (x,0,z)

face face
avant dessous

3 (0,0,z) 0 (x,0,z) 7 (0,0,0) 4 (x,0,0)


154 Programmez des jeux vidéo 3D avec C#5 et WPF
La géométrie du cube, composée de 6 faces, se résume à un assemblage de 24 triangles.
L’ordre utilisé pour cet assemblage sera le suivant: face gauche, face avant, face droite,
face arrière, face du dessus et face du dessous. La figure 5.16 visualise les 6 faces
éclatées du cube avec leurs sommets respectifs. D’un point de vue pratique, on utilise
toujours le sens contraire des aiguilles d’une montre, cela permet de signifier que
l’ordre que l’on donne est celui de la face vue (donc la face sur laquelle on appliquera
une texture avec la propriété Material, la face cachée recevra quant à elle une autre
texture par la propriété BackMaterial). Comme le visualise la figure 5.16, on ajoute
à chaque sommet un indice, en commençant par 0. Cela nous servira plus tard pour
effectuer le placage de texture au travers de la propriété TextureCoordinates.
FIGURE 5.16

6 6 0 2 2 2 4 1

0 3 4 7

1 2 2 5 6 6
7 3 3 3 0 0

face gauche face avant

5 5 12 6 1 1 8 5

12 15 8 11

13 14 14 9 10 10
4 7 7 0 4 4

face arrière face droite

6 6 16 5 3 3 20 0
Copyright 2013 Patrice REY

16 19 20 23

17 18 18 21 22 22
2 1 1 7 4 4

face dessus face dessous


CHAPITRE 5 Les modèles 3D basiques 155

On définit une méthode MaillageCube qui reçoit en paramètre une largeur de côté cote
de type double et qui retourne la géométrie du maillage, intitulée maillage, de type
MeshGeometry3D. Les 8 sommets sont exprimés par des structures Point3D intitulées
pt_0 à pt_7.
private MeshGeometry3D MaillageCube(double cote) {
MeshGeometry3D maillage = new MeshGeometry3D();
Point3D pt_0 = new Point3D(cote, 0, cote);
Point3D pt_1 = new Point3D(cote, cote, cote);
Point3D pt_2 = new Point3D(0, cote, cote);
Point3D pt_3 = new Point3D(0, 0, cote);
Point3D pt_4 = new Point3D(cote, 0, 0);
Point3D pt_5 = new Point3D(cote, cote, 0);
Point3D pt_6 = new Point3D(0, cote, 0);
Point3D pt_7 = new Point3D(0, 0, 0);
...
return maillage;
}
On ajoute les sommets de chaque face à la propriété Positions de l’objet maillage dans
l’ordre indiqué sur la figure 5.16 (sens contraire des aiguilles d’une montre).
private MeshGeometry3D MaillageCube(double cote) {
MeshGeometry3D maillage = new MeshGeometry3D();
...
//6 7 3 2 face gauche
maillage.Positions.Add(pt_6);
maillage.Positions.Add(pt_7);
maillage.Positions.Add(pt_3);
maillage.Positions.Add(pt_2);
//2 3 0 1 face avant
maillage.Positions.Add(pt_2);
maillage.Positions.Add(pt_3);
maillage.Positions.Add(pt_0);
maillage.Positions.Add(pt_1);
//1 0 4 5 face droite
maillage.Positions.Add(pt_1);
maillage.Positions.Add(pt_0);
maillage.Positions.Add(pt_4);
maillage.Positions.Add(pt_5);
//5 4 7 6 face arriere
maillage.Positions.Add(pt_5);
maillage.Positions.Add(pt_4);
maillage.Positions.Add(pt_7);
maillage.Positions.Add(pt_6);
//6 2 1 5 face dessus
maillage.Positions.Add(pt_6);
maillage.Positions.Add(pt_2);
maillage.Positions.Add(pt_1);
156 Programmez des jeux vidéo 3D avec C#5 et WPF
maillage.Positions.Add(pt_5);
//3 7 4 0 face dessous
maillage.Positions.Add(pt_3);
maillage.Positions.Add(pt_7);
maillage.Positions.Add(pt_4);
maillage.Positions.Add(pt_0);
...
return maillage;
}
Puis il faut générer les indices des sommets que l’on doit ajouter à la propriété
TriangleIndices. La propriété Positions contient une collection de Point3D. Avec une
boucle for, on parcourt cette collection par pas de 4, et on ajoute un nouvel indice à
partir de l’indice 0 de base. On s’aide d’une variable depart qui délivre les indices à la
demande. Pourquoi utiliser un pas de 4? Parce que lorsque l’on a 2 triangles accolés l’un
à l’autre avec 4 sommets aux indices 0, 1, 2 et 3, on doit passer en premier la séquence
012 du premier triangle puis en deuxième la séquence 230 du second triangle.
//generer les indices pour la suite de triangles
private void GenererTriangleIndice(MeshGeometry3D maillage) {
int depart = 0;
for (int xx = 0; xx < maillage.Positions.Count; xx += 4) {
maillage.TriangleIndices.Add(depart);
maillage.TriangleIndices.Add(depart + 1);
maillage.TriangleIndices.Add(depart + 2);
maillage.TriangleIndices.Add(depart + 2);
maillage.TriangleIndices.Add(depart + 3);
maillage.TriangleIndices.Add(depart);
depart += 4;
}
}

2.2 - Le placage de texture

Pour effectuer un placage de texture, il faut renseigner la propriété TextureCoordinates


de MeshGeometry3D. Pour notre cube, le placage de texture consiste à appliquer
une texture sous forme d’image sur l’ensemble des triangles. Cela veut dire que les
Copyright 2013 Patrice REY

coordonnées de texture doivent être en concordance avec la séquence passée des


triangles. La texture est représentée par une image 2D (figure 5.17) dont l’origine est
en haut et à gauche avec les coordonnées (0,0), et la fin est en bas à droite avec les
coordonnées (1,1).
L’emplacement des faces sur cette image permet de déterminer les coordonnées
relatives à prendre en compte pour la propriété TextureCoordinates. Par exemple la
face de gauche sera composée des coordonnées de texture (0,1/3), (0,2/3) et (1/4,2/3)
CHAPITRE 5 Les modèles 3D basiques 157

pour le premier triangle, et (1/4,2/3), (1/4,1/3) et (0,1/3) pour le second triangle.


FIGURE 5.17
(0,0) (1/4,0) (1/2,0) (3/4,0) (1,0)

face
dessus

(0,1/3)

face face face face


gauche avant droite arrière

(0,2/3)

face
dessous

(0,1)
(1,1)
(0,1/3) (1/4,1/3)

face
gauche

(0,2/3) (1/4,2/3)

On ajoute donc à la propriété TextureCoordinates de maillage les coordonnées de texture


qui sont des structures Point avec une coordonnée x et une coordonnée y de type
double. Comme on exprime les coordonnées de façon relative en passant par exemple
la valeur 2/3, de façon à effectuer une division qui donne un résultat de type double (et
non une division entière comme par défaut), les coordonnées passées doivent être de
type double. On ajoute donc un d pour les passer en double (par exemple 2d/3d). Notre
158 Programmez des jeux vidéo 3D avec C#5 et WPF
méthode MaillageCube est donc:
private MeshGeometry3D MaillageCube(double cote) {
MeshGeometry3D maillage = new MeshGeometry3D();
...
//6 7 3 2 face gauche
maillage.TextureCoordinates.Add(new Point(0d, 1d / 3d));
maillage.TextureCoordinates.Add(new Point(0d, 2d / 3d));
maillage.TextureCoordinates.Add(new Point(1d / 4d, 2d / 3d));
maillage.TextureCoordinates.Add(new Point(1d / 4d, 1d / 3d));
//2 3 0 1 face avant
maillage.TextureCoordinates.Add(new Point(1d / 4d, 1d / 3d));
maillage.TextureCoordinates.Add(new Point(1d / 4d, 2d / 3d));
maillage.TextureCoordinates.Add(new Point(1d / 2d, 2d / 3d));
maillage.TextureCoordinates.Add(new Point(1d / 2d, 1d / 3d));
//1 0 4 5 face droite
maillage.TextureCoordinates.Add(new Point(1d / 2d, 1d / 3d));
maillage.TextureCoordinates.Add(new Point(1d / 2d, 2d / 3d));
maillage.TextureCoordinates.Add(new Point(3d / 4d, 2d / 3d));
maillage.TextureCoordinates.Add(new Point(3d / 4d, 1d / 3d));
//5 4 7 6 face arriere
maillage.TextureCoordinates.Add(new Point(3d / 4d, 1d / 3d));
maillage.TextureCoordinates.Add(new Point(3d / 4d, 2d / 3d));
maillage.TextureCoordinates.Add(new Point(1d, 2d / 3d));
maillage.TextureCoordinates.Add(new Point(1d, 1d / 3d));
//6 2 1 5 face dessus
maillage.TextureCoordinates.Add(new Point(1d / 4d, 0d));
maillage.TextureCoordinates.Add(new Point(1d / 4d, 1d / 3d));
maillage.TextureCoordinates.Add(new Point(1d / 2d, 1d / 3d));
maillage.TextureCoordinates.Add(new Point(1d / 2d, 0d));
//3 7 4 0 face dessous
maillage.TextureCoordinates.Add(new Point(1d / 4d, 2d / 3d));
maillage.TextureCoordinates.Add(new Point(1d / 4d, 1d));
maillage.TextureCoordinates.Add(new Point(1d / 2d, 1d));
maillage.TextureCoordinates.Add(new Point(1d / 2d, 2d / 3d));
//
GenererTriangleIndice(maillage);
return maillage;
}
Pour nos 4 cubes, on utilise des ressources image au format PNG qui sont texture_
Copyright 2013 Patrice REY

cube_1.png de taille 1000x746 pixels, texture_cube_2.png de taille 340x255 pixels,


texture_cube_3.png de taille 340x255 pixels et texture_cube_4.png de taille 1000x750
pixels.
Maintenant nous utilisons une méthode AjouterObjetCube qui reçoit en paramètre une
largeur de côté largeur_cote de type double, un chemin absolu nommé chemin_texture,
de type string, qui pointe sur une ressource image embarquée, et deplac_x, deplac_y et
deplac_z, de type double, pour translater le cube sur la scène. Cette méthode retourne
CHAPITRE 5 Les modèles 3D basiques 159

FIGURE 5.18

face
dessus

ressource image
texture_cube_1.png
de taille 1000x746 face face face face
pixels
gauche avant droite arrière

face
dessous

ressource image
texture_cube_2.png de taille ressource image texture_cube_4.png de
340x255 pixels taille 1000x750 pixels

ressource image
texture_cube_3.png de taille
340x255 pixels
160 Programmez des jeux vidéo 3D avec C#5 et WPF
un objet ModelUIElement3D qui correspond au cube texturé qui peut alors être
positionné directement sur la scène.
L’objet ModelUIElement3D nommé cube est instancié. On affecte à sa propriété Model
une géométrie de maillage geo_cube de type GeometryModel3D. La propriété Geometry
de geo_cube reçoit le maillage MeshGeometry3D de la méthode MaillageCube. La
propriété Material de geo_cube reçoit la texture de placage de la face vue, et la propriété
BackMaterial de geo_cube reçoit la texture de placage de la face cachée.
Pour appliquer une texture sous forme d’une image, on instancie un ImageBrush et
on affecte à sa propriété ImageSource un objet BitmapImage dont on fixe le chemin
sous forme d’un URI. Pour référencer une image embarquée dans l’application, on
utilise le format d’URI absolu suivant: new Uri(«pack://application:,,/» + chemin_texture,
UriKind.Absolute) qui référence une image dans l’arborescence des fichiers. Puis l’objet
ImageBrush est affecté à la propriété Brush d’un objet DiffuseMaterial, lui-même étant
affecté à la propriété Material de geo_cube.
Pour translater le cube sur la scène, on utilise un objet TranslateTransform3D qui
reçoit en paramètre les déplacements deplac_x, deplac_y et deplac_z. Puis on l’affecte à
la propriété Transform de cube.
private ModelUIElement3D AjouterObjetCube(double largeur_cote,
string chemin_texture, double deplac_x, double deplac_y, double deplac_z) {
ModelUIElement3D cube = new ModelUIElement3D();
GeometryModel3D geo_cube = new GeometryModel3D();
geo_cube.Geometry = MaillageCube(largeur_cote);
DiffuseMaterial diffus = new DiffuseMaterial();
ImageBrush img_brush = new ImageBrush();
img_brush.ImageSource = new BitmapImage(
new Uri(«pack://application:,,/» + chemin_texture, UriKind.Absolute));
diffus.Brush = img_brush;
geo_cube.Material = diffus;
geo_cube.BackMaterial = new DiffuseMaterial(Brushes.Yellow);
cube.Model = geo_cube;
cube.Transform = new TranslateTransform3D(deplac_x,deplac_y,deplac_z);
return cube;
}
Copyright 2013 Patrice REY

2.3 - Visualisation des cubes

Dans le gestionnaire de l’événement Loaded de l’UserControl, on instancie quatre


objets ModelUIElement3D intitulés cube_1, cube_2, cube_3 et cube_4. On fixe
leur largeur de côté, le chemin qui pointe sur l’image de placage de texture, et les
déplacements consécutifs à la translation à effectuer. Ces 4 cubes sont ajoutés aux
fils (propriété Children) du conteneur x_conteneur_3d, de type ContainerUIElement3D,
CHAPITRE 5 Les modèles 3D basiques 161

par la méthode Add.


//usercontrol evenement Loaded
private void UserControl_Loaded(object sender, RoutedEventArgs e) {
this.UpdateLayout();
ModelUIElement3D cube_1 = AjouterObjetCube(1,
«chapitre_05/image_texture/texture_cube_1.png», 1, 0, 0);
x_conteneur_3d.Children.Add(cube_1);
ModelUIElement3D cube_2 = AjouterObjetCube(0.75,
«chapitre_05/image_texture/texture_cube_2.png», -1.5, 0, 0);
x_conteneur_3d.Children.Add(cube_2);
ModelUIElement3D cube_3 = AjouterObjetCube(1.15,
«chapitre_05/image_texture/texture_cube_3.png», -0.75, 0, -1.30);
x_conteneur_3d.Children.Add(cube_3);
ModelUIElement3D cube_4 = AjouterObjetCube(0.80,
«chapitre_05/image_texture/texture_cube_4.png», -0.5, 0, 1.30);
x_conteneur_3d.Children.Add(cube_4);
}
Pour permettre une meilleure visualisation de la scène, on utilise trois glissières de
type Slider pour effectuer des rotation de la caméra autour des axes X, Y et Z. Les
variables v_angle_rotation_axe_x, v_angle_rotation_axe_y et v_angle_rotation_axe_z
stockent les angles de rotation effectués selon les trois axes, en récupérant leur valeur
en fonction de la glissière correspondant. Le méthode EffectuerTransformationCamera
réalise l’opération de rotation attendue.
//glissiere pour faire tourner la caméra
private void x_slider_camera_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e) {
v_angle_rotation_axe_y = x_slider_camera_axe_y.Value;
v_angle_rotation_axe_x = x_slider_camera_axe_x.Value;
v_angle_rotation_axe_z = x_slider_camera_axe_z.Value;
EffectuerTransformationCamera();
}
Pour effectuer une combinaison de transformations, on utilise un objet
Transform3DGroup qui reçoit dans sa propriété Children les transformations à
effectuer dans un ordre précis. La rotation autour d’un axe se fait par l’instanciation
d’un objet RotateTransform3D qui reçoit en paramètre un objet AxisAngleRotation3D.
Un objet AxisAngleRotation3D représente une rotation dans l’espace 3D en fonction
de la valeur d’un angle et selon un axe. Sa propriété Angle reçoit une valeur d’angle
en degrés sous la forme d’un double, et sa propriété Axis reçoit l’indication de l’axe de
rotation sous forme d’un Vector3D. Une rotation autour de l’axe X sera symbolisée par
un Vector3D(1,0,0), autour de l’axe Y par un Vector3D(0,1,0), et autour de l’axe Z par un
Vector3D(0,0,1). L’objet Transform3DGroup est affecté à la propriété Transform de la
caméra x_camera pour réaliser effectivement la transformation.
162 Programmez des jeux vidéo 3D avec C#5 et WPF
private void EffectuerTransformationCamera() {
Transform3DGroup gp = new Transform3DGroup();
RotateTransform3D rot_x = new RotateTransform3D(new AxisAngleRotation3D(
new Vector3D(1, 0, 0), v_angle_rotation_axe_x));
RotateTransform3D rot_y = new RotateTransform3D(new AxisAngleRotation3D(
new Vector3D(0, 1, 0), v_angle_rotation_axe_y));
RotateTransform3D rot_z = new RotateTransform3D(new AxisAngleRotation3D(
new Vector3D(0, 0, 1), v_angle_rotation_axe_z));
gp.Children.Add(rot_x);
gp.Children.Add(rot_y);
gp.Children.Add(rot_z);
x_camera.Transform = gp;
}
Les figures 5.19 et 5.20 montrent différentes vues obtenues en fonction des rotations
de la scène effectuées en fonction des trois glissières. Cela permet d’apprécier plus
facilement le placage des textures sur les cubes ainsi que les positions de déplacements
effectués.
FIGURE 5.19

Copyright 2013 Patrice REY


CHAPITRE 5 Les modèles 3D basiques 163
FIGURE 5.20

2
164 Programmez des jeux vidéo 3D avec C#5 et WPF

2.4 - Les axes de coordonnées et le plan de travail

Généralement les logiciels de modélisation 3D permettent d’afficher (ou de masquer)


un repère des coordonnées et un plan de travail. Cela permet une meilleure visualisation
des objets dans l’espace 3D.
L’UserControl AxePlanTravail.xaml, dans le dossier chapitre_05, visualise un ensemble
de quatre cubes texturés avec un repère de coordonnées et un plan de travail (figure
5.21). Les axes du repère sont colorés (rouge pour l’axe X, bleu pour l’axe Y et vert
pour l’axe Z). Le plan de travail, qui symbolise le sol, est représenté par une grille.
Cela permet de voir à travers pour observer les objets du dessus vers le dessous, et
inversement. Les trois glissières permettent d’effectuer la rotation de la caméra autour
des trois axes. Un bouton permet de remettre la vue de la scène à son état d’origine.
Deux cases à cocher permettent, en fonction de leur état (coché ou décoché), d’afficher
ou de masquer le repère des coordonnées et le plan de travail, de façon individuelle.
Un contrôle de type TreeView permet d’afficher une arborescence de données dans
laquelle on recense les objets 3D contenus dans la scène. La scène est, quant à elle,
illuminée au moyen d’un éclairage ambiant.
La modélisation des axes de coordonnées est une extension de la modélisation du
cube. En effet, on peut considérer qu’un axe de coordonnée a la forme d’un cube
très long, pas haut et pas profond. A partir de ce constat, modéliser un axe revient à
modéliser un cube centré sur l’origine du repère des coordonnées.
La figure 5.22 visualise géométriquement un cube centré sur l’origine du repère, et dont
les coordonnées de ses coins sont exprimées en fonction de x, y et z. Il y a huit coins
repérés par les points numéro 0(+x,-y,+z), numéro 1(+x,+y,+z), numéro 2(-x,+y,+z),
numéro 3(-x,-y,+z), numéro 4(+x,-y,-z), numéro 5(+x,+y,-z), numéro 6(-x,+y,-z) et
numéro 7(-x,-y,-z). Chaque face est repérée par un ensemble de quatre points qui
représentent deux triangles avec (en suivant toujours le sens contraire des aiguilles
d’une montre pour signifier la face visible):
• la face avant avec les points 2, 3, 0 et 1,
• la face de droite avec les points 1, 0, 4 et 5,
Copyright 2013 Patrice REY

• la face arrière avec les points 5, 4, 7 et 6,


• la face de gauche avec les points 6, 7, 3 et 2,
• la face du dessus avec les points 6, 2, 1 et 5,
• la face du dessous avec les points 3, 7, 4 et 0.
La méthode MaillageAxe permet de générer la géométrie d’un axe en fonction de trois
coordonnées long_x, long_y et long_z (la longueur, la largeur et la profondeur de l’axe).
CHAPITRE 5 Les modèles 3D basiques 165

FIGURE 5.21
166 Programmez des jeux vidéo 3D avec C#5 et WPF

FIGURE 5.22
6 (-x,+y,-z) 5 (+x,+y,-z) 5 4 7 6

face face
dessus arrière

2 (-x,+y,+z) 1 (+x,+y,+z) 5 (+x,+y,-z) 6 (-x,+y,-z)


face
gauche
6 2 1 5
6 (-x,+y,-z) 2 (-x,+y,+z)

4 (+x,-y,-z) 7 (-x,-y,-z)
Y

7 (-x,-y,-z) 3 (-x,-y,+z) 6
(-x,+y,-z) 5 (+x,+y,-z)
6 7 3 2

1
(-x,+y,+z) 2 (+x,+y,+z)
7 X
(-x,-y,-z) 4 (+x,-y,-z)

1 (+x,+y,+z) 5 (+x,+y,-z)

face
(-x,-y,+z) 3 0 droite
Z (+x,-y,+z)
0 (+x,-y,+z) 4 (+x,-y,-z)

3 (-x,-y,+z) 0 (+x,-y,+z) 1 0 4 5
2 (-x,+y,+z) 1 (+x,+y,+z)
Copyright 2013 Patrice REY

face face
avant dessous

3 (-x,-y,+z) 0 (+x,-y,+z)) 7 (-x,-y,-z) 4 (+x,-y,-z)

2 3 0 1 3 7 4 0
CHAPITRE 5 Les modèles 3D basiques 167

A noter que l’on ajoute aucune coordonnée de texture dans la propriété


TextureCoordinates puisque les axes seront de couleur unie.
private MeshGeometry3D MaillageAxe(double long_x, double long_y, double long_z) {
MeshGeometry3D maillage = new MeshGeometry3D();
Point3D pt_0 = new Point3D(+long_x, -long_y, +long_z);
Point3D pt_1 = new Point3D(+long_x, +long_y, +long_z);
Point3D pt_2 = new Point3D(-long_x, +long_y, +long_z);
Point3D pt_3 = new Point3D(-long_x, -long_y, +long_z);
Point3D pt_4 = new Point3D(+long_x, -long_y, -long_z);
Point3D pt_5 = new Point3D(+long_x, +long_y, -long_z);
Point3D pt_6 = new Point3D(-long_x, +long_y, -long_z);
Point3D pt_7 = new Point3D(-long_x, -long_y, -long_z);
...
}
Pour générer un axe coloré, on utilise la méthode AjouterAxe qui retourne un objet
ModelUIElement3D représentant un axe. Elle reçoit en paramètre une longueur, une
largeur, une profondeur et une couleur.
private ModelUIElement3D AjouterAxe(double long_x, double long_y, double long_z,
Color couleur) {
ModelUIElement3D axe = new ModelUIElement3D();
GeometryModel3D geo_axe = new GeometryModel3D();
geo_axe.Geometry = MaillageAxe(long_x, long_y, long_z);
geo_axe.Material = new DiffuseMaterial(new SolidColorBrush(couleur));
geo_axe.BackMaterial = new DiffuseMaterial(new SolidColorBrush(couleur));
axe.Model = geo_axe;
return axe;
}
Comme il nous faudra la possibilité d’afficher ou de masquer le repère des coordonnées,
on instancie un conteneur de type ContainerUIElement3D qui va stocker dans sa
propriété Children les trois axes. Et ce conteneur sera ajouté au conteneur principal
qui contient tous les objets 3D de la scène. Pour faire référence au conteneur des
axes, il faut lui donner un nom. Cela se fait en fixant une propriété de dépendance par
l’intermédiaire de la méthode SetValue, qui reçoit le nom de la propriété de dépendance
invoquée (NameProperty) et une chaîne texte unique pour référencer l’objet.
//usercontrol evenement Loaded
private void UserControl_Loaded(object sender, RoutedEventArgs e) {
...
//repere de coordonnee
ContainerUIElement3D conteneur_repere_axe = new ContainerUIElement3D();
conteneur_repere_axe.SetValue(NameProperty, «x_conteneur_repere_axe»);
x_conteneur_3d.Children.Add(conteneur_repere_axe);
ModelUIElement3D repere_axe_x = AjouterAxe(10, 0.005, 0.005, Colors.Red);
repere_axe_x.SetValue(NameProperty, «x_repere_axe_x»);
168 Programmez des jeux vidéo 3D avec C#5 et WPF
conteneur_repere_axe.Children.Add(repere_axe_x);
ModelUIElement3D axe_y = AjouterAxe(0.005, 10, 0.005, Colors.Blue);
axe_y.SetValue(NameProperty, «x_repere_axe_y»);
conteneur_repere_axe.Children.Add(axe_y);
ModelUIElement3D axe_z = AjouterAxe(0.005, 0.005, 10, Colors.Green);
axe_z.SetValue(NameProperty, «x_repere_axe_z»);
conteneur_repere_axe.Children.Add(axe_z);
...
}
La case à cocher x_check_xyz, de type CheckBox, permet de masquer ou d’afficher
le repère des coordonnées. On lui ajoute un gestionnaire pour l’événement Checked
(état coché) et Unchecked (état décoché). La méthode AfficherCacherConteneur va
permettre d’afficher ou de masquer le repère. Elle reçoit en paramètre le nom de
référence du conteneur (x_conteneur_repere_axe) et un état à effectuer («masquer» ou
«afficher»). Masquer le conteneur, qui contient les trois axes, consiste à parcourir le
conteneur principal de la scène (x_conteneur_3d) et à localiser le conteneur demandé.
Une fois fait, la propriété Visibility du conteneur passe de la valeur Visible à Hidden
pour masquer le conteneur, et inversement.
//
private void x_check_xyz_Checked(object sender, RoutedEventArgs e) {
AfficherCacherConteneur(«x_conteneur_repere_axe», «masquer»);
}
//
private void x_check_xyz_Unchecked(object sender, RoutedEventArgs e) {
AfficherCacherConteneur(«x_conteneur_repere_axe», «afficher»);
}
//
private void AfficherCacherConteneur(string nom_objet_3d_xaml,
string etat_visibilite) {
for (int xx = 0; xx < x_conteneur_3d.Children.Count; xx++) {
Visual3D visual3d = x_conteneur_3d.Children[xx];
string nom = (string)visual3d.GetValue(NameProperty);
if (nom == nom_objet_3d_xaml) {
ContainerUIElement3D conteneur_axe =
(ContainerUIElement3D)x_conteneur_3d.Children[xx];
if (etat_visibilite == «masquer») {
conteneur_axe.Visibility = Visibility.Hidden;
Copyright 2013 Patrice REY

}
if (etat_visibilite == «afficher») {
conteneur_axe.Visibility = Visibility.Visible;
}
break;
}
}
}
CHAPITRE 5 Les modèles 3D basiques 169

Pour augmenter le réalisme de la scène 3D, généralement on ajoute un plan de travail,


sous forme d’une grille, pour symboliser le sol sur lequel on positionne les objets. Ce
plan de travail est représenté par le plan (Ox,Oz). De plus, il faut à la fois voir à travers la
grille, du dessus et du dessous. La manière la plus simple de réaliser ce plan de travail
avec ces exigences, c’est de modéliser plusieurs petites faces planes séparées d’une
unité. D’un point de vue de l’implémentation, la méthode AjouterPlanDeTravail génère
un plan de travail en fonction d’un nombre de lignes voulues dist de chaque côté.
Le maillage du plan de travail reste le même que pour celui d’une face rectangulaire
classique. Pour les colorations, on utilise une couleur grise unie recto verso.
private ContainerUIElement3D AjouterPlanDeTravail(double dist) {
ContainerUIElement3D conteneur_plan_travail = new ContainerUIElement3D();
conteneur_plan_travail.SetValue(NameProperty, «x_conteneur_plan_travail»);
ModelUIElement3D mui_plan_travail = new ModelUIElement3D();
conteneur_plan_travail.Children.Add(mui_plan_travail);
Model3DGroup md_gp = new Model3DGroup();
mui_plan_travail.Model = md_gp;
for (double xx = -dist; xx <= dist; xx += 1) {
GeometryModel3D face = new GeometryModel3D();
MeshGeometry3D maillage = new MeshGeometry3D();
maillage.Positions.Add(new Point3D(xx + 0.005, 0, +dist));
maillage.Positions.Add(new Point3D(xx + 0.005, 0, -dist));
maillage.Positions.Add(new Point3D(xx - 0.005, 0, -dist));
maillage.Positions.Add(new Point3D(xx - 0.005, 0, +dist));
maillage.TriangleIndices.Add(0);
maillage.TriangleIndices.Add(1);
maillage.TriangleIndices.Add(2);
maillage.TriangleIndices.Add(2);
maillage.TriangleIndices.Add(3);
maillage.TriangleIndices.Add(0);
face.Geometry = maillage;
face.Material = new DiffuseMaterial(Brushes.LightGray);
face.BackMaterial = new DiffuseMaterial(Brushes.LightGray);
md_gp.Children.Add(face);
}
for (double zz = -dist; zz <= dist; zz += 1) {
GeometryModel3D face = new GeometryModel3D();
MeshGeometry3D maillage = new MeshGeometry3D();
maillage.Positions.Add(new Point3D(+dist, 0, zz + 0.005));
maillage.Positions.Add(new Point3D(-dist, 0, zz + 0.005));
maillage.Positions.Add(new Point3D(-dist, 0, zz - 0.005));
maillage.Positions.Add(new Point3D(+dist, 0, zz - 0.005));
maillage.TriangleIndices.Add(0);
maillage.TriangleIndices.Add(1);
maillage.TriangleIndices.Add(2);
maillage.TriangleIndices.Add(2);
maillage.TriangleIndices.Add(3);
170 Programmez des jeux vidéo 3D avec C#5 et WPF
maillage.TriangleIndices.Add(0);
face.Geometry = maillage;
face.Material = new DiffuseMaterial(Brushes.LightGray);
face.BackMaterial = new DiffuseMaterial(Brushes.LightGray);
md_gp.Children.Add(face);
}
return conteneur_plan_travail;
}
Le conteneur conteneur_plan_travail est ajouté sur la scène, et le principe de l’affichage
ou du masquage du plan de travail est identique à celui utilisé pour le repère des
coordonnées.
//usercontrol evenement Loaded
private void UserControl_Loaded(object sender, RoutedEventArgs e) {
...
ContainerUIElement3D conteneur_plan_travail = AjouterPlanDeTravail(4d);
x_conteneur_3d.Children.Add(conteneur_plan_travail);
...
}
//
private void x_check_plan_travail_Checked(object sender, RoutedEventArgs e) {
AfficherCacherConteneur(«x_conteneur_plan_travail», «masquer»);
}
//
private void x_check_plan_travail_Unchecked(object sender, RoutedEventArgs e) {
AfficherCacherConteneur(«x_conteneur_plan_travail», «afficher»);
}
Très souvent, les logiciels de 3D affichent le détail des objets contenus sur la scène
(caméra, lumière, objets 3D, etc.) dans un contrôle de type TreeView (contrôle qui
affiche une arborescence).
On ajoute en XAML un contrôle x_arbo de type TreeView dans lequel on ajoute deux
objets TreeViewItem x_arbo_doss_camera et x_arbo_doss_scene. Ces deux TreeViewItem
représentent le dossier contenant la caméra et le dossier contenant les objets 3D de la
scène qui sont stockés dans le conteneur principal.
<!-- arborescence des objets -->
<TreeView x:Name=ˈx_arboˈ Height=ˈ383ˈ Canvas.Left=ˈ10ˈ Canvas.Top=ˈ607ˈ
Copyright 2013 Patrice REY

Width=ˈ439ˈ FontSize=ˈ16ˈ>
<TreeViewItem x:Name=ˈx_arbo_doss_cameraˈ Header=ˈDossier caméraˈ></TreeViewItem>
<TreeViewItem x:Name=ˈx_arbo_doss_sceneˈ Header=ˈDossier scène 3Dˈ>
</TreeViewItem>
</TreeView>
Par un parcours des éléments contenus dans le conteneur principal x_conteneur_3d, on
récupère le nom des conteneurs stockés et on instancie un TreeViewItem qui reçoit
une chaîne texte contenant ce nom. Ensuite il est ajouté au TreeView dans le dossier
CHAPITRE 5 Les modèles 3D basiques 171

correspondant. A noter que la méthode TreeViewItem.ExpandSubtree permet de garder


ouvert le contenu d’un dossier.
//usercontrol evenement Loaded
private void UserControl_Loaded(object sender, RoutedEventArgs e) {
...
//arborescence des objets
x_arbo_doss_camera.Items.Add((string)x_camera.GetValue(NameProperty));
for (int xx = 0; xx < x_conteneur_3d.Children.Count; xx++) {
Visual3D visual3d = x_conteneur_3d.Children[xx];
string nom = (string)visual3d.GetValue(NameProperty);
TreeViewItem tvi = new TreeViewItem();
tvi.Header = (string)nom;
x_arbo_doss_scene.Items.Add(tvi);
}
x_arbo_doss_camera.ExpandSubtree();
x_arbo_doss_scene.ExpandSubtree();
}
Les figures 5.23 et 5.24 visualisent les quatre cubes texturés posés sur le plan de travail
à des endroits différents, avec l’affichage du plan de travail et le masquage du repère
des coordonnées (figure 5.23), et inversement (figure 5.24).
FIGURE 5.23
172 Programmez des jeux vidéo 3D avec C#5 et WPF
FIGURE 5.24

3 - La modélisation du cylindre

Le cylindre est un solide avec une surface cylindrique délimitée par deux plans
parallèles. Nous allons voir la modélisation du cylindre circulaire creux (le tube par
exemple) après avoir abordé la notion de coordonnée sphérique.

3.1 - Les coordonnées sphériques

Les coordonnées cartésiennes d’un point, dans l’espace 2D, s’expriment par 2
coordonnées (x,y,) selon les 2 axes. Tous les points qui se situent à une distance R de
Copyright 2013 Patrice REY

l’origine O appartiennent à un cercle de centre O et de rayon R.


Un point A appartenant à ce cercle peut être repéré par des coordonnées que l’on
appelle coordonnées sphériques. Soient xa et ya les coordonnées de A selon l’axe X et
l’axe Y et R le rayon du cercle, ces coordonnées s’expriment par (avec l’angle φ qui est
l’angle entre les segments [OA] et [Oxa]):
xa = R * cos(φ)
ya = R * sin(φ)
CHAPITRE 5 Les modèles 3D basiques 173

R = rayon du cercle point A:


A xa = R * cos(φ)
ya ya = R * sin(φ)

R φ
X
O xa

3.2 - La géométrie du cylindre

Le cylindre est un solide avec une surface cylindrique délimitée par deux plans
parallèles. Une surface cylindrique est formée par le déplacement parallèle d’une
droite le long d’une courbe fermée. La figure ci-dessous visualise géométriquement la
constitution d’un cylindre circulaire plein.
Y
B
Rg = rayon du cercle externe

point A:
xa = Rg*cos(φ)
ya = 0
za = -Rg*sin(φ)

cas du point B:
cylindre xb = Rg*cos(φ) = xa
plein: yb = H
zb = -Rg*sin(φ) = za
A
za
O φ
X
xa
Z
174 Programmez des jeux vidéo 3D avec C#5 et WPF
Dans le cas du cylindre circulaire plein, le point A peut être repéré par ses coordonnées
sphériques (avec Rg le rayon du cercle et φ l’angle de rotation):
xa = Rg * cos(φ)
ya = 0
za = -Rg * sin(φ)
Le point B a des coordonnées similaires à part une hauteur différente (H hauteur du
cylindre) d’où yb=H.
xb = Rg * cos(φ)
yb = H
zb = -Rg * sin(φ)
Le cylindre creux est similaire au cylindre plein avec un cercle extérieur de rayon Rg
et un cercle intérieur plus petit de rayon Rp. La position des points se calculera de la
même façon en choisissant le rayon Rg ou Rp d’appartenance du point considéré.
Y
D

Rg = rayon du cercle externe


cas du Rp = rayon du cercle interne
cylindre
creux:

C
B
O X

point A: point B:
Copyright 2013 Patrice REY

xa = Rg*cos(φ) xb = Rg*cos(φ)
ya = H ya = 0
za = -Rg*sin(φ) zb = -Rg*sin(φ)

Partant de là, on s’aperçoit déjà que l’on va devoir modéliser quatre facettes dans le
cas général (cas du cylindre creux). Le cylindre plein sera en fait une exception, puisque
l’on choisira un rayon interne Rp de quantité nulle.
CHAPITRE 5 Les modèles 3D basiques 175

3.3 - La facette cylindrique

Il y a donc quatre facettes à générer pour modéliser la facette cylindrique à faire tourner
autour d’un axe vertical de façon générale (figure 5.25). Les facettes de quatre points,
qui sont composées de deux triangles, sont (avec l’ordre de celui du sens contraire des
aiguilles d’une montre):
• la facette extérieure ABCD,
• la facette du dessus HADE,
• la facette intérieure EFGH,
• la facette du dessous BGFC.
La figure 5.25 visualise les coordonnées de chacun de ces points en fonction de leur
position externe ou interne.
FIGURE 5.25

D
E point A:
xa = Rg*cos(φ)
A ya = H
H za = -Rg*sin(φ)

point B:
facette ABCD
xb = Rg*cos(φ)
ya = 0
facette HADE zb = -Rg*sin(φ)

point F:
facette EFGH
xf = Rp*cos(φ)
yf = 0
facette BGFC zf = -Rp*sin(φ)

point E:
xe = Rp*cos(φ)
C ye = H
F ze = -Rp*sin(φ)

B
G
On commence par définir la géométrie de la facette extérieure ABCD par la méthode
MaillageCylindreFaceExterne, qui reçoit en paramètre la hauteur du cylindre (hauteur
de type double), le rayon du cercle extérieure (ray_ext de type double) et le nombre
de division correspondant au nombre de facettes voulues (nb_div de type int). L’angle
176 Programmez des jeux vidéo 3D avec C#5 et WPF
φ sera calculé en fonction du nombre de division. Pour cela on définit un pas d’angle
pas_angle, de type double, égal à la division de 360° par nb_div. Avec une boucle
for on parcourt la plage d’angle de 0° à 360° par pas de pas_angle. On calcule les
coordonnées des quatre sommets pt_a, pt_b, pt_c et pt_d de la facette (figure 5.26), de
type Point3D, par des coordonnées sphériques. Pour le calcul des sinus et cosinus, on
utilise les méthodes statiques Math.Sin et Math.Cos de la classe System.Math. L’angle
demandé par ces méthodes statiques est de type double pour représenter un angle
en radians. Donc il suffit de multiplier l’angle en degrés par Math.Pi/180 pour obtenir
l’angle en radians. Les quatre points obtenus sont ajoutés à la propriété Positions du
MeshGeometry.
FIGURE 5.26
Y
D +

A A D
face cachée

C face vue

φ
X
B B C
Z

//modeliser la geometrie de la face externe


private Geometry3D MaillageCylindreFaceExterne(double hauteur, double ray_ext,
int nb_div) {
MeshGeometry3D maillage = new MeshGeometry3D();
double pas_angle = 360d / nb_div;
for (double angle = 0; angle < 360; angle += pas_angle) {
Point3D pt_a = new Point3D();
Copyright 2013 Patrice REY

pt_a.X = ray_ext * Math.Cos(angle * Math.PI / 180);


pt_a.Y = hauteur;
pt_a.Z = -ray_ext * Math.Sin(angle * Math.PI / 180);
Point3D pt_b = new Point3D();
pt_b.X = ray_ext * Math.Cos(angle * Math.PI / 180);
pt_b.Y = 0;
pt_b.Z = -ray_ext * Math.Sin(angle * Math.PI / 180);
Point3D pt_c = new Point3D();
pt_c.X = ray_ext * Math.Cos((angle + pas_angle) * Math.PI / 180);
CHAPITRE 5 Les modèles 3D basiques 177

pt_c.Y = 0;
pt_c.Z = -ray_ext * Math.Sin((angle + pas_angle) * Math.PI / 180);
Point3D pt_d = new Point3D();
pt_d.X = ray_ext * Math.Cos((angle + pas_angle) * Math.PI / 180);
pt_d.Y = hauteur;
pt_d.Z = -ray_ext * Math.Sin((angle + pas_angle) * Math.PI / 180);
maillage.Positions.Add(pt_a);
maillage.Positions.Add(pt_b);
maillage.Positions.Add(pt_c);
maillage.Positions.Add(pt_d);
...
}
...
return maillage;
}
On utilise la méthode GenererTriangleIndice pour générer l’ordre des indices à utiliser
pour la suite des triangles (comme on l’a déjà vu pour le cube).
//generer les indices pour la suite de triangles
private void GenererTriangleIndice(MeshGeometry3D maillage) {
int depart = 0;
for (int xx = 0; xx < maillage.Positions.Count; xx += 4) {
maillage.TriangleIndices.Add(depart);
maillage.TriangleIndices.Add(depart + 1);
maillage.TriangleIndices.Add(depart + 2);
maillage.TriangleIndices.Add(depart + 2);
maillage.TriangleIndices.Add(depart + 3);
maillage.TriangleIndices.Add(depart);
depart += 4;
}
}
Pour générer le cylindre, on ajoute une méthode AjouterObjetCylindre, qui retourne
la modélisation du cylindre sous forme d’un ModelUIElement3D, et qui reçoit, pour
l’instant, en paramètre une hauteur de cylindre, un rayon extérieure et un nombre
de division. La facette cylindrique est composée de quatre facettes à générer. Il
s’agit donc d’une modélisation composite. On définit donc un ModelUIElement3D
cylindre dont sa propriété Model reçoit un ensemble de modélisations regroupées
dans un Model3DGroup gp_geo_cyl. Les quatre facettes seront modélisées par
un GeometryModel3D et elles seront ajoutées à la propriété Children du groupe
gp_geo_cyl. On procède ainsi car un GeometryModel3D possède une géométrie
(propriété Geometry), une texture de face avant (Material) et une texte de face arrière
(BackMaterial). Le repère 1 de la figure 5.27 visualise le résultat obtenu dans le cas d’un
nombre de division égal à 10 (une couleur verte est affectée à Material et une couleur
jaune est affectée à BackMaterial) et le repère 2 de la figure 5.27 montre la différence
178 Programmez des jeux vidéo 3D avec C#5 et WPF
de lissage obtenue entre un nombre de division de 10 et de 40. Avec 40 divisions, on
obtient un résultat très acceptable.
FIGURE 5.27

Y face externe avec


10 divisions

D
A 72°
1 soit 2 divisions
C
36°
pas_angle soit 1 division
Z
B

2
Copyright 2013 Patrice REY

face externe avec face externe avec


10 divisions 40 divisions
CHAPITRE 5 Les modèles 3D basiques 179

//modeliser un cylindre
private ModelUIElement3D AjouterObjetCylindre(double hauteur, double ray_ext,
int nb_div) {
ModelUIElement3D cylindre = new ModelUIElement3D();
Model3DGroup gp_geo_cyl = new Model3DGroup();
//modeliser face exterieure
GeometryModel3D geo_cyl_face_ext = new GeometryModel3D();
geo_cyl_face_ext.Geometry = MaillageCylindreFaceExterne(hauteur, ray_ext,
nb_div);
gp_geo_cyl.Children.Add(geo_cyl_face_ext);
geo_cyl_face_ext.Material = new DiffuseMaterial(Brushes.Green);
geo_cyl_face_ext.BackMaterial = new DiffuseMaterial(Brushes.Yellow);
//
cylindre.Model = gp_geo_cyl;
return cylindre;
}
Maintenant que la face extérieure est complètement modélisée, on va pouvoir
ajouter les coordonnées pour le placage de texture (propriété TextureCoordinates de
MeshGeometry3D).
Ici, nous allons appliquer une image sur le pourtour de l’extérieur du cylindre comme
le montre la figure 5.28. On a une ressource image stockée sous forme d’un fichier
(chapitre_05/image_texture/texture_cyl_ext_1.png). Cette image a une largeur et une
hauteur. Tous les triangles de la facette sont positionnés sur cette image pour effectuer
un relevé des coordonnées de texture. Comme on l’a vu pour le cube, les coordonnées
de texture sont exprimées en relatif entre 0 et 1 suivant l’axe X et l’axe Y de l’image 2D.
Avec un compteur qui stocke le nombre de triangles générés et en fonction du nombre
de division, on détermine les coins haut gauche, haut droit, bas gauche et bas droit
d’une facette composée de 4 sommets (pt_hg, pt_hd, pt_bg et pt_bd).
//modeliser la geometrie de la face externe
private Geometry3D MaillageCylindreFaceExterne(double hauteur, double ray_ext,
int nb_div) {
MeshGeometry3D maillage = new MeshGeometry3D();
double pas_angle = 360d / nb_div;
int cpt_facette = 0;
for (double angle = 0; angle < 360; angle += pas_angle) {
...
Point pt_hg = new Point((double)cpt_facette / (double)nb_div, 0);
Point pt_hd = new Point((double)(cpt_facette + 1) / (double)nb_div, 0);
Point pt_bg = new Point((double)cpt_facette / (double)nb_div, 1);
Point pt_bd = new Point((double)(cpt_facette + 1) / (double)nb_div, 1);
maillage.TextureCoordinates.Add(pt_hg);
maillage.TextureCoordinates.Add(pt_bg);
maillage.TextureCoordinates.Add(pt_bd);
maillage.TextureCoordinates.Add(pt_hd);
180 Programmez des jeux vidéo 3D avec C#5 et WPF
cpt_facette++;
}
GenererTriangleIndice(maillage);
return maillage;
}
FIGURE 5.28

(0,0) (1,0)

(0,1) (1,1)
A D

B C

Copyright 2013 Patrice REY


CHAPITRE 5 Les modèles 3D basiques 181

On procède de même pour modéliser la facette du dessus (facette HADE) par la


méthode MaillageCylindreFaceDessus qui reçoit en paramètre la hauteur du cylindre,
le rayon extérieur, le rayon intérieur et le nombre de division.
FIGURE 5.29
Y
D +
E
A
H
H E face cachée

C face vue

φ
X
B A D
Z

//modeliser la geometrie de la face du dessus


private Geometry3D MaillageCylindreFaceDessus(double hauteur, double ray_ext,
double ray_int, int nb_div) {
MeshGeometry3D maillage = new MeshGeometry3D();
double pas_angle = 360d / nb_div;
int cpt_facette = 0;
for (double angle = 0; angle < 360; angle += pas_angle) {
Point3D pt_h = new Point3D();
pt_h.X = ray_int * Math.Cos(angle * Math.PI / 180);
pt_h.Y = hauteur;
pt_h.Z = -ray_int * Math.Sin(angle * Math.PI / 180);
Point3D pt_a = new Point3D();
pt_a.X = ray_ext * Math.Cos(angle * Math.PI / 180);
pt_a.Y = hauteur;
pt_a.Z = -ray_ext * Math.Sin(angle * Math.PI / 180);
Point3D pt_d = new Point3D();
pt_d.X = ray_ext * Math.Cos((angle + pas_angle) * Math.PI / 180);
pt_d.Y = hauteur;
pt_d.Z = -ray_ext * Math.Sin((angle + pas_angle) * Math.PI / 180);
Point3D pt_e = new Point3D();
pt_e.X = ray_int * Math.Cos((angle + pas_angle) * Math.PI / 180);
pt_e.Y = hauteur;
pt_e.Z = -ray_int * Math.Sin((angle + pas_angle) * Math.PI / 180);
...
182 Programmez des jeux vidéo 3D avec C#5 et WPF
cpt_facette++;
}
//
GenererTriangleIndice(maillage);
return maillage;
}
Il en est de même pour la modélisation de la face du dessous (méthode
MaillageCylindreFaceDessous) calquée sur la face du dessus, et pour la modélisation de
la face intérieure (méthode MaillageCylindreFaceInterne) calquée sur la face extérieure.
Notre méthode AjouterObjetCylindre reçoit désormais en paramètre une hauteur
(double hauteur), un rayon extérieur (double ray_ext), un rayon intérieur (double ray_
int), un nombre de division (int nb_div), et 4 chemins qui pointent vers des ressources
image pour les textures.
//modeliser un cylindre
private ModelUIElement3D AjouterObjetCylindre(double hauteur, double ray_ext,
double ray_int, int nb_div, string chem_texture_externe,
string chem_texture_dessus, string chem_texture_dessous,
string chem_texture_interne) {
ModelUIElement3D cylindre = new ModelUIElement3D();
Model3DGroup gp_geo_cyl = new Model3DGroup();
//modeliser face exterieure
GeometryModel3D geo_cyl_face_ext = new GeometryModel3D();
geo_cyl_face_ext.Geometry = MaillageCylindreFaceExterne(hauteur, ray_ext,
nb_div);
gp_geo_cyl.Children.Add(geo_cyl_face_ext);
DiffuseMaterial diffus_ext = new DiffuseMaterial();
ImageBrush img_brush_ext = new ImageBrush();
img_brush_ext.ImageSource = new BitmapImage(
new Uri(«pack://application:,,/» + chem_texture_externe, UriKind.Absolute));
diffus_ext.Brush = img_brush_ext;
geo_cyl_face_ext.Material = diffus_ext;
geo_cyl_face_ext.BackMaterial = new DiffuseMaterial(Brushes.Yellow);
//modeliser face dessus
GeometryModel3D geo_cyl_face_dessus = new GeometryModel3D();
geo_cyl_face_dessus.Geometry = MaillageCylindreFaceDessus(hauteur, ray_ext,
ray_int, nb_div);
gp_geo_cyl.Children.Add(geo_cyl_face_dessus);
Copyright 2013 Patrice REY

DiffuseMaterial diffus_dessus = new DiffuseMaterial();


ImageBrush img_brush_dessus = new ImageBrush();
img_brush_dessus.ImageSource = new BitmapImage(
new Uri(«pack://application:,,/» + chem_texture_dessus, UriKind.Absolute));
diffus_dessus.Brush = img_brush_dessus;
geo_cyl_face_dessus.Material = diffus_dessus;
geo_cyl_face_dessus.BackMaterial = new DiffuseMaterial(Brushes.Yellow);
//modeliser face dessous
GeometryModel3D geo_cyl_face_dessous = new GeometryModel3D();
CHAPITRE 5 Les modèles 3D basiques 183

geo_cyl_face_dessous.Geometry = MaillageCylindreFaceDessous(ray_ext, ray_int,


nb_div);
gp_geo_cyl.Children.Add(geo_cyl_face_dessous);
DiffuseMaterial diffus_dessous = new DiffuseMaterial();
ImageBrush img_brush_dessous = new ImageBrush();
img_brush_dessous.ImageSource = new BitmapImage(
new Uri(«pack://application:,,/» + chem_texture_dessous, UriKind.Absolute));
diffus_dessous.Brush = img_brush_dessous;
geo_cyl_face_dessous.Material = diffus_dessous;
geo_cyl_face_dessous.BackMaterial = new DiffuseMaterial(Brushes.Yellow);
//modeliser face interieure
GeometryModel3D geo_cyl_face_int = new GeometryModel3D();
geo_cyl_face_int.Geometry = MaillageCylindreFaceInterne(hauteur, ray_int,
nb_div);
gp_geo_cyl.Children.Add(geo_cyl_face_int);
DiffuseMaterial diffus_int = new DiffuseMaterial();
ImageBrush img_brush_int = new ImageBrush();
img_brush_int.ImageSource = new BitmapImage(
new Uri(«pack://application:,,/» + chem_texture_interne, UriKind.Absolute));
diffus_int.Brush = img_brush_int;
geo_cyl_face_int.Material = diffus_int;
geo_cyl_face_int.BackMaterial = new DiffuseMaterial(Brushes.Yellow);
//
cylindre.Model = gp_geo_cyl;
return cylindre;
}
L’UserControl ModeleCylindre.xaml, dans le dossier chapitre_05, visualise un cylindre
dont la face extérieure, la face intérieure, la face du dessus et la face du dessous, sont
texturées avec des ressources image. Le cylindre creux est positionné au centre du
repère de coordonnées sur un plan de travail (figure 5.30).
Les axes du repère sont colorés en rouge pour l’axe X, bleu pour l’axe Y et vert pour
l’axe Z. Le plan de travail, qui symbolise le sol, est représenté par une grille. Les trois
glissières permettent d’effectuer la rotation de la caméra autour des trois axes. Un
bouton permet de remettre la vue de la scène à son état d’origine. Deux cases à cocher
permettent, en fonction de leur état (coché ou décoché), d’afficher ou de masquer
le repère des coordonnées et le plan de travail, d’une façon individuelle. Un contrôle
de type TreeView permet d’afficher une arborescence de données dans laquelle on
recense les objets 3D contenus dans la scène. La scène est, quant à elle, illuminée au
moyen d’un éclairage ambiant. La face extérieure du cylindre est texturée avec une
image panoramique qui représente Manhattan. Les faces du dessus et du dessous sont
texturées avec des images qui représentent des textures de bois. La face intérieure du
cylindre creux est texturée avec une image qui représente un motif.
184 Programmez des jeux vidéo 3D avec C#5 et WPF
FIGURE 5.30

Copyright 2013 Patrice REY


CHAPITRE 5 Les modèles 3D basiques 185

3.4 - Les formes polygonales

La modélisation que nous avons utilisé pour définir un cylindre nous permet de réaliser
d’autres volumes comme ceux qui ont un pourtour avec une forme polygonale. En effet,
dans la modélisation du cylindre, on utilise un nombre de division pour augmenter le
nombre de facettes extérieures dans le but d’obtenir un lissage le plus parfait possible.
Or, si on utilise un petit nombre de division, on obtient un pourtour dont la forme est
polygonale. Par exemple un nombre de division égal à 5 donne un pourtour doté d’une
forme de pentagone (5 côtés). Avec 6, ce sera une forme d’hexagone, avec 7 une forme
d’heptagone, avec 8 une forme d’octogone, etc.
L’UserControl EnsemblePolygone.xaml, dans le dossier chapitre_05, visualise un
ensemble de 4 volumes (figure 5.31) dont un avec le pourtour en forme de pentagone,
un avec le pourtour en forme d’hexagone, un avec le pourtour en forme d’heptagone
et un avec le pourtour en forme d’octogone. Ces 4 volumes sont texturés sur chacun de
leur côté comme on l’a fait avec le cylindre (ce sont des volumes correspondant à des
«cylindres particuliers»). La figure 5.31 visualise la scène avec les 4 volumes.

pentagone hexagone
(5 côtés) (6 côtés)

heptagone
(7 côtés) octogone
(8 côtés)
186 Programmez des jeux vidéo 3D avec C#5 et WPF
FIGURE 5.31

pentagone hexagone

octogone heptagone

Copyright 2013 Patrice REY


CHAPITRE 5 Les modèles 3D basiques 187

Quatre objets ModelUIElement3D pentagone, hexagone, heptagone et octogone sont


instanciés par la méthode AjouterObjetCylindre qui reçoit un nombre de division de 5
pour le pentagone, 6 pour l’hexagone, 7 pour l’heptagone et 8 pour l’octogone. Pour
chacun des polygones on leur donne un nom de référence par la méthode SetValue
concernant la propriété de dépendance NameProperty.
//objets pentagone regulier
ModelUIElement3D pentagone = AjouterObjetCylindre(0.5d, 0.7d, 0.5d, 5,
«chapitre_05/image_texture/texture_polygone_cote.png»,
«chapitre_05/image_texture/texture_polygone_dessus_dessous.jpg»,
«chapitre_05/image_texture/texture_polygone_dessus_dessous.jpg»,
«chapitre_05/image_texture/texture_polygone_cote.png»);
pentagone.SetValue(NameProperty, «x_pentagone»);
x_conteneur_3d.Children.Add(pentagone);
Transform3DGroup pentagone_trans = new Transform3DGroup();
pentagone_trans.Children.Add(new RotateTransform3D(
new AxisAngleRotation3D(new Vector3D(0, 1, 0), 18)));
pentagone_trans.Children.Add(new TranslateTransform3D(-1.5d, 0d, 0d));
pentagone.Transform = pentagone_trans;
//objets hexagone regulier
ModelUIElement3D hexagone = AjouterObjetCylindre(0.5d, 0.7d, 0.5d, 6,
«chapitre_05/image_texture/texture_polygone_cote.png»,
«chapitre_05/image_texture/texture_polygone_dessus_dessous.jpg»,
«chapitre_05/image_texture/texture_polygone_dessus_dessous.jpg»,
«chapitre_05/image_texture/texture_polygone_cote.png»);
hexagone.SetValue(NameProperty, «x_hexagone»);
x_conteneur_3d.Children.Add(hexagone);
Transform3DGroup hexagone_trans = new Transform3DGroup();
hexagone_trans.Children.Add(new TranslateTransform3D(0d, 0d, -1.5d));
hexagone.Transform = hexagone_trans;
//objets heptagone regulier
ModelUIElement3D heptagone = AjouterObjetCylindre(0.5d, 0.7d, 0.5d, 7,
«chapitre_05/image_texture/texture_polygone_cote.png»,
«chapitre_05/image_texture/texture_polygone_dessus_dessous.jpg»,
«chapitre_05/image_texture/texture_polygone_dessus_dessous.jpg»,
«chapitre_05/image_texture/texture_polygone_cote.png»);
heptagone.SetValue(NameProperty, «x_heptagone»);
x_conteneur_3d.Children.Add(heptagone);
//objets octogone regulier
ModelUIElement3D octogone = AjouterObjetCylindre(0.5d, 0.7d, 0.5d, 8,
«chapitre_05/image_texture/texture_polygone_cote.png»,
«chapitre_05/image_texture/texture_polygone_dessus_dessous.jpg»,
«chapitre_05/image_texture/texture_polygone_dessus_dessous.jpg»,
«chapitre_05/image_texture/texture_polygone_cote.png»);
octogone.SetValue(NameProperty, «x_octogone»);
x_conteneur_3d.Children.Add(octogone);
Transform3DGroup octogone_trans = new Transform3DGroup();
octogone_trans.Children.Add(new TranslateTransform3D(0d, 0d, 1.5d));
188 Programmez des jeux vidéo 3D avec C#5 et WPF

4 - La modélisation du cône

Le cône est un solide formé par l’intersection d’une surface conique avec un plan
quelconque. Une surface conique est engendrée par une droite (la génératrice) qui se
déplace le long d’une courbe (la directrice) et qui passe toujours par un point fixe (le
sommet du cône). La figure ci-dessous montre la visualisation géométrique du cône.
Y

Rb = rayon du cercle bas


Rh = rayon du cercle haut
point A:
xa = Rh * cos(φ)
A ya = H
za = -Rh * sin(φ)

point B:
xb = Rb * cos(φ) = xa
yb = 0
zb = -Rb * sin(φ) = za
B
za
O φ
X
xa

Un point B qui appartient à la base du cône a comme coordonnées (avec Rb le rayon


du cercle de la base):
xa = Rb * cos(φ)
ya = 0
Copyright 2013 Patrice REY

za = -Rb * sin(φ)
Un point A qui représente le sommet du cône dans le cas du cône circulaire droit, a
comme coordonnées (avec H la hauteur du cône):
xb = 0
yb = H
zb = 0
CHAPITRE 5 Les modèles 3D basiques 189

Un point A, dans le cas du cône circulaire tronqué, a comme coordonnées (avec H la


hauteur du cône, et Rh le rayon du cercle du sommet):
xb = Rh*cos(φ)
yb = H
zb = -Rh*sin(φ)
Y

D
E
A
Rb = rayon du cercle bas
Rh = rayon du cercle haut

C
O B
X

Z
On s’aperçoit donc qu’il va falloir générer une facette rectangulaire (ABCD) et deux
facettes triangulaires, (EAD) et (OBC), dans le cas du cône circulaire tronqué (le cône
circulaire droit étant une exception avec le rayon du cercle du sommet de valeur nulle).

Y Rb = rayon du cercle bas


Rh = rayon du cercle haut
Y D
D
E E A
A
Rh

C
O B
X C
O B X
Z
Z Rb
190 Programmez des jeux vidéo 3D avec C#5 et WPF
La méthode MaillageConeFaceExterne permet de générer le maillage de la face externe
du cône en fonction d’une hauteur, d’un rayon du cercle de la base, d’un rayon du
cercle du sommet (0 si le cône n’est pas tronqué), et un nombre de division. On utilise
rigoureusement la même façon de faire que pour la face extérieure du cylindre, y
compris pour le calcul des coordonnées de texture.
//modeliser la geometrie de la face externe
private Geometry3D MaillageConeFaceExterne(double hauteur, double ray_base,
double ray_sup, int nb_div) {
MeshGeometry3D maillage = new MeshGeometry3D();
double pas_angle = 360d / nb_div;
int cpt_facette = 0;
for (double angle = 0; angle < 360; angle += pas_angle) {
Point3D pt_a = new Point3D();
pt_a.X = ray_sup * Math.Cos(angle * Math.PI / 180);
pt_a.Y = hauteur;
pt_a.Z = -ray_sup * Math.Sin(angle * Math.PI / 180);
Point3D pt_b = new Point3D();
pt_b.X = ray_base * Math.Cos(angle * Math.PI / 180);
pt_b.Y = 0;
pt_b.Z = -ray_base * Math.Sin(angle * Math.PI / 180);
Point3D pt_c = new Point3D();
pt_c.X = ray_base * Math.Cos((angle + pas_angle) * Math.PI / 180);
pt_c.Y = 0;
pt_c.Z = -ray_base * Math.Sin((angle + pas_angle) * Math.PI / 180);
Point3D pt_d = new Point3D();
pt_d.X = ray_sup * Math.Cos((angle + pas_angle) * Math.PI / 180);
pt_d.Y = hauteur;
pt_d.Z = -ray_sup * Math.Sin((angle + pas_angle) * Math.PI / 180);
maillage.Positions.Add(pt_a);
maillage.Positions.Add(pt_b);
maillage.Positions.Add(pt_c);
maillage.Positions.Add(pt_d);
Point pt_hg = new Point((double)cpt_facette / (double)nb_div, 0);
Point pt_hd = new Point((double)(cpt_facette + 1) / (double)nb_div, 0);
Point pt_bg = new Point((double)cpt_facette / (double)nb_div, 1);
Point pt_bd = new Point((double)(cpt_facette + 1) / (double)nb_div, 1);
maillage.TextureCoordinates.Add(pt_hg);
maillage.TextureCoordinates.Add(pt_bg);
maillage.TextureCoordinates.Add(pt_bd);
Copyright 2013 Patrice REY

maillage.TextureCoordinates.Add(pt_hd);
cpt_facette++;
}
GenererTriangleIndice(maillage);
return maillage;
}
Les méthodes MaillageConeFaceDessus et MaillageConeFaceDessous génèrent le
maillage de la face du dessus (cas du cône tronqué) et de la face du dessous (la base).
CHAPITRE 5 Les modèles 3D basiques 191

Elles consistent à générer un ensemble de triangles collés les uns à côté des autres par
une de leurs arêtes. Ne pas oublier d’ajouter une méthode GenererIndiceFacetteTriangle
pour générer les indices quand on passe un ensemble de facettes triangulaires.
//modeliser la geometrie de la face du dessus
private Geometry3D MaillageConeFaceDessus(double hauteur, double ray_sup,
int nb_div) {
MeshGeometry3D maillage = new MeshGeometry3D();
double pas_angle = 360d / nb_div;
int cpt_facette = 0;
for (double angle = 0; angle < 360; angle += pas_angle) {
Point3D pt_haut = new Point3D();
pt_haut.X = 0;
pt_haut.Y = hauteur;
pt_haut.Z = 0;
Point3D pt_a = new Point3D();
pt_a.X = ray_sup * Math.Cos(angle * Math.PI / 180);
pt_a.Y = hauteur;
pt_a.Z = -ray_sup * Math.Sin(angle * Math.PI / 180);
Point3D pt_d = new Point3D();
pt_d.X = ray_sup * Math.Cos((angle + pas_angle) * Math.PI / 180);
pt_d.Y = hauteur;
pt_d.Z = -ray_sup * Math.Sin((angle + pas_angle) * Math.PI / 180);
maillage.Positions.Add(pt_haut);
maillage.Positions.Add(pt_a);
maillage.Positions.Add(pt_d);
Point pt_centre = new Point(0.5, 0.5);
Point pt_centre_g = new Point(0,1);
Point pt_centre_d = new Point(1,1);
maillage.TextureCoordinates.Add(pt_centre);
maillage.TextureCoordinates.Add(pt_centre_g);
maillage.TextureCoordinates.Add(pt_centre_d);
cpt_facette++;
}
//
GenererIndiceFacetteTriangle(maillage);
return maillage;
}
//generer les indices pour la suite de triangles
private void GenererIndiceFacetteTriangle(MeshGeometry3D maillage) {
int depart = 0;
for (int xx = 0; xx < maillage.Positions.Count; xx += 3) {
maillage.TriangleIndices.Add(depart);
maillage.TriangleIndices.Add(depart + 1);
maillage.TriangleIndices.Add(depart + 2);
depart += 3;
}
}
192 Programmez des jeux vidéo 3D avec C#5 et WPF
La figure 5.32 visualise la géométrie du cône dans le cas d’un cône tronquée avec un
nombre de division égal à 10. Y
FIGURE 5.32

nombre de division = 10

D
A

36°
soit 1 division
Z B
X

face du dessus face du dessous

C
D
Copyright 2013 Patrice REY

pt_Bas
pt_Haut

A B
CHAPITRE 5 Les modèles 3D basiques 193

L’UserControl ModeleCone.xaml, dans le dossier chapitre_05, visualise un ensemble de


deux cônes positionnés l’un sur l’autre (figure 5.33). Le premier cône (celui du bas) est
un cône circulaire tronqué avec une face externe texturée par une image représentant
la texture du bois, et les faces du dessus et du dessous (la base) texturées par une
image représentant un motif bleu. On lui affecte un nombre de division égal à 10.
Le second cône (celui positionné sur la face du dessus du premier cône) est un cône
circulaire dont la face externe est texturée par application d’un image représentant
une vue sur les immeubles de Manhattan à New-York. On lui affecte un nombre de
division égal à 40, ce qui lui donne un aspect lissé pour sa face externe.
On applique au second cône (ModelUIElment3D cone) une transformation de
translation pour le positionner sur le premier cône.
//objets cone tronque
ModelUIElement3D cone_tronque = AjouterObjetCone(0.5d, 1.85d, 1.25d, 10,
«chapitre_05/image_texture/texture_cone_cote_1.png»,
«chapitre_05/image_texture/texture_cone_dessus_1.png»,
«chapitre_05/image_texture/texture_cone_dessus_1.png»);
cone_tronque.SetValue(NameProperty, «x_cone_tronque»);
x_conteneur_3d.Children.Add(cone_tronque);
Transform3DGroup cone_tronque_trans = new Transform3DGroup();
cone_tronque_trans.Children.Add(new TranslateTransform3D(0d, 0d, 0d));
cone_tronque.Transform = cone_tronque_trans;
//objets cone
ModelUIElement3D cone = AjouterObjetCone(1d, 1d, 0d, 40,
«chapitre_05/image_texture/texture_cone_cote_2.png»,
«chapitre_05/image_texture/texture_cone_dessus_1.png»,
«chapitre_05/image_texture/texture_cone_dessus_1.png»);
cone.SetValue(NameProperty, «x_cone»);
x_conteneur_3d.Children.Add(cone);
Transform3DGroup cone_trans = new Transform3DGroup();
cone_trans.Children.Add(new TranslateTransform3D(0d, 0.5d, 0d));
cone.Transform = cone_trans;
Le ModelUIElement3D cone est composé d’un groupe Model3DGroup pour
l’assemblage de la face externe, la face du dessous et la face du dessus.
ModelUIElement3D cone = new ModelUIElement3D();
Model3DGroup gp_geo_cone = new Model3DGroup();
//modeliser face exterieure
GeometryModel3D geo_cone_face_ext = new GeometryModel3D();
geo_cone_face_ext.Geometry = MaillageConeFaceExterne(hauteur, ray_base, ray_sup,
nb_div);
gp_geo_cone.Children.Add(geo_cone_face_ext); ...
//modeliser face dessus
GeometryModel3D geo_cone_face_dessus = new GeometryModel3D();
geo_cone_face_dessus.Geometry = MaillageConeFaceDessus(hauteur, ray_sup, nb_div);
gp_geo_cone.Children.Add(geo_cone_face_dessus); ...
194 Programmez des jeux vidéo 3D avec C#5 et WPF

FIGURE 5.33

Copyright 2013 Patrice REY


CHAPITRE 5 Les modèles 3D basiques 195

//modeliser face dessous


GeometryModel3D geo_cone_face_dessous = new GeometryModel3D();
geo_cone_face_dessous.Geometry = MaillageConeFaceDessous(hauteur, ray_base,
nb_div);
gp_geo_cone.Children.Add(geo_cone_face_dessous); ...
//
cone.Model = gp_geo_cone;

5 - La modélisation de la sphère

Une sphère est composée de facettes triangulaires qui sont localisées dans l’espace
3D et assemblées entre elles. Les sommets des triangles doivent être localisés par
rapport au centre de la sphère. Tous les points d’une sphère ont une caractéristique
unique, c’est qu’ils se trouvent à une distance fixe d’un centre. Les points d’une sphère
appartiennent à une surface qui se trouve à une distance R d’un centre 0 (R est le rayon
de la sphère et O est le centre de la sphère).
Les coordonnées cartésiennes d’un point M s’expriment par trois coordonnées (x,y,z)
selon les 3 axes. Or, pour balayer selon des angles à une distance R, en vue de l’obtention
de coordonnées, il va falloir exprimer (x,y,z) en coordonnées sphériques (comme on l’a
vu en préalable au paragraphe concernant le cylindre).
Comme le montre géométriquement le haut de la figure 5.34, il y a une infinité de point
M de coordonnées (x,y,z) qui sont positionnés à une distance R du centre O. Dans le
plan (OX,OZ), l’ensemble des points M à une distance R (les points B et C par exemple)
correspond au balayage de l’angle ϕ quand celui-ci varie de 0 à 2π (avec l’angle β égal
à 0). Dans un plan quelconque, l’ensemble des points M à une distance R (les points A
et D par exemple) correspond au balayage de l’angle ϕ quand celui-ci varie de 0 à 2π,
avec le balayage de l’angle β qui varie de -π/2 à +π/2.
On s’aperçoit donc que l’on peut construire des facettes, comme celle dont les coins
sont A, B, C et D. Et par conséquent, on peut définir un nombre de facettes à la fois sur le
plan horizontal et sur le plan vertical. Un nombre identique de facettes horizontalement
et verticalement, conduira à avoir des facettes généralement de forme carré, sauf à
l’approche des pôles de la sphère. Un nombre important de facettes, horizontalement
et verticalement, permettra une meilleure définition de la sphère.
Le schéma géométrique du bas de la figure 5.34 nous permet de définir les coordonnées
sphériques d’un point M quelconque appartenant à la sphère. Soit un point M
appartenant à la sphère, et par conséquent, se trouvant à une distance R du centre O.
On effectue les projections orthogonales du point M sur les axes (X,Y,Z).
196 Programmez des jeux vidéo 3D avec C#5 et WPF

FIGURE 5.34

D
A

β
C
O φ X
B

Y
M(x,y,z)
ym points M:
xm = OP * cos(φ) = R * cos(β) * cos(φ)
R ym = R * sin(β)
zm = -OP * sin(φ) = -R * cos(β) * sin(φ)

zm P(x,0,z)
β
Copyright 2013 Patrice REY

O φ
xm
X

Z
CHAPITRE 5 Les modèles 3D basiques 197

Cela conduit à déterminer les coordonnées du point M qui sont xm, ym et zm:
• dans le triangle (O,P,xm), rectangle en xm, on a xm = OP * cos(ϕ); dans le triangle
(O,M,P), rectangle en P, on a la distance OP = R * cos(β); on en déduit donc que la
coordonnée xm = OP * cos(ϕ) = R * cos(β) * cos(ϕ).
• dans le triangle (O,M,P), rectangle en P, on a la distance MP = R * sin(β); comme la
distance MP est égale justement à ym, on a donc ym = R * sin(β).
• dans le triangle (O,P,xm), rectangle en xm, on a la distance Pxm = zm = -OP * sin(ϕ);
comme OP = R * cos(β), on en déduit que zm = -R * cos(β) * sin(β).
Ainsi on a pu exprimer les coordonnées cartésiennes (xm,ym,zm) en fonction du triplet
(R,ϕ,β). Ce triplet constitue les coordonnées sphériques du point M.
La modélisation sphérique consiste à générer les facettes de la sphère, et en particulier,
à calculer les coordonnées sphériques de quatre points qui constituent les coins de
la facette. Chaque facette, dont les points sont A, B, C et D, est composée de deux
triangles avec le premier triangle (A,B,C) et le deuxième triangle (C,D,A). Comme on
l’a déjà vu, on utilise cet ordre, dans le sens contraire des aiguilles d’une montre, pour
signifier que c’est la face que l’on voit et qui est texturée (celle de devant). La figure
ci-dessous montre les points A, B, C et D, pour une facette avec une division de l’angle
ϕ et une division de l’angle β (avec un nombre de division égal à 4, horizontalement et
verticalement, la portion de la sphère est générée quand l’angle ϕ varie de 0 à π/2 et
l’angle β varie de -π/2 à +π/2).
Y
division verticale = 4
division horizontale = 4

A
C
β

Z B
φ

X
198 Programmez des jeux vidéo 3D avec C#5 et WPF
La figure ci-dessous montre cette portion de sphère générée sous un autre angle de
visualisation.

X
L’UserControl ModeleSphere.xaml, dans le dossier chapitre_05, visualise une sphère
texturée avec une ressource image (figure 5.35). Un nombre de division égal à 60 est
utilisé horizontalement et verticalement, ce qui permet d’avoir un aspect lissé très
correcte de la surface sphérique. L’image qui est appliquée comme placage de texture
est composée dans sa partie haute par la représentation d’une vue panoramique de
Manhattan, et dans sa partie basse par la représentation d’un damier.
La méthode MaillageSphereFace permet d’élaborer le maillage de la surface sphérique
en fonction d’un rayon sphérique rayon, d’un nombre de division horizontale nb_
div_hori et d’un nombre de division verticale nb_div_verti. La variable pas_angle_phi
représente le pas de l’angle φ selon le plan horizontal (entre 0° et 360°), et la variable
pas_angle_beta représente le pas de l’angle β selon le plan vertical (entre -90° et +90°).
Deux boucles for imbriquées permettent de parcourir les valeurs de β pour un angle φ
Copyright 2013 Patrice REY

donné.
double pas_angle_phi = 360d / nb_div_hori;
double pas_angle_beta = 180d / nb_div_verti;
for (double angle_phi = 0; angle_phi < 360; angle_phi += pas_angle_phi) {
for (double angle_beta = 90; angle_beta > -90; angle_beta -= pas_angle_beta) {
...
}}
CHAPITRE 5 Les modèles 3D basiques 199

FIGURE 5.35
200 Programmez des jeux vidéo 3D avec C#5 et WPF
On définit les coordonnées sphériques des points pt_a, pt_b, pt_c et pt_d, de type
Point3D, en fonction des angles φ et β, et on les ajoute à la propriété Positions.

Y M(x,y,z)
ym points M:
xm = OP * cos(φ) = R * cos(β) * cos(φ)
R ym = R * sin(β)
zm = -OP * sin(φ) = -R * cos(β) * sin(φ)

zm P(x,0,z)
β
O φ
xm
X
Z
for (double angle_phi = 0; angle_phi < 360; angle_phi += pas_angle_phi) {
for (double angle_beta = 90; angle_beta > -90; angle_beta -= pas_angle_beta) {
Point3D pt_a = new Point3D();
pt_a.X = rayon * Math.Cos(angle_beta * Math.PI / 180)
* Math.Cos(angle_phi * Math.PI / 180);
pt_a.Y = rayon * Math.Sin(angle_beta * Math.PI / 180);
pt_a.Z = -rayon * Math.Cos(angle_beta * Math.PI / 180)
* Math.Sin(angle_phi * Math.PI / 180);
Point3D pt_b = new Point3D();
pt_b.X = rayon * Math.Cos((angle_beta - pas_angle_beta)
* Math.PI / 180) * Math.Cos(angle_phi * Math.PI / 180);
pt_b.Y = rayon * Math.Sin((angle_beta - pas_angle_beta) * Math.PI / 180);
pt_b.Z = -rayon * Math.Cos((angle_beta - pas_angle_beta) * Math.PI / 180)
* Math.Sin(angle_phi * Math.PI / 180);
Point3D pt_c = new Point3D();
pt_c.X = rayon * Math.Cos((angle_beta - pas_angle_beta) * Math.PI / 180)
* Math.Cos((angle_phi + pas_angle_phi) * Math.PI / 180);
pt_c.Y = rayon * Math.Sin((angle_beta - pas_angle_beta) * Math.PI / 180);
pt_c.Z = -rayon * Math.Cos((angle_beta - pas_angle_beta) * Math.PI / 180)
* Math.Sin((angle_phi + pas_angle_phi) * Math.PI / 180);
Point3D pt_d = new Point3D();
pt_d.X = rayon * Math.Cos(angle_beta * Math.PI / 180)
Copyright 2013 Patrice REY

* Math.Cos((angle_phi + pas_angle_phi) * Math.PI / 180);


pt_d.Y = rayon * Math.Sin(angle_beta * Math.PI / 180);
pt_d.Z = -rayon * Math.Cos(angle_beta * Math.PI / 180)
* Math.Sin((angle_phi + pas_angle_phi) * Math.PI / 180);
maillage.Positions.Add(pt_a);
maillage.Positions.Add(pt_b);
maillage.Positions.Add(pt_c);
maillage.Positions.Add(pt_d); ... }}
CHAPITRE 5 Les modèles 3D basiques 201

Il faut déterminer maintenant les coordonnées de placage de texture à ajouter à la


propriété TextureCoordinates de MeshGeometry3D. La figure ci-dessous montre, pour
un angle φ donné, les quatres facettes obtenues en faisant varier β dans le cas d’un
nombre de division verticale égal à 4. Il faut exprimer les coordonnées de texture en
relatif (entre 0 et 1) dans un plan 2D. En utilisant les variables cpt_div_h et cpt_div_v,
qui représentent une division donnée à un moment donné dans le sens horizontal
et vertical respectivement, on peut exprimer les coordonnées 2D de la texture par
les sommets pt_hg (haut gauche), pt_bg (bas gauche), pt_bd (bas droit) et pt_hd (haut
droit):
• pt_hg avec (cpt_div_h / nb_div_hori , cpt_div_v / nb_div_verti)
• pt_bg avec (cpt_div_h / nb_div_hori , cpt_div_v + 1 / nb_div_verti)
• pt_bd avec (cpt_div_h + 1 / nb_div_hori , cpt_div_v + 1 / nb_div_verti)
• Z pt_hd avec (cpt_div_h + 1 / nb_div_hori , cpt_div_v / nb_div_verti)

0,0 (1/nb_div_h , 0) 1,0


pt_hd
pt_hg
pt_bd
pt_bg

0,1
(1/nb_div_h , 1/nb_div_v) 1,1

int cpt_div_v = 0;
int cpt_div_h = 0;
for (double angle_phi = 0; angle_phi < 360; angle_phi += pas_angle_phi) {
for (double angle_beta = 90; angle_beta > -90; angle_beta -= pas_angle_beta) {
...
Point pt_hg = new Point((double)cpt_div_h / (double)nb_div_hori,
(double)cpt_div_v / (double)nb_div_verti);
Point pt_bg = new Point((double)cpt_div_h / (double)nb_div_hori,
(double)(cpt_div_v + 1) / (double)nb_div_verti);
Point pt_bd = new Point((double)(cpt_div_h + 1) / (double)nb_div_hori,
(double)(cpt_div_v + 1) / (double)nb_div_verti);
Point pt_hd = new Point((double)(cpt_div_h + 1) / (double)nb_div_hori,
(double)cpt_div_v / (double)nb_div_verti);
maillage.TextureCoordinates.Add(pt_hg);
maillage.TextureCoordinates.Add(pt_bg);
202 Programmez des jeux vidéo 3D avec C#5 et WPF
maillage.TextureCoordinates.Add(pt_bd);
maillage.TextureCoordinates.Add(pt_hd);
cpt_div_v++;
}
cpt_div_v = 0;
cpt_div_h++;
}
La méthode AjouterObjetSphere permet d’ajouter une sphère sur la scène. La figure 5.35
représentait une sphère de rayon 1.5 unité avec 60 divisions horizontales et verticales.
//objets sphere
ModelUIElement3D sphere_1 = AjouterObjetSphere(1.5d, 60, 60,
«chapitre_05/image_texture/texture_sphere_vue.png»);
sphere_1.SetValue(NameProperty, «x_sphere_1»);
x_conteneur_3d.Children.Add(sphere_1);
Transform3DGroup sphere_1_trans = new Transform3DGroup();
sphere_1_trans.Children.Add(new TranslateTransform3D(0d, 0d, 0d));
sphere_1.Transform = sphere_1_trans;
La modélisation de l’ellipsoïde (figure 5.36) se déduit très facilement de celle de la
sphère. En effet, il suffit de distinguer le rayon sphérique global en deux rayons: un rayon
selon l’horizontal et un rayon selon la verticale. La figure 5.36 montre une ellipsoïde,
avec un rayon vertical supérieur au rayon horizontal, sur laquelle on a appliqué une
texture composée d’un motif.
La méthode MaillageSphereFace est remplacée par la méthode MaillageEllipsoideFace,
qui reçoit en paramètre un rayon selon l’axe horizontal (rayon_hori), un rayon selon
l’axe vertical (rayon_verti), un nombre de division selon l’axe horizontal (nb_div_hori) et
un nombre de division selon la verticale (nb_div_verti).
for (double angle_phi = 0; angle_phi < 360; angle_phi += pas_angle_phi) {
for (double angle_beta = 90; angle_beta > -90; angle_beta -= pas_angle_beta) {
Point3D pt_a = new Point3D();
pt_a.X = rayon_hori * Math.Cos(angle_beta * Math.PI / 180)
* Math.Cos(angle_phi * Math.PI / 180);
pt_a.Y = rayon_verti * Math.Sin(angle_beta * Math.PI / 180);
pt_a.Z = -rayon_hori * Math.Cos(angle_beta * Math.PI / 180)
* Math.Sin(angle_phi * Math.PI / 180);
Point3D pt_b = new Point3D();
Copyright 2013 Patrice REY

pt_b.X = rayon_hori * Math.Cos((angle_beta - pas_angle_beta) * Math.PI / 180)


* Math.Cos(angle_phi * Math.PI / 180);
pt_b.Y = rayon_verti * Math.Sin((angle_beta - pas_angle_beta) * Math.PI / 180);
pt_b.Z = -rayon_hori * Math.Cos((angle_beta - pas_angle_beta) * Math.PI / 180)
* Math.Sin(angle_phi * Math.PI / 180);
Point3D pt_c = new Point3D();
pt_c.X = rayon_hori * Math.Cos((angle_beta - pas_angle_beta) * Math.PI / 180)
* Math.Cos((angle_phi + pas_angle_phi) * Math.PI / 180);
pt_c.Y = rayon_verti * Math.Sin((angle_beta - pas_angle_beta) * Math.PI / 180);
CHAPITRE 5 Les modèles 3D basiques 203

FIGURE 5.36
204 Programmez des jeux vidéo 3D avec C#5 et WPF
pt_c.Z = -rayon_hori * Math.Cos((angle_beta - pas_angle_beta) * Math.PI / 180)
* Math.Sin((angle_phi + pas_angle_phi) * Math.PI / 180);
Point3D pt_d = new Point3D();
pt_d.X = rayon_hori * Math.Cos(angle_beta * Math.PI / 180)
* Math.Cos((angle_phi + pas_angle_phi) * Math.PI / 180);
pt_d.Y = rayon_verti * Math.Sin(angle_beta * Math.PI / 180);
pt_d.Z = -rayon_hori * Math.Cos(angle_beta * Math.PI / 180)
* Math.Sin((angle_phi + pas_angle_phi) * Math.PI / 180);
...
}}
L’ellipsoïde de la figure 5.36 possède un rayon horizontal de 1 unité, un rayon vertical
de 1.5 unité et un nombre de division de 60 horizontalement et verticalement.
ModelUIElement3D ellipsoide_1 = AjouterObjetEllipsoide(1d,1.5d, 60, 60,
«chapitre_05/image_texture/texture_ellipsoide_1.png»);
ellipsoide_1.SetValue(NameProperty, «x_ellipsoide_1»);
x_conteneur_3d.Children.Add(ellipsoide_1);
Transform3DGroup ellipsoide_1_trans = new Transform3DGroup();
ellipsoide_1_trans.Children.Add(new TranslateTransform3D(0d, 0d, 0d));
ellipsoide_1.Transform = ellipsoide_1_trans;

6 - La modélisation du tore

Le tore est un solide engendré par la rotation d’un cercle autour d’une droite. Nous
allons voir la modélisation du tore avec notamment le tore dit «ouvert», le tore dit «à
collier nul» et le tore dit «croisé».
Le tore désigne un solide géométrique de l’espace euclidien 3D engendré par la
rotation d’un cercle C de rayon rt autour d’une droite affine D située dans son plan à
une distance Dr de son centre.
Si Dr est inférieur à rt, on est en présence d’un tore dit «croisé» (le tore ressemble
visuellement à une citrouille). Si Dr est égal à rt, on est en présence d’un tore dit «à
collier nul». Si Dr est supérieur à rt, on est en présence d’un tore dit «ouvert» (le tore
ressemble à une chambre à air). La figure 5.37 visualise géométriquement la réalisation
d’un tore dit «ouvert».
Un point A sur le tore sera localisé par ses coordonnées sphériques suivantes (avec Dr
Copyright 2013 Patrice REY

la distance de l’axe Y au centre du cercle, et rt le rayon du cercle):


xa = ( Dr + rt * cos(β) ) * cos(ϕ)
ya = rt * sin(β)
za = -( Dr + rt * cos(β) ) * sin(ϕ)
Intuitivement, on en déduit que l’on va devoir générer des facettes rectangulaires avec
quatre points.
CHAPITRE 5 Les modèles 3D basiques 205

FIGURE 5.37 Y
point A:
xa = (Dr + rt * cos(β)) * cos(φ)
ya = rt * sin(β)
za = -(Dr + rt * cos(β)) * sin(φ)

A
rt
O1 φ β
X
O2

Z Dr

Comme le schématise la figure 5.38, on va générer des facettes rectangulaires en


fonction d’un nombre de division (angle ϕ) dans le plan (OX,OZ) et d’un nombre de
division (angle β) dans le plan (OX,OY). Pour un angle ϕ donné, on fera varier l’angle β
de -π/2 à 2π/3 (soit de -90° à +270°). L’angle ϕ variera quant à lui de 0 à 2π (soit de 0°
à 360°). Le nombre de division nb_div_phi permettra de calculer le pas pour l’angle ϕ
et le nombre de division nb_div_beta permettra de calculer le pas pour l’angle β. Plus
les nombres de division seront importants, plus les facettes seront petites et plus la
définition sera meilleure. Le bas de la figure 5.38 montre une partie du tore pour un
angle ϕ allant de 0 à π/2 (cas du tore ouvert).
Y
270° -90°

D
rt A
β C
B


X
90°
206 Programmez des jeux vidéo 3D avec C#5 et WPF

FIGURE 5.38

Dr D
rt A
β C
point A:
xa = (Dr + rt * cos(β)) * cos(φ) B
φ
ya = rt * sin(β)
za = -(Dr + rt * cos(β)) * sin(φ)
X

tore généré avec


l’angle φ variant
de 0 à 90 degrés
Copyright 2013 Patrice REY
CHAPITRE 5 Les modèles 3D basiques 207

L’UserControl ModeleTore.xaml, dans le dossier chapitre_05, visualise un tore ouvert,


texturé avec une ressource image (figure 5.39). Un nombre de division égal à 60 est
utilisé horizontalement et verticalement, ce qui permet d’avoir un aspect lissé très
correcte de la surface torique. L’image qui est appliquée comme placage de texture
est composée de 3 motifs: un motif pour la partie haute de la face externe, un motif
pour la partie basse de la face externe, et un motif pour les parties haute et basse de
la face interne.
La méthode MaillageToreFace réalise le maillage du tore en fonction de la distance
rayon_dist entre l’axe Y et le centre du cercle, du rayon du tore rayon_tore, du nombre
de division horizontalement nb_div_hori, et du nombre de division verticalement nb_
div_verti.
//modeliser la geometrie d’un tore
private Geometry3D MaillageToreFace(double rayon_dist, double rayon_tore,
int nb_div_hori, int nb_div_verti) {
MeshGeometry3D maillage = new MeshGeometry3D();
double pas_angle_phi = 360d / nb_div_hori;
double pas_angle_beta = 180d / nb_div_verti;
int cpt_div_v = 0;
int cpt_div_h = 0;
for (double angle_phi = 0; angle_phi < 360; angle_phi += pas_angle_phi) {
for (double angle_beta = 90; angle_beta > -270; angle_beta -= pas_angle_beta) {
Point3D pt_a = new Point3D();
pt_a.X = (rayon_dist + rayon_tore * Math.Cos(angle_beta * Math.PI / 180))
* Math.Cos(angle_phi * Math.PI / 180);
pt_a.Y = rayon_tore * Math.Sin(angle_beta * Math.PI / 180);
pt_a.Z = -(rayon_dist + rayon_tore * Math.Cos(angle_beta * Math.PI / 180))
* Math.Sin(angle_phi * Math.PI / 180);
Point3D pt_b = new Point3D();
pt_b.X = (rayon_dist + rayon_tore * Math.Cos((angle_beta - pas_angle_beta)
* Math.PI / 180)) * Math.Cos(angle_phi * Math.PI / 180);
pt_b.Y = rayon_tore * Math.Sin((angle_beta - pas_angle_beta)
* Math.PI / 180);
pt_b.Z = -(rayon_dist + rayon_tore * Math.Cos((angle_beta - pas_angle_beta)
* Math.PI / 180)) * Math.Sin(angle_phi * Math.PI / 180);
Point3D pt_c = new Point3D();
pt_c.X = (rayon_dist + rayon_tore * Math.Cos((angle_beta - pas_angle_beta)
* Math.PI / 180)) * Math.Cos((angle_phi + pas_angle_phi) * Math.PI / 180);
pt_c.Y = rayon_tore * Math.Sin((angle_beta - pas_angle_beta)
* Math.PI / 180);
pt_c.Z = -(rayon_dist + rayon_tore * Math.Cos((angle_beta - pas_angle_beta)
* Math.PI / 180)) * Math.Sin((angle_phi + pas_angle_phi) * Math.PI / 180);
Point3D pt_d = new Point3D();
pt_d.X = (rayon_dist + rayon_tore * Math.Cos(angle_beta * Math.PI / 180))
* Math.Cos((angle_phi + pas_angle_phi) * Math.PI / 180);
pt_d.Y = rayon_tore * Math.Sin(angle_beta * Math.PI / 180);
208 Programmez des jeux vidéo 3D avec C#5 et WPF

FIGURE 5.39

Copyright 2013 Patrice REY


CHAPITRE 5 Les modèles 3D basiques 209

pt_d.Z = -(rayon_dist + rayon_tore * Math.Cos(angle_beta * Math.PI / 180))


* Math.Sin((angle_phi + pas_angle_phi) * Math.PI / 180);
maillage.Positions.Add(pt_a);
maillage.Positions.Add(pt_b);
maillage.Positions.Add(pt_c);
maillage.Positions.Add(pt_d); ... }}
}
Le placage de l’image de texture se fait de la même façon que celle utilisée pour le
placage sur la sphère. Il faut juste voir ici que l’on commence à appliquer l’image de
texture sur la face externe en haut (-90°) et que l’on termine par le haut de la face
interne (270°) en utilisant le sens des aiguilles d’une montre.

270° -90°

90°

//modeliser la geometrie d’un tore


private Geometry3D MaillageToreFace(double rayon_dist, double rayon_tore,
int nb_div_hori, int nb_div_verti) {
MeshGeometry3D maillage = new MeshGeometry3D();
double pas_angle_phi = 360d / nb_div_hori;
double pas_angle_beta = 180d / nb_div_verti;
int cpt_div_v = 0;
int cpt_div_h = 0;
for (double angle_phi = 0; angle_phi < 360; angle_phi += pas_angle_phi) {
for (double angle_beta = 90; angle_beta > -270; angle_beta -= pas_angle_beta) {
Point pt_hg = new Point((double)cpt_div_h / (double)nb_div_hori,
(double)cpt_div_v / (double)nb_div_verti);
Point pt_bg = new Point((double)cpt_div_h / (double)nb_div_hori,
(double)(cpt_div_v + 1) / (double)nb_div_verti);
Point pt_bd = new Point((double)(cpt_div_h + 1) / (double)nb_div_hori,
(double)(cpt_div_v + 1) / (double)nb_div_verti);
Point pt_hd = new Point((double)(cpt_div_h + 1) / (double)nb_div_hori,
(double)cpt_div_v / (double)nb_div_verti);
210 Programmez des jeux vidéo 3D avec C#5 et WPF
maillage.TextureCoordinates.Add(pt_hg);
maillage.TextureCoordinates.Add(pt_bg);
maillage.TextureCoordinates.Add(pt_bd);
maillage.TextureCoordinates.Add(pt_hd);
cpt_div_v++;
}
cpt_div_v = 0;
cpt_div_h++;
}}
Un tore de rayon 0.5 unité, positionné à une distance de 1.25 unité de l’axe Y, avec un
nombre de division de 60 horizontalement et verticalement, sera ajouté sur la scène
par la méthode AjouterObjetTore.
//objets tore
ModelUIElement3D tore_1 = AjouterObjetTore(1.25d,0.5d, 60, 60,
«chapitre_05/image_texture/texture_tore.png»);
tore_1.SetValue(NameProperty, «x_tore_1»);
x_conteneur_3d.Children.Add(tore_1);
Transform3DGroup tore_1_trans = new Transform3DGroup();
tore_1_trans.Children.Add(new TranslateTransform3D(0d, 0d, 0d));
tore_1.Transform = tore_1_trans;

Copyright 2013 Patrice REY


6
L’interactivité des modèles
3D

Au sommaire de ce chapitre
• apporter de l’interactivité aux modèles 3D par héritage de la classe
UIElement3D
• implémentation du test d’atteinte des objets 3D dans le Viewport3D
• implémentation d’un Trackball pour visualiser la scène sous différents
angles par les déplacements de la souris
• apporter de l’interactivité à la scène par l’implémentation d’un zoom par
l’intermédiaire de la molette de la souris
• effectuer des translations et des rotations des objets 3D sur la scène
212 Programmez des jeux vidéo 3D avec C#5 et WPF
Nous avons vu que les géométries des modèles 3D pouvaient être exprimées en
XAML et en code procédural. Très souvent, il s’avère pratique de pouvoir modéliser en
XAML tout comme en code procédural, un modèle 3D avec des propriétés spécifiques
déterminées à l’avance et en fonction d’une utilisation ultérieure. Cette façon de faire
passe par une personnalisation des objets 3D avec pour but une réutilisation à volonté,
rapide et précise.
La première partie de ce chapitre traite de la façon de créer ces modèles personnalisés
et réutilisables par l’intermédiaire de la classe UIElement3D. Dans la seconde partie
de ce chapitre, nous verrons comment apporter de l’interactivité à la scène 3D et aux
objets qu’elle contient.

1 - Hériter de la classe UIElement3D

La création de modèles 3D est passionnante, mais faire en sorte que votre objet 3D
réalisé puisse réagir aux entrées clavier, au focus et aux événements est encore plus
attrayant. La solution consiste à faire hériter votre modèle 3D de la classe UIElement3D.
UIElement3D est une classe de base pour les implémentations au niveau du noyau
WPF, reposant sur les éléments WPF et les caractéristiques de présentation de base.
UIElement3D est une classe de base abstraite, qui est apparue avec le framework .NET
3.5, à partir de laquelle vous pouvez dériver des classes pour représenter des éléments
3D spécifiques.
La majeure partie du comportement d’entrée, de mise en focus et de traitement
d’événements pour des éléments 3D est généralement définie dans la classe
UIElement3D. Cela inclut les événements liés à l’entrée au niveau du clavier, de la
souris et du stylet, ainsi que les propriétés d’état qui s’y rattachent. Ces événements
correspondent souvent à des événements routés et de nombreux événements associés
à l’entrée présentent aussi bien une version de routage par propagation qu’une version
par tunneling. Ces événements doubles sont en général les événements les plus
intéressants pour contrôler des objets.
L’objet UIElement3D présente les fonctionnalités suivantes définies spécialement par
Copyright 2013 Patrice REY

la classe UIElement3D :
• il peut répondre à l’entrée de l’utilisateur (y compris contrôler la destination
d’envoi de l’entrée à travers la gestion d’événements de routage ou le routage de
commandes).
• il peut déclencher des événements routés qui suivent un itinéraire à travers
l’arborescence logique d’éléments.
CHAPITRE 6 L’interactivité des modèles 3D 213

L’UserControl HeritageUIElement3D.xaml, dans le dossier chapitre_06, permet l’ajout


d’objets 3D comme un plan de travail et un repère d’axes de coordonnées (figure 6.1).
Ces objets 3D sont dérivés de la classe UIElement3D. Ils peuvent être ajoutés soit par
code procédural, soit par code XAML. Ils sont dotés de propriétés de dépendance
personnalisables. En mode XAML, ces propriétés personnalisables sont ajustées à
la fois dans la fenêtre des propriétés et par code XAML. Généralement les logiciels
3D comportent des boutons qui permettent, par un clic, d’afficher ou de masquer la
visualisation du plan de travail et du repère des coordonnées (figure 6.1 repère 2).
Nous allons voir comment implémenter ce type d’action.
FIGURE 6.1

2
214 Programmez des jeux vidéo 3D avec C#5 et WPF
Nous allons commencer par définir une géométrie personnalisée pour visualiser le
repère des coordonnées XYZ. La démarche consiste à créer un contrôle qui hérite de
UIElement3D, et à lui ajouter ses propriétés de dépendance spécifiques. Pour cela
nous créons une nouvelle classe intitulée Repere, qui hérite de UIElement3D. On
redéfinit la méthode héritée OnUpdateModel qui participe aux opérations de rendu en
cas de substitution dans une classe dérivée.
Lorsque vous dérivez une classe à partir de la classe UIElement3D, vous pouvez utiliser
cette méthode en combinaison avec la méthode InvalidateModel pour actualiser le
modèle de l’élément. Vous devez uniquement appeler cette méthode dans des scénarios
avancés. C’est notamment le cas si la classe dérivée possède plusieurs propriétés qui
affectent l’apparence et que vous ne souhaitez mettre à jour le modèle sous-jacent
qu’une seule fois. Au sein de la méthode OnUpdateModel, vous pouvez mettre à jour la
propriété Visual3DModel de la classe Visual3D. A noter que cette méthode ne possède
aucune implémentation par défaut dans la classe UIElement3D.
public class Repere : UIElement3D {
...
//redefinition de la methode de rendu
protected override void OnUpdateModel() {
...
}
...
}
On définit une propriété de dépendance P_Modele pour Repere et on affecte à cette
propriété un Model3DGroup intitulé groupe_axe. Ce modèle contient les trois axes X,
Y et Z.
//redefinition de la methode de rendu
protected override void OnUpdateModel() {
Model3DGroup groupe_axe = new Model3DGroup();
...
P_Modele = groupe_axe;
}
La démarche qui consiste à ajouter une propriété de dépendance s’effectue toujours
de la même façon quelque soit le type de la propriété de dépendance à définir. Prenons
Copyright 2013 Patrice REY

le cas de la propriété de dépendance P_Modele dont nous détaillons ici la façon de


l’implémenter.
On commence par ajouter une propriété publique statique, en lecture seule (readonly),
de type DependencyProperty, nommée ProprieteModele. Sa déclaration passe par
l’emploi de la méthode statique DependencyProperty.Register qui reçoit en paramètre:
• une chaîne qui indique le nom de la propriété de dépendance à inscrire (ici P_
Modele).
CHAPITRE 6 L’interactivité des modèles 3D 215

• un type de propriété correspondant à cette propriété (ici un type Model3D).


• le type du propriétaire qui inscrit la propriété de dépendance (ici c’est Repere).
• une instance de PropertyMetadata qui représente les métadonnées de propriété
pour la propriété de dépendance (ici on passe le nom de la méthode statique,
PM_Modele, qui modifie la propriété de dépendance).
La méthode PM_Modele récupère l’objet de dépendance d, de type Repere, et définit
sa propriété héritée Visual3DModel par l’affectation de la propriété de dépendance
Repere.P_Modele. Une fois la propriété de dépendance définie, on déclare les accesseurs,
get et set, pour lire et écrire la propriété P_Modele.
//la propriete Modele pour le repere
private static readonly DependencyProperty ProprieteModele =
DependencyProperty.Register(«P_Modele»,
typeof(Model3D),
typeof(Repere),
new PropertyMetadata(PM_Modele));
private static void PM_Modele(DependencyObject d,
DependencyPropertyChangedEventArgs e) {
Repere le_repere = (Repere)d;
le_repere.Visual3DModel = le_repere.P_Modele;
}
private Model3D P_Modele {
get {
return (Model3D)GetValue(ProprieteModele);
}
set {
SetValue(ProprieteModele, value);
}
}
Ce que nous avons besoin aussi comme propriété de dépendance, c’est celle qui
stocke la longueur de l’axe. Nous déclarons une propriété de dépendance nommée
P_LongueurDemiAxe, qui stocke la longueur d’un demi-axe (le demi-axe côté positif
étant égal au demi-axe côté négatif). Cette propriété est de type double, son type
propriétaire est Repere, et l’instance de PropertyMetadata spécifie la méthode PM_
LongueurDemiAxe comme méthode qui invalide le modèle pour le redessiner ainsi
qu’une valeur 1.0, de type double comme valeur d’initialisation. Les accesseurs get et
set permettent de lire et de modifier la propriété P_LongueurDemiAxe.
//la propriete LongueurDemiAxe
private static readonly DependencyProperty ProprieteLongueurDemiAxe =
DependencyProperty.Register(«P_LongueurDemiAxe»,
typeof(double),
typeof(Repere),
new PropertyMetadata(1.0, PM_LongueurDemiAxe));
216 Programmez des jeux vidéo 3D avec C#5 et WPF
private static void PM_LongueurDemiAxe(DependencyObject d,
DependencyPropertyChangedEventArgs e) {
Repere le_repere = (Repere)d;
le_repere.InvalidateModel();
}
public double P_LongueurDemiAxe {
get {
return (double)GetValue(ProprieteLongueurDemiAxe);
}
set {
SetValue(ProprieteLongueurDemiAxe, value);
}
}
Sur le même principe, nous ajoutons les propriétés de dépendance suivantes pour
personnaliser complètement le repère:
• la propriété P_EpaisseurAxe, de type double, qui donne une épaisseur à l’axe,
• la propriété P_CouleurAxeX, de type Color, qui donne la couleur de remplissage de
l’axe X,
• la propriété P_CouleurAxeY, de type Color, qui donne la couleur de remplissage de
l’axe Y,
• la propriété P_CouleurAxeZ, de type Color, qui donne la couleur de remplissage de
l’axe Z.
//la propriete EpaisseurAxe
private static readonly DependencyProperty ProprieteEpaisseurAxe =
DependencyProperty.Register(«P_EpaisseurAxe»,
typeof(double),
typeof(Repere),
new PropertyMetadata(0.01, PM_EpaisseurAxe));
//la propriete CouleurAxeX
private static readonly DependencyProperty ProprieteCouleurAxeX =
DependencyProperty.Register(«P_CouleurAxeX»,
typeof(Color),
typeof(Repere),
new PropertyMetadata(Colors.Red, PM_CouleurAxeX));
//la propriete CouleurAxeY
private static readonly DependencyProperty ProprieteCouleurAxeY =
DependencyProperty.Register(«P_CouleurAxeY»,
Copyright 2013 Patrice REY

typeof(Color),
typeof(Repere),
new PropertyMetadata(Colors.Green, PM_CouleurAxeY));
//la propriete CouleurAxeZ
private static readonly DependencyProperty ProprieteCouleurAxeZ =
DependencyProperty.Register(«P_CouleurAxeZ»,
typeof(Color), typeof(Repere),
new PropertyMetadata(Colors.Blue, PM_CouleurAxeZ));
CHAPITRE 6 L’interactivité des modèles 3D 217

Pour ajouter en XAML une instance de Repere, il faut au préalable ajouter dans
les propriétés de l’UserControl une référence xmlns à l’espace de nom Geo.Repere,
préfixée de visuel, en écrivant: xmlns:visuel = «clr-namespace:VS2012_3d_c5_wpf45.
Geo». Ensuite on ajoute une instance en XAML par un nœud <visuel:Repere> en fixant
les valeurs des propriétés de dépendance comme par exemple <visuel:Repere x:Name
= «x_repere» P_LongueurDemiAxe = «4» P_EpaisseurAxe = «0.02»/>. Le repère alors
s’affiche dans la fenêtre du rendu (Design) en fonction de ses propriétés modifiées
comme le montre la figure ci-dessous (ou par défaut si on le souhaite).

1
218 Programmez des jeux vidéo 3D avec C#5 et WPF
La personnalisation des propriétés de dépendance peut se faire aussi directement
dans la fenêtre des propriétés (comme le montre la figure ci-dessous) dès lors que
les propriétés à modifier sont de type simple (double, Color, bool, etc.). Comme nous
avons préfixé les propriétés de dépendance par P_ lors de l’attribution de leur nom,
elles apparaissent les unes sous les autres dans la fenêtre des propriétés.

Il ne reste plus qu’à fixer le nom donné au repère comme référencement de sa propriété
Name par code procédural en écrivant x_repere.SetValue(NameProperty, «x_repere»).
Si on préfère ajouter le repère uniquement par code procédural, il suffit alors de
l’instancier et de fixer ses propriétés de dépendance aux valeurs souhaitées.
Geo.Repere repere = new Geo.Repere();
repere.SetValue(NameProperty, «x_repere»);
repere.P_LongueurDemiAxe = 6d;
repere.P_EpaisseurAxe = 0.02d;
repere.P_CouleurAxeX = Colors.Red;
repere.P_CouleurAxeY = Colors.Blue;
Copyright 2013 Patrice REY

repere.P_CouleurAxeZ = Colors.Green;
x_conteneur_3d.Children.Add(repere);
On procède de même pour l’ajout d’un plan de travail. On définit une classe PlanTravail
qui hérite de UIElement3D. On lui ajoute les propriétés de dépendance suivantes:
• P_Modele, de type Model3D, qui définit le modèle représentant la grille,
• P_LongueurX, de type double, qui définit la longueur totale de la grille centrée sur
X,
CHAPITRE 6 L’interactivité des modèles 3D 219

• P_LongueurZ, de type double, qui définit la longueur totale de la grille centrée sur
Z,
• P_LongueurPas, de type double, qui définit l’espace entre deux lignes,
• P_CouleurGrille, de type Color, qui définit la couleur de la grille.
En XAML, par un nœud <visuel:PlanTravail>, on ajoute un plan de travail et on
personnalise ses propriétés de dépendance directement en XAML, et par la fenêtre
des propriétés.

Par code procédural, on instancie un objet PlanTravail, on lui fixe sa propriété Name.
On personnalise ses propriétés P_LongueurX, P_Longueur_Z, P_LongueurPas et P_
CouleurGrille. On l’ajoute au conteneur des objets 3D de la scène, par la méthode Add,
à la propriété Children de x_conteneur_3d.
//objet plan de travail par code
Geo.PlanTravail plan_travail = new Geo.PlanTravail();
plan_travail.SetValue(NameProperty, «x_plan_travail»);
plan_travail.P_LongueurX = 8d;
plan_travail.P_LongueurZ = 6d;
plan_travail.P_LongueurPas = 1d;
plan_travail.P_CouleurGrille = Colors.Black;
x_conteneur_3d.Children.Add(plan_travail);
Au lieu d’utiliser une case à cocher pour afficher ou masquer le plan de travail et le
repère, nous allons utiliser un logo représentatif de cet état. Au départ le plan de travail
(référencé par x_plan_travail) et le repère (référencé par x_repere) sont visibles. Pour
220 Programmez des jeux vidéo 3D avec C#5 et WPF
cela, un contrôle Image affiche la représentation de la grille par la ressource image
image/logo_grille_actif.png, et un autre contrôle Image affiche la représentation du
repère par la ressource image image/logo_repere_actif.png. Les variables booléennes
v_plan_travail_visible et v_repere_visible servent à indiquer la visibilité ou non des
contrôles. Les deux contrôles Image sont dotés d’un ToolTip indiquant l’action qui se
produira si on clique gauche dessus les contrôles.

<!-- boutons utilitaires: afficher masquer la grille -->


<Image x:Name=ˈx_vis_grilleˈ Height=ˈ50ˈ Canvas.Left=ˈ13ˈ Canvas.Top=ˈ88ˈ
Width=ˈ50ˈ Source=ˈimage/logo_grille_actif.pngˈ Stretch=ˈFillˈ
MouseLeftButtonDown=ˈx_vis_grille_MouseLeftButtonDownˈ Cursor=ˈHandˈ
ToolTip=ˈMasquer la grilleˈ/>
<!-- bouton utilitaire: afficher/masquer le repere xyz -->
<Image x:Name=ˈx_vis_repereˈ Height=ˈ50ˈ Canvas.Left=ˈ68ˈ Canvas.Top=ˈ88ˈ
Width=ˈ50ˈ Source=ˈimage/logo_repere_actif.pngˈ Stretch=ˈFillˈ
MouseLeftButtonDown=ˈx_vis_repere_MouseLeftButtonDownˈ Cursor=ˈHandˈ
ToolTip=ˈMasquer le repèreˈ/>
Les gestionnaires de l’événement MouseLeftButtonDown permettent de gérer le clic
gauche enfoncé sur les contrôles (x_vis_grille_MouseLeftButtonDown et x_vis_repere_
MouseLeftButtonDown). Quand l’événement se produit, la visibilité du contrôle change
et la méthode AfficherMasquerPlanRepere se charge d’effectuer ce changement visuel.
//afficher cacher le plan de travail
private void x_vis_grille_MouseLeftButtonDown(object sender,
MouseButtonEventArgs e) {
v_plan_travail_visible = !v_plan_travail_visible;
if (v_plan_travail_visible == false) {
x_vis_grille.Source = new BitmapImage(new Uri(
«pack://application:,,/chapitre_06/image/logo_grille_inactif.png»,
UriKind.Absolute));
AfficherMasquerPlanTravail(«x_plan_travail», «masquer»);
x_vis_grille.ToolTip = «Afficher la grille»;
Copyright 2013 Patrice REY

}
else {
x_vis_grille.Source = new BitmapImage(new Uri(
«pack://application:,,/chapitre_06/image/logo_grille_actif.png»,
UriKind.Absolute));
AfficherMasquerPlanTravail(«x_plan_travail», «afficher»);
x_vis_grille.ToolTip = «Masquer la grille»;
}
}
CHAPITRE 6 L’interactivité des modèles 3D 221

Elle parcourt les enfants contenus dans l’arbre visuel de x_conteneur_3d, et dès qu’elle
trouve le contrôle demandé, elle change sa propriété Visibility par Hidden (pour
masquer) ou par Visible (pour afficher).

private void AfficherMasquerPlanRepere(string nom_objet_3d_xaml,


string etat_visibilite) {
for (int xx = 0; xx < x_conteneur_3d.Children.Count; xx++) {
Visual3D visual3d = x_conteneur_3d.Children[xx];
string nom = (string)visual3d.GetValue(NameProperty);
if (nom == nom_objet_3d_xaml && visual3d.GetType()==typeof(Geo.PlanTravail)) {
Geo.PlanTravail plan_travail = (Geo.PlanTravail)x_conteneur_3d.Children[xx];
if (etat_visibilite == «masquer») {
plan_travail.Visibility = Visibility.Hidden;
}
if (etat_visibilite == «afficher») {
plan_travail.Visibility = Visibility.Visible;
}
break;
}
if (nom == nom_objet_3d_xaml && visual3d.GetType() == typeof(Geo.Repere)) {
Geo.Repere repere = (Geo.Repere)x_conteneur_3d.Children[xx];
if (etat_visibilite == «masquer») {
repere.Visibility = Visibility.Hidden;
}
if (etat_visibilite == «afficher») {
repere.Visibility = Visibility.Visible;
}
break;
}
}
}

2 - Implémenter le test d’atteinte

Le contrôle Viewport3D restitue dans sa zone de rendu 2D un environnement d’une


scène 3D. Si on lui ajoute un gestionnaire pour l’événement MouseLeftButtonDown, on
s’apercevra que cet événement sera déclenché uniquement si on clique gauche sur un
élément rendu dans sa zone de rendu. Si on clique gauche sur le Viewport3D dans une
zone ne comportant pas d’élément 3D, l’événement MouseLeftButtonDown ne sera pas
détecté. Il faudra tenir compte de ce problème pour l’implémentation d’un Trackball
comme on le verra dans le paragraphe suivant.
222 Programmez des jeux vidéo 3D avec C#5 et WPF
Pour bien mettre en évidence le test d’atteinte dans le Viewport3D, en plus du
repère x_repere (classe Geo.Repere) et du plan de travail x_plan_travail (classe Geo.
PlanTravail), nous allons ajouter un objet 3D représentant une boite d’allumettes à
partir de la modélisation d’un cube. Pour cela, on définit une classe Geo.Cube dans le
dossier Geo, qui hérite de UIElement. On lui ajoute les propriétés suivantes:
• P_Modele, de type Model3D, qui représente le modèle de la classe Cube,
• P_LargeurX, de type double, qui représente la largeur du cube suivant l’axe X (valeur
par défaut 1d),
• P_HauteurY, de type double, qui représente la hauteur du cube suivant l’axe Y
(valeur par défaut 1d),
• P_ProfondeurZ, de type double, qui représente la profondeur du cube suivant l’axe
Z (valeur par défaut 1d),
• P_CouleurUnie, de type Color, qui représente la couleur des faces du cube (valeur
par défaut Colors.Red),
• P_Texture, de type Material, qui représente la texture appliquée au cube à partir
d’une ressource image (valeur par défaut null).
La classe Cube est dotée d’une variable v_mode_affichage de type Cube.ModeAffichage
(énumération pour sélectionner le mode d’affichage du cube entre le mode couleur
ModeAffichage.uni et le mode texturé ModeAffichage.texture). Dans le constructeur, on
affecte la valeur ModeAffichage.uni à la variable v_mode_affichage. Dans la méthode
redéfinie OnUpdateModel de la classe Cube, si le mode d’affichage est ModeAffichage.
uni, alors on applique la propriété P_CouleurUnie à la propriété Material du modèle, et
si le mode d’affichage est ModeAffichage.texture, on applique la propriété P_Texture.
Pour passer du mode d’affichage par défaut (ModeAffichage.uni) au mode texturé, on
ajoute la méthode PasserEnModeTexture qui reçoit en paramètre un URI qui pointe
vers une ressource image. Avec cet URI, la propriété Material est modifiée et la
méthode InvalidateModel invalide le modèle pour qu’il soit redessiné. La méthode
PasserEnModeCouleurUni permet de passer du mode texturé au mode couleur par
défaut.
public class Cube : UIElement3D {
Copyright 2013 Patrice REY

...
//
private enum ModeAffichage { uni, texture };
private ModeAffichage v_mode_affichage;
public Cube() {
v_mode_affichage = ModeAffichage.uni;
}
//
public void PasserEnModeCouleurUni() {
CHAPITRE 6 L’interactivité des modèles 3D 223

v_mode_affichage = ModeAffichage.uni;
this.InvalidateModel();
}
//
public void PasserEnModeTexture(Uri uri_chemin_ressource) {
DiffuseMaterial diffus = new DiffuseMaterial();
ImageBrush img_brush = new ImageBrush();
img_brush.ImageSource = new BitmapImage(uri_chemin_ressource);
diffus.Brush = img_brush;
this.P_Texture = diffus;
v_mode_affichage = ModeAffichage.texture;
this.InvalidateModel();
}
//redefinition de la methode de rendu
protected override void OnUpdateModel() {
GeometryModel3D geo_cube = new GeometryModel3D();
geo_cube.Geometry = MaillageAxe(this.P_LargeurX, this.P_HauteurY,
this.P_ProfondeurZ);
switch (v_mode_affichage) {
case ModeAffichage.uni:
geo_cube.Material = new DiffuseMaterial(
new SolidColorBrush(this.P_CouleurUnie));
break;
case ModeAffichage.texture:
geo_cube.Material = this.P_Texture;
break;
}
geo_cube.BackMaterial = new DiffuseMaterial(
new SolidColorBrush(Colors.Yellow));
P_Modele = geo_cube;
}
...
}
Pour représenter une boite d’allumette, en XAML, on ajoute un cube x_cube et on
personnalise ses propriétés P_LargeurX (fixée à 2d), P_ProfondeurZ (fixée à 1.5d) et P_
couleurUnie (fixée à #FFE2A217).
<Viewport3D.Children>
<ContainerUIElement3D x:Name=ˈx_conteneur_3dˈ>
<ContainerUIElement3D.Children>
...
<visuel:PlanTravail x:Name=ˈx_plan_travailˈ P_LongueurX=ˈ6ˈ P_LongueurZ=ˈ6ˈ/>
<!-- repere xyz -->
<visuel:Repere x:Name=ˈx_repereˈ P_LongueurDemiAxe=ˈ4ˈ P_EpaisseurAxe=ˈ0.02ˈ/>
<!-- cube -->
<visuel:Cube x:Name=ˈx_cubeˈ P_ProfondeurZ=ˈ1.5ˈ P_LargeurX=ˈ2ˈ
P_CouleurUnie=ˈ#FFE2A217ˈ/>
</ContainerUIElement3D.Children>
224 Programmez des jeux vidéo 3D avec C#5 et WPF

Par code procédural on fixe la propriété Name du cube et on le passe en mode texturé
en indiquant l’URI chapitre_06/image/texture_cube_allumette.png correspondant au
fichier image de placage de texture.
//objet cube par xaml
x_cube.SetValue(NameProperty, «x_cube»);
x_cube.PasserEnModeTexture(new Uri(«pack://application:,,/» + «chapitre_06/image/
texture_cube_allumette.png», UriKind.Absolute));
L’UserControl TestAtteinte.xaml, dans le dossier chapitre_06, permet de mettre en
œuvre le test d’atteinte sur le Viewport3D (figure 6.2). La zone de rendu x_viewport
contient un repère, un plan de travail et une boite d’allumettes texturée. Cette zone de
rendu est dotée d’un gestionnaire pour l’événement MouseLeftButtonDown. Comme
le montre la figure 6.2, si on clique sur la boite d’allumettes (repère 1), une fenêtre
MessageBox s’affiche en mentionnant le nom du modèle 3D et son type. Si on clique
sur un axe du repère (repère 2), la boite MessageBox indique «Geo.Repere -> x_repere»
Copyright 2013 Patrice REY

(qui correspond à l’instance x_repere de la classe Geo.Repere). Si on clique sur le plan


de travail, la MessageBox affiche «Geo.PlanTravail -> x_plan_travail». Et si on clique
dans une zone ne comportant pas d’objets 3D, aucun clic gauche n’est détecté.
Pour réaliser le test d’atteinte, nous allons avoir besoin de la classe statique
VisualTreeHelper, de la classe HitTestResult et de sa classe dérivée
RayMeshGeometry3DHitTestResult (la figure 6.3 visualise l’arbre d’héritage).
CHAPITRE 6 L’interactivité des modèles 3D 225

FIGURE 6.2

3
226 Programmez des jeux vidéo 3D avec C#5 et WPF
FIGURE 6.3

La méthode statique VisualTreeHelper.HitTest, qui reçoit en paramètre le Viewport3D


x_viewport et la position de la souris sur ce dernier, retourne un objet HitTestResult.
La position de la souris sur le Viewport3D est obtenu par la méthode GetPosition
appliquée à l’événement MouseButtonEventArgs émanant du x_viewport.
La classe HitTestResult est une classe abstraite. Elle possède une propriété VisualHit
qui définit l’objet de l’arbre visuel qui a été atteint par le test du rayon pour le test
d’atteinte. Une de ses classes dérivées est RayHitTestResult qui est aussi une classe
abstraite qui apporte principalement les propriétés ModelHit (qui retourne l’objet de
type Model3D obtenu par le test d’atteinte) et PointHit (qui retourne le point de type
Point3D représentant l’intersection entre l’objet 3D et le rayon servant pour réaliser
le test d’atteinte). La classe scellée RayMeshGeometry3DHitTestResult, qui hérite de
RayHitTestResult, sera celle utilisée pour le test d’atteinte. Elle apportera les propriétés
héritées VisualHit, ModelHit et PointHit.
Ici, après avoir récupéré la position de la souris pos_souris sur le x_viewport, on instancie
un objet result, de type HitTestResult, par la méthode statique VisualTreeHelper.
HitTest en fonction de x_viewport et pos_souris. On définit un objet mesh, de type
Copyright 2013 Patrice REY

RayMeshGeometry3DHitTestResult, par un cast de result (puisque que la classe


HitTestResult possède parmi ses classes dérivées RayMeshGeometry3DHitTestResult).
Le type de l’objet atteint par le test d’atteinte est obtenu par la méthode GetType
appliquée à la propriété VisualHit, et le nom de l’objet atteint est obtenu par la méthode
GetValue appliquée à la propriété VisualHit. L’obtention du maillage du modèle 3D
atteint par le test d’atteinte sera obtenu par le propriété ModelHit de mesh.
CHAPITRE 6 L’interactivité des modèles 3D 227

//clic gauche enfoncé sur viewport3d


private void x_viewport_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
string test = «»;
Point pos_souris = e.GetPosition(x_viewport);
HitTestResult result = VisualTreeHelper.HitTest(x_viewport, pos_souris);
RayMeshGeometry3DHitTestResult mesh = result as RayMeshGeometry3DHitTestResult;
test = result.VisualHit.GetType().ToString() + « -> «
+ result.VisualHit.GetValue(NameProperty) + RC;
test += mesh.ModelHit.GetType().ToString() + RC;
MessageBox.Show(test);
e.Handled = true;
}

3 - Implémenter un Trackball

En informatique, un Trackball est une boule insérée dans un clavier, que l’on manipule
avec les doigts et qui fait office de souris. Dans la modélisation 3D, reproduire les
déplacements de la souris d’un Trackball avec une incidence sur le visuel de la zone
de rendu, est incontestablement un plus, et cela apporte un niveau d’intuitivité
supplémentaire. Nous allons implémenter un Trackball pour le Viewport3D.
Le Trackball transforme les mouvements 2D de la souris sur la zone de rendu en
mouvements 3D par des rotations. Cela est possible en projetant la position de la
souris sur une sphère imaginaire positionnée derrière le Viewport3D. Quand la souris
bouge horizontalement, une rotation autour de l’axe Y est nécessaire pour viser le
même point sur la sphère positionnée derrière le pointeur de la souris.

Y Y

X
X

Z
Z

curseur de
la souris curseur de
la souris
228 Programmez des jeux vidéo 3D avec C#5 et WPF
Quand la souris bouge verticalement, une rotation autour de l’axe X est nécessaire
pour viser le même point sur la sphère positionnée derrière le pointeur de la souris.

Y
Y

X curseur de X
la souris
Z Z

curseur de
la souris

Le Trackball fournit une méthode intuitive par laquelle une scène 3D peut être
manipulée dans toutes les directions en appliquant une combinaison de rotations.
Commençons par détailler un déplacement horizontal du curseur de la souris qui passe
de la variable v_pos_souris de coordonnées (200,100) à la variable pos_souris_deplac de
coordonnées (300,100) dans l’espace 2D. Le vecteur delta_2d, pour aller de la position
initiale à la position finale, sera delta_2d = v_pos_souris - pos_souris_deplac soit (100,0).
X
v_pos_souris -> (200,100) (200,100) (300,100)
pos_souris_deplac -> (300,120)

Y
vecteur: delta_2d = v_pos_souris - pos_souris_deplac = (300-200,100-100) = (100,0)
X
(200,100) (300,100)
Copyright 2013 Patrice REY

delta_2d
Y (100,0)

Si on transpose le vecteur delta_2d de l’espace 2D en vecteur delta_3d de l’espace 3D,


alors les coordonnées de delta_3d seront (100,0,0) en tenant compte des orientations
des repères XY de l’espace 2D et XYZ de l’espace 3D.
CHAPITRE 6 L’interactivité des modèles 3D 229

Y
(200,100,0) (300,100,0)

delta_3d
(100,0,0)
X

Z
Maintenant si on calcule le produit vectoriel de delta_3d par un vecteur Z (qui représente
la direction positive de l’axe Z), on obtiendra un vecteur perpendiculaire à delta_3d et
à Z, possédant une direction et une longueur. Ce vecteur vect_axe résultant du produit
vectoriel représentera l’axe autour duquel la scène pourra tourner, et la valeur de sa
longueur représentera l’angle de rotation. Plus la différence entre la position initiale
de la souris et la position de la souris déplacée, sera grande et plus la rotation sera
importante. On a vu au premier chapitre comment calculer un produit vectoriel. Ici
nous avons delta_3d (100,0,1) et le vecteur Z(0,0,1). Le produit vectoriel donne un
vecteur vect_axe(0,-100,0).
delta_3d 100 0 0 100 0
vecteur Z 0 0 1 0 0

première troisième
coordonnée deuxième coordonnée
0*1-0*0 coordonnée 100 * 0 - 0 * 0
=0 0 * 0 - 1 * 100 = -100 =0

Y delta_3d
(100 , 0 , 0)
vecteur
Z (0,0,1)

X
vect_axe (0 , -100, 0)
= produit vectoriel de
Z delta_3d par vecteur Z
230 Programmez des jeux vidéo 3D avec C#5 et WPF
En appliquant le même principe, si on effectue un déplacement vertical du curseur de
la souris qui passe de la variable v_pos_souris de coordonnées (200,100) à la variable
pos_souris_deplac de coordonnées (200,50) dans l’espace 2D, on obtiendra un vecteur
vect_axe(50,0,0).

1 Y

X (200,50,0)
delta_3d
(200,50) (0,50,0)
delta_2d 2 (200,100,0)
(0,-50)
(200,100) X
Y

delta_3d 0 50 0 0 50
3 vecteur Z 0 0 1 0 0

première troisième
coordonnée deuxième coordonnée
50 * 1 - 0 * 0 coordonnée 0 * 0 - 0 * 50
= 50 0*0-1*0 =0
=0
Y
delta_3d
(0 , 50 , 0)
4 vect_axe (50 , 0, 0)
= produit vectoriel de
delta_3d par vecteur Z
X
Copyright 2013 Patrice REY

vecteur
Z Z (0,0,1)

Enfin, si on effectue un déplacement à la fois horizontal et vertical du curseur de la


souris qui passe de la variable v_pos_souris de coordonnées (200,100) à la variable
pos_souris_deplac de coordonnées (300,120) dans l’espace 2D, on obtiendra un vecteur
CHAPITRE 6 L’interactivité des modèles 3D 231

vect_axe(-20,-100,0).
Y
X
(200,100)
1
2 delta_3d
(300,120) (100 , -20 , 0)
Y delta_2d X
(100,20)

delta_3d 100 -20 0 100 -20


3 vecteur Z 0 0 1 0 0

première troisième
coordonnée deuxième coordonnée
-20 * 1 - 0 * 0 coordonnée 100 * 0 - 0 * (-20)
= -20 0 * 0 - 1 * 100 =0
=-100

4 vecteur
Z (0,0,1)
delta_3d
(100 , -20 , 0)
X

vect_axe (-20 , -100, 0)


= produit vectoriel de
Z delta_3d par vecteur Z

On a vu au précédent paragraphe pour le test d’atteinte que l’événement


MouseLeftButtonDown sur le Viewport3D se déclenchait uniquement sur un objet
3D. Or on veut pouvoir faire tourner la scène en maintenant un clic gauche enfoncé
sur toute la surface du Viewport3D. Comme notre Viewport3D est l’unique enfant
du contrôle Border x_bordure, on va ajouter les gestionnaires d’événement pour les
232 Programmez des jeux vidéo 3D avec C#5 et WPF
événements MouseLeftButtonDown, MouseMove et MouseLeftButtonUp au x_bordure. Et
de façon à ce qu’il n’y ait pas de propagation de l’événement au Viewport3D, ces trois
événements auront la propriété Handled fixée à true.
L’UserControl TestTrackball.xaml, dans le dossier chapitre_06, permet de mettre en
œuvre l’implémentation du Trackball pour faire tourner la scène avec le clic gauche
de la souris enfoncé et maintenu (figure 6.4). Dès que le clic gauche est enfoncé et
maintenu, le curseur de la souris change de forme et le déplacement de la souris fait
tourner la scène. Au relâchement du clic, le curseur revient à sa forme de départ.
FIGURE 6.4

Copyright 2013 Patrice REY

Le gestionnaire Border_MouseLeftButtonDown traite du clic gauche enfoncé sur


le x_bordure. La propriété Handled de l’événement est fixée à true pour éviter sa
propagation à d’autres contrôles. On stocke dans v_pos_souris la position de la souris
sur l’élément qui a déclenché l’événement. On appelle la méthode CaptureMouse qui
CHAPITRE 6 L’interactivité des modèles 3D 233

lance le processus de capture de la souris sur un élément. On change la propriété


Cursor de x_bordure par une ressource embarquée représentant un curseur à quatre
flèches (chapitre_06/curseur/curseur_trackball.cur).
//clic gauche enfoncé sur le border
private void Border_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
e.Handled = true;
UIElement element = (UIElement)sender;
v_pos_souris = e.MouseDevice.GetPosition(element);
element.CaptureMouse();
StreamResourceInfo info = Application.GetResourceStream(
new Uri(«pack://application:,,/chapitre_06/curseur/curseur_trackball.cur»,
UriKind.Absolute));
x_bordure.Cursor = new Cursor(info.Stream);
}
Le gestionnaire Border_MouseMove traite du déplacement de la souris sur le x_bordure.
La propriété Handled de l’événement est fixée à true pour éviter sa propagation à
d’autres contrôles. Si le clic gauche est enfoncé et maintenu lors du déplacement,
alors la méthode booléenne IsMouseCaptured sur x_bordure retournera true. Dans ce
cas, on relève la nouvelle position de la souris pos_souris_deplac. On calcule le vecteur
delta_2d, et on en déduit le vecteur delta_3d. On calcule le vecteur vect_axe qui résulte
du produit vectoriel de delta_3d par le vecteur Z (représentant l’axe Z). On calcule long_
axe qui représente la longueur du vecteur vect_axe.
Pour effectuer une rotation autour d’un axe donné par une orientation quelconque
et en fonction d’une valeur d’angle, on va utiliser un objet de type Quaternion. La
structure Quaternion représente une rotation en 3 dimensions. Un de ses constructeurs
demande en paramètre l’axe de rotation, de type Vector3D, et l’angle en dégrés, de
type double. On calcule la rotation v_rotation_delta, de type Quaternion, en fonction
de vect_axe et de long_axe. La variable v_rotation_effectue, de type Quaternion, stocke
la rotation déjà effectuée. La nouvelle rotation q, de type Quaternion, à effectuer sera
le produit de v_rotation_delta par v_rotation_effectue.
Maintenant il faut affecter à la propriété Transform de x_conteneur_3d une transformation
de rotation de type RotateTransform3D. On instancie un RotateTransform3D
rotation et on affecte à ses propriétés CenterX et CenterY les coordonnées de l’origine
(centre de rotation de la scène). Puis on affecte à sa propriété Rotation un objet
QuaternionRotation3D qui représente une transformation de rotation définie sur un
objet Quaternion. L’objet Quaternion demandé est la nouvelle rotation q calculée.
Les quaternions permettent d’étendre la notion de rotation en 3 dimensions à celle en
4 dimensions. Un quaternion est défini via l’usage de 4 valeurs réelles (x, y, z, w). Elles
sont calculées par une combinaison des 3 coordonnées de l’axe de rotation et de l’angle
234 Programmez des jeux vidéo 3D avec C#5 et WPF
correspondant. Au lieu de tourner un objet via une série de rotations successives,
les quaternions permettent au programmeur de tourner un objet autour d’un axe
arbitraire et d’un angle quelconque. La rotation est toujours réalisée par des calculs
matriciels. Néanmoins, au lieu de multiplier les matrices ensembles, les quaternions
les représentant sont multipliés. Le résultat final est reconverti en la matrice désirée.
Les quaternions offrent aussi l’avantage de permettre l’interpolation. Ceci permet des
rotations plus souples et plus réalistes.
//deplacement souris sur le border
private void Border_MouseMove(object sender, MouseEventArgs e) {
e.Handled = true;
UIElement element = (UIElement)sender;
if (element.IsMouseCaptured) {
Point pos_souris_deplac = e.MouseDevice.GetPosition(element);
Vector delta_2d = v_pos_souris - pos_souris_deplac;
Vector3D delta_3d = new Vector3D(delta_2d.X, -delta_2d.Y, 0);
Vector3D vect_axe = Vector3D.CrossProduct(delta_3d, new Vector3D(0, 0, 1));
double long_axe = vect_axe.Length;
if (long_axe < 0.00001) {
v_rotation_delta = new Quaternion(new Vector3D(0, 0, 1), 0);
}
else {
v_rotation_delta = new Quaternion(vect_axe, long_axe);
}
Quaternion q = v_rotation_delta * v_rotation_effectue;
RotateTransform3D rotation = new RotateTransform3D();
QuaternionRotation3D quatRotation = new QuaternionRotation3D(q);
rotation.Rotation = quatRotation;
rotation.CenterX = 0;
rotation.CenterY = 0;
x_conteneur_3d.Transform = rotation;
}
}
Le gestionnaire Border_MouseLeftButtonUp traite du relâchement du clic gauche de
la souris sur le x_bordure. La propriété Handled de l’événement est fixée à true pour
éviter sa propagation à d’autres contrôles. On procède à la mise à jour de la variable
v_rotation_effectue. On arrête la capture de la souris sur x_bordure par la méthode
Copyright 2013 Patrice REY

ReleaseMouseCapture. Et on affecte, à la propriété Cursor de x_bordure, sa valeur initiale.


//clic gauche relaché sur le border
private void Border_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) {
e.Handled = true;
v_rotation_effectue = v_rotation_delta * v_rotation_effectue;
UIElement el = (UIElement)sender;
el.ReleaseMouseCapture();
x_bordure.Cursor = Cursors.Arrow; }
CHAPITRE 6 L’interactivité des modèles 3D 235

4 - Se rapprocher et s’éloigner de la scène

L’UserControl TestZoom.xaml, dans le dossier chapitre_06, permet de mettre en œuvre


l’implémentation du zoom pour s’éloigner et se rapprocher de la scène grâce à la
molette de la souris (on a toujours l’utilisation du Trackball pour faire tourner la scène
avec le clic gauche de la souris enfoncé et maintenu). La figure 6.5 visualise l’effet
obtenu quand on se rapproche très prés de la boite d’allumettes.
La molette de la souris peut être tournée vers l’avant ou vers nous. La propriété Delta
de l’événement MouseWheelEventArgs donne une valeur qui indique la quantité
correspondant au fait de tourner la molette de la souris. Si cette quantité est positive,
alors la molette a été tournée vers l’avant. Si cette quantité est négative, alors la molette
a été tournée vers nous. Ce qui nous intéresse c’est de pouvoir transformer cette
quantité en un effet de zoom: si on tourne la molette vers l’avant alors on s’éloigne de
la scène, et si on tourne la molette vers nous alors on se rapproche de la scène.
On ajoute au x_bordure un gestionnaire pour l’événement MouseWheel (événement
déclenché dès que l’on tourne la molette). On fixe la propriété Handled de
MouseWheelEventArgs à true pour éviter la propagation de l’événement. On utilise
une variable v_zoom, de type double, pour stocker la valeur du zoom en cours. Si la
propriété Delta de l’événement est positive, alors on incrémente v_zoom de la valeur
0.25, et si la propriété Delta est négative, alors on décrémente v_zoom de 0.25.

si on tourne la si on tourne la
molette vers nous molette vers l’avant
-> Delta < 0 -> Delta > 0
donc on se donc on s’éloigne
rapproche (zoom négatif )
(zoom positif )
236 Programmez des jeux vidéo 3D avec C#5 et WPF

FIGURE 6.5

Copyright 2013 Patrice REY


CHAPITRE 6 L’interactivité des modèles 3D 237

Au préalable, on positionnera la caméra de façon à ce qu’elle soit à 45° de l’axe X et de


l’axe Z. On affectera par exemple à x_camera le Point3D (5,5,5) à sa propriété Position,
et le Vector3D (-5,-5,-5) à sa propriété LookDirection (de cette façon la caméra vise
l’origine du repère).
<Viewport3D.Camera>
<PerspectiveCamera x:Name=ˈx_cameraˈ Position=ˈ5,5,5ˈ LookDirection=ˈ-5,-5,-5ˈ
FieldOfView=ˈ60ˈ UpDirection=ˈ0 1 0ˈ NearPlaneDistance=ˈ0.125ˈ
FarPlaneDistance=ˈ100ˈ></PerspectiveCamera>
</Viewport3D.Camera>
Pour appliquer l’effet d’éloignement et de rapprochement de la scène, on affecte à la
propriété Transform de x_camera une transformation 3D de translation par un objet
TranslateTransform3D, dont ses propriétés OffsetX, OffsetY et OffsetZ sont fixées à v_
zoom.
//mouvement de la molette
private void x_bordure_MouseWheel(object sender, MouseWheelEventArgs e) {
e.Handled = true;
if (e.Delta > 0) {
//eloigner
v_zoom += 0.25d;
}
else {
//se rapprocher
v_zoom -= 0.25d;
}
Transform3DGroup gp = new Transform3DGroup();
TranslateTransform3D trans = new TranslateTransform3D(v_zoom, v_zoom, v_zoom);
gp.Children.Add(trans);
x_camera.Transform = gp;
}

5 - Déplacer des objets sur la scène

L’UserControl DeplacementObjet.xaml, dans le dossier chapitre_06, permet de mettre


en œuvre l’implémentation concernant le déplacement d’objet par translation. La
scène contient une boite d’allumettes (cube de type Geo.Cube) et un cylindre (type
Geo.Cylindre). La figure 6.6 visualise les deux objets au départ et la figure 6.7 visualise
le cylindre positionné dessus la boite d’allumettes après avoir effectué des translations
sur le cube et le cylindre.
Un contrôle Image x_logo_trackball permet d’activer le mode de visualisation avec le
Trackball comme on l’a vu dans le paragraphe précédent. Un autre contrôle Image
x_logo_deplac_objet permet d’activer le mode de déplacement d’objet par translation.
238 Programmez des jeux vidéo 3D avec C#5 et WPF
FIGURE 6.6

Copyright 2013 Patrice REY


CHAPITRE 6 L’interactivité des modèles 3D 239

FIGURE 6.7
240 Programmez des jeux vidéo 3D avec C#5 et WPF
Les variables booléennes v_logo_trackball_actif et v_logo_deplac_objet_actif permettent
d’indiquer le mode en cours d’activité (soit avec le Trackball, soit le déplacement
d’objet).
Le cylindre est du type Geo.Cylindre. Il hérite de UIElement3D. Ses propriétés sont:
• P_Modele, de type Model3D, qui représente le modèle du cylindre,
• P_Hauteur, de type double, qui représente la hauteur du cylindre (valeur par défaut
1d),
• P_RayonExterne, de type double, qui représente le rayon de la face extérieure du
cylindre (valeur par défaut 1d),
• P_RayonInterne, de type double, qui représente le rayon de la face intérieure du
cylindre (valeur par défaut 0.5d),
• P_NombreDivision, de type int, qui représente le nombre de division pour les faces
du cylindre (valeur par défaut 40),
• P_CouleurUnie, de type Color, qui représente la couleur de toutes les parties vues
du cylindre (par défaut Colors.Blue),
• P_TextureExterieure, de type Material, qui représente la texture appliquée sur la
face extérieure du cylindre en fonction d’une ressource image (valeur par défaut
null),
• P_TextureInterieure, de type Material, qui représente la texture appliquée sur la
face intérieure du cylindre en fonction d’une ressource image (valeur par défaut
null),
• P_TextureDessus, de type Material, qui représente la texture appliquée sur la face
du dessus du cylindre en fonction d’une ressource image (valeur par défaut null),
• P_TextureDessous, de type Material, qui représente la texture appliquée sur la face
du dessous du cylindre en fonction d’une ressource image (valeur par défaut null),
Les méthodes FixerTextureExterieur, FixerTextureInterieur, FixerTextureDessus et
FixerTextureDessous permettent de fixer les textures en fonction d’un URI pointant sur
une ressource image. Les méthodes AfficherObjetModeUni et AfficherObjetModeTexture
permettent de passer en mode d’affichage respectivement couleur et texture.
Le principe que nous allons utiliser ici pour déplacer les objets est simple d’un point
Copyright 2013 Patrice REY

de vue interface utilisateur. Ce qui compte c’est de retenir la démarche qui pourra être
améliorée en terme d’ergonomie. Comme le montre la figure 6.8, pour déplacer la boite
d’allumettes, on active le mode de déplacement selon les axes (repère 1). On clique
sur l’objet à déplacer (repère 2), celui-ci passe en mode couleur unie. Les boutons de
translation (repère 3) s’activent, et on clique dessus pour effectuer les translations
voulues. Quand c’est fini, on clique sur OK et la boite repasse en mode texture (repère
4). On effectue la même démarche pour positionner le cylindre sur la boite.
CHAPITRE 6 L’interactivité des modèles 3D 241
FIGURE 6.8

1 activation du mode déplacement d’objet

utilisation des boutons de


translation selon les axes 3

clic sur un objet à


2 déplacer et il passe en
mode couleur unie

4
242 Programmez des jeux vidéo 3D avec C#5 et WPF
Pour sélectionner un objet 3D, on effectue un test d’atteinte sur le Viewport3D comme
on l’a déjà vu. La propriété héritée VisualHit de RayMeshGeometry3DHitTestResult
nous permet d’obtenir le type de l’objet 3D. On récupère son nom (propriété
NameProperty) que l’on stocke dans une variable v_nom_xaml_obj_select. On passe
l’objet 3D en mode couleur unie par la méthode AfficherObjetModeUni. On récupère
la matrice de transformation déjà effectuée par la propriété Transform de l’objet
3D. On stocke dans les variables v_deplac_x, v_deplac_y et v_deplac_z, les valeurs
des translations effectuées (propriétés OffsetX, OffsetY et OffsetZ de la matrice). La
méthode AfficherBoutonDeplacement active les boutons de translation.
//clic gauche enfoncé sur viewport3d
private void x_viewport_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (v_logo_deplac_objet_actif == true) {
e.Handled = true;
Point pos_souris = e.GetPosition(x_viewport);
HitTestResult result = VisualTreeHelper.HitTest(x_viewport, pos_souris);
RayMeshGeometry3DHitTestResult mesh = result as RayMeshGeometry3DHitTestResult;
if (mesh.VisualHit.GetType() == typeof(Geo.Cube)) {
Geo.Cube cube = (Geo.Cube)mesh.VisualHit;
v_nom_xaml_obj_select = (string)cube.GetValue(NameProperty);
cube.AfficherObjetModeUni();
Matrix3D mat = cube.Transform.Value;
v_deplac_x = mat.OffsetX;
v_deplac_y = mat.OffsetY;
v_deplac_z = mat.OffsetZ;
AfficherBoutonDeplacement();
}
if (mesh.VisualHit.GetType() == typeof(Geo.Cylindre)) {
Geo.Cylindre cylindre = (Geo.Cylindre)mesh.VisualHit;
v_nom_xaml_obj_select = (string)cylindre.GetValue(NameProperty);
cylindre.AfficherObjetModeUni();
Matrix3D mat = cylindre.Transform.Value;
v_deplac_x = mat.OffsetX;
v_deplac_y = mat.OffsetY;
v_deplac_z = mat.OffsetZ;
AfficherBoutonDeplacement();
}
Copyright 2013 Patrice REY

}
}
Un clic sur un bouton de translation permet d’incrémenter ou de décrémenter
les variables v_deplac_x, v_deplac_y et v_deplac_z, par pas de 0.1 en fonction de la
translation souhaitée (par exemple axe X côté positif ou côté négatif, etc.). La liste
v_liste_objet, de type List<UIElement3D>, contient les objets 3D ajoutés (cube et
cylindre). On parcourt cette liste pour trouver l’objet 3D sélectionné (par l’intermédiaire
CHAPITRE 6 L’interactivité des modèles 3D 243

de son nom stocké dans v_nom_xaml_obj_select. Pour effectuer la translation, il suffit


d’affecter à la propriété Transform de l’objet 3D une transformation de translation de
type TranslateTransform3D, avec les valeurs de translation v_deplac_x, v_deplac_y et
v_deplac_z.
//clic sur un bouton de translation a effectuer
private void x_btn_trans_Click(object sender, RoutedEventArgs e) {
Button btn = (Button)sender;
switch (btn.Name) {
case «x_btn_trans_x_plus»:
v_deplac_x += 0.1d;
break;
case «x_btn_trans_x_moins»:
v_deplac_x -= 0.1d;
break;
case «x_btn_trans_y_plus»:
v_deplac_y += 0.1d;
break;
case «x_btn_trans_y_moins»:
v_deplac_y -= 0.1d;
break;
case «x_btn_trans_z_plus»:
v_deplac_z += 0.1d;
break;
case «x_btn_trans_z_moins»:
v_deplac_z -= 0.1d;
break;
}
for (int xx = 0; xx < v_liste_objet.Count; xx++) {
UIElement3D elem = v_liste_objet[xx];
string nom = (string)elem.GetValue(NameProperty);
if (nom == v_nom_xaml_obj_select) {
elem.Transform = new TranslateTransform3D(v_deplac_x, v_deplac_y,
v_deplac_z);
}
}
}
On valide les opérations de translation en appuyant sur le bouton OK. Pour cela, on
parcourt la liste v_liste_objet pour trouver l’objet 3D déplacé et on le repasse en mode
texturé (méthode AfficherObjetModeTexture). Les boutons de translation sont alors
désactivés. On peut alors repasser en mode Trackball pour visualiser la scène sous
d’autres angle.
//clic pour valider les opérations de translation
private void x_btn_trans_ok_Click(object sender, RoutedEventArgs e) {
...
for (int xx = 0; xx < v_liste_objet.Count; xx++) {
244 Programmez des jeux vidéo 3D avec C#5 et WPF
UIElement3D elem = v_liste_objet[xx];
string nom = (string)elem.GetValue(NameProperty);
if (nom == v_nom_xaml_obj_select) {
if (elem.GetType() == typeof(Geo.Cube)) {
Geo.Cube cube = (Geo.Cube)elem;
cube.AfficherObjetModeTexture();
}
}
if (nom == v_nom_xaml_obj_select) {
if (elem.GetType() == typeof(Geo.Cylindre)) {
Geo.Cylindre cylindre = (Geo.Cylindre)elem;
cylindre.AfficherObjetModeTexture();
}
}
}
}

6 - Tourner des objets sur la scène

L’UserControl TournerObjet.xaml, dans le dossier chapitre_06, permet de mettre


en œuvre l’implémentation concernant la rotation d’objet autour des axes de
coordonnées. La scène contient une boite d’allumettes (cube de type Geo.Cube). La
figure 6.9 visualise la boite au départ et la figure 6.10 visualise la boite après rotation
autour des axes de coordonnées.
Un contrôle Image x_logo_rotation permet d’activer le mode de rotation d’objet autour
des axes de coordonnées par l’intermédiaire de 3 glissières nommées x_slider_rot_x,
x_slider_rot_y et x_slider_rot_z.
On utilise le même procédé que dans le paragraphe précédent concernant la translation.
Quand le bouton de rotation est activé (repère 1 figure 6.11), un clic sur l’objet à faire
tourner permet de réaliser un test d’atteinte et de passer l’objet en mode couleur
unie (repère 2 figure 6.11). Les trois glissières alors sont utilisables pour effectuer les
rotations nécessaires (repère 3 figure 6.11). On valide avec le bouton x_btn_rot_ok pour
valider la rotation et la boite repasse en mode texturé (repère 4 figure 6.11).
Pour effectuer la rotation du cube, on ajoute une méthode AppliquerRotationAxe
Copyright 2013 Patrice REY

à la classe Geo.Cube. Cette méthode reçoit en paramètre des valeurs d’angle


angle_x, angle_y et angle_z, qui correspondent à des valeurs de rotation à effectuer
respectivement autour des axes X, Y et Z (valeurs d’angle qui sont fonction des valeurs
relevées sur les trois glissières). Comme il faut effectuer une rotation autour des trois
axes, on instancie un objet Transform3DGroup et on ajoute à sa propriété Children
trois objets RotateTransform3D, qui reçoivent un objet QuaternionRotation3D. Un
objet QuaternionRotation3D reçoit en paramètre une structure Quaternion (structure
CHAPITRE 6 L’interactivité des modèles 3D 245
FIGURE 6.9
246 Programmez des jeux vidéo 3D avec C#5 et WPF
FIGURE 6.10

Copyright 2013 Patrice REY


CHAPITRE 6 L’interactivité des modèles 3D 247
FIGURE 6.11

utilisation des glissières de


1 activation du mode rotation d’objet rotation selon les axes 3

clic sur un objet à faire


2 tourner et il passe en
mode couleur unie

4
248 Programmez des jeux vidéo 3D avec C#5 et WPF
qui représente une rotation en trois dimensions). Pour réaliser la rotation, la structure
Quaternion reçoit en paramètre un Vector3D représentant l’axe de rotation (Vector3D(1,
0, 0) pour l’axe X, Vector3D(0, 1, 0) pour l’axe Y, et Vector3D(0, 0, 1) pour l’axe Z), et une
valeur d’angle, de type double, pour représenter un angle de rotation en dégrés.
public class Cube : UIElement3D {
...
public void AppliquerRotationAxe(double angle_x,double angle_y,double angle_z) {
Quaternion qx = new Quaternion(new Vector3D(1, 0, 0), angle_x);
Quaternion qy = new Quaternion(new Vector3D(0, 1, 0), angle_y);
Quaternion qz = new Quaternion(new Vector3D(0, 0, 1), angle_z);
Transform3DGroup gp = new Transform3DGroup();
gp.Children.Add(new RotateTransform3D(new QuaternionRotation3D(qx)));
gp.Children.Add(new RotateTransform3D(new QuaternionRotation3D(qy)));
gp.Children.Add(new RotateTransform3D(new QuaternionRotation3D(qz)));
this.Transform = gp;
this.InvalidateModel();
}
...
}

Copyright 2013 Patrice REY


Deuxième Partie

Animation 3D
du
jeu vidéo
7
Construction du jeu du
Casse-Briques

Au sommaire de ce chapitre
• visualisation du résultat à obtenir pour une application sur Windows 7 et
pour une application sur une tablette tactile équipée avec Windows 8
• mise en place des objets 3D modélisés comme le plateau, les murs, la
raquette, la balle et les briques
• mise en application du rendu d’animation avec la classe CompositionTarget
• gérer le déplacement de la raquette et de la balle
• détection des collisions entre la balle et les autres objets 3D comme la
raquette, les briques et les murs
• mise en place de l’affichage du temps et du score
• gérer la fin de jeu
252 Programmez des jeux vidéo 3D avec C#5 et WPF
Le but de ce chapitre consiste à construire le jeu du Casse-Briques. Les différents objets
3D sont modélisés et ils sont ajoutés à la scène 3D. Puis, par l’intermédiaire d’un rendu
de type image par image, le jeu est animé pour permettre à l’utilisateur de jouer avec
une raquette déplaçable horizontalement, qui renvoie une balle pour détruire des
briques qui sont positionnées sur le plan du jeu, et tout cela dans un environnement
3D.
Pour permettre une meilleure utilisation à la fois sur un ordinateur de bureau et sur
une tablette tactile, le jeu va être réalisé pour remplir le bureau d’une tablette tactile
de 10.1 pouces (soit une résolution d’écran de 1280 par 800 pixels).

1 - Visualisation avec Windows 7 et avec Windows 8

Avec une seule solution de projet, nous réalisons une application de jeu qui fonctionne
à la fois sur un ordinateur de bureau équipé d’un Windows 7 (figure 7.1) et sur une
tablette tactile de 10.1 pouces, équipée de Windows 8 (figure 7.2).
La tablette tactile utilisée ici est une tablette ASUS EP121, dotée d’un processeur Core
i3, avec une RAM de 2Go, et un système d’exploitation Windows 8 (en mode 64 bits).
Son écran de 10.1 pouces possède une résolution écran de 1280 par 800 pixels.
Comme vous pouvez le constater sur la figure 7.2, le rendu 2D de la scène 3D (par
l’intermédiaire du Viewport3D) occupe tout l’espace disponible sur le bureau de la
tablette tactile. Et pour l’application de jeu sur l’ordinateur de bureau (figure 7.1), on
définit une fenêtre de taille calculée pour remplir le même espace que sur un écran
de 10.1 pouces (celui de la tablette). La solution de projet CasseBrique3D.sln se trouve
dans le dossier 02_VS2012_3d_c5_wpf45_jeu/CasseBrique3D du code en téléchargement
gratuit sur www.reypatrice.fr.
Le jeu est composé d’objets 3D que l’on modélise au préalable (comme on l’a vu dans
la première partie du livre), puis on ajoute ces objets 3D à la scène 3D et on les anime
pour réaliser le jeu. L’animation consiste à donner la possibilité à la balle de se déplacer
dans l’espace 3D. La trajectoire de cette balle va être modifiée lorsqu’elle rencontrera
les murs de droite, de gauche et du fond. Sa trajectoire changera aussi lorsqu’elle tapera
Copyright 2013 Patrice REY

contre la raquette déplacée par l’utilisateur, et bien évidemment contre les briques
qu’elle détruira. En cours de jeu, un panneau permettra d’afficher le nombre de points
obtenus en fonction des briques cassées, ainsi qu’un indicateur de temps permettant
de connaître le temps écoulé jusqu’à la fin du jeu.
Pour percevoir l’ensemble de ces déplacements et autres effets, l’animation du jeu
consiste aussi à effectuer un rendu image par image, avec une cadence généralement
constatée de 60 images par seconde.
FIGURE
7.1
FIGURE
7.2
CHAPITRE 7 Construction du jeu du Casse-Briques 255

2 - Mise en place du jeu

La construction du jeu, dans sa version minimale mais complète et effective, se fait


en plusieurs étapes. La première étape consiste à définir les limites de la fenêtre
qui va héberger le jeu en 3D. La deuxième étape consiste à modéliser les objets 3D
nécessaires au jeu, et à les positionner sur la scène 3D. La troisième étape consiste à
mettre en place un rendu image par image avec ses caractéristiques et ses fonctions.
La quatrième étape, qui est l’ultime étape, consiste à apporter l’animation nécessaire
pour rendre vivant le jeu avec les différentes interactions entre l’utilisateur et la logique
de jeu.

2.1 - Les dimensions utiles

Commençons par définir la surface disponible pour le jeu sachant qu’il doit occuper
au moins la surface maximale d’un écran 10.1 pouces d’une tablette tactile. La classe
Screen représente le périphérique graphique de visualisation c’est-à-dire l’écran, qu’il
soit unique ou multiple (visualisation du bureau sur deux moniteurs par exemple).
Pour utiliser un objet Screen, il faut ajouter une référence à System.Windows.Forms.dll
(dans le dossier References) et une instruction using System.Windows.Forms pour son
utilisation. La figure 7.3 visualise l’arbre d’héritage de la classe Screen.
FIGURE 7.3
256 Programmez des jeux vidéo 3D avec C#5 et WPF
La classe Screen expose les propriétés suivantes:
• la propriété booléenne Primary permet d’obtenir une valeur pour savoir si l’écran
de visualisation est l’écran principal,
• la propriété DeviceName donne le nom associé à l’écran principal,
• la propriété Bounds permet d’obtenir la taille complète de l’écran en pixels (largeur
et hauteur),
• la propriété BitsPerPixel permet d’obtenir le nombre de bits pour la représentation
de la couleur d’un pixel,
• la propriété WorkingArea permet d’obtenir la surface utile du bureau c’est-à-dire la
surface ne contenant pas la barre des tâches et d’éventuelles fenêtres flottantes.
La méthode statique Screen.PrimaryScreen permet d’obtenir l’écran principal de
visualisation, et la méthode statique Screen.AllScreens permet d’obtenir un tableau
référençant tous les écrans disponibles du système.
On définit une classe Utils et on ajoute les méthodes statiques Utils.LargeurBureau
et Utils.HauteurBureau qui retournent respectivement la largeur et la hauteur de la
surface utile du bureau. Ces deux méthodes statiques s’appuient sur la propriété
WorkingArea de l’objet Screen.
public class Utils {
//trouver la largeur utile du bureau
public static int LargeurBureau() {
Rectangle rect_bureau= Screen.PrimaryScreen.WorkingArea;
return rect_bureau.Width;
}
//trouver la hauteur utile du bureau
public static int HauteurBureau() {
Rectangle rect_bureau = Screen.PrimaryScreen.WorkingArea;
return rect_bureau.Height;
}
}//end class
L’écran 10.1 pouces de la tablette tactile offre une résolution de 1280 par 800 pixels
(propriété Bounds de l’objet Screen). La hauteur de la barre des tâches est de 40 pixels
sous Windows 8 (même hauteur sous Windows 7). Par conséquent la surface utile du
bureau est de 1280 par 760 pixels.
Copyright 2013 Patrice REY

Dans le constructeur de la fenêtre de l’application (MainWindow.xaml.cs), on ajoute


des instructions conditionnelles pour fixer la surface utile minimale de l’application à
1280 par 760 pixels. De cette façon, sur la tablette tactile, la surface de l’application
occupera toute la surface du bureau, et sur un ordinateur de bureau avec un grand
écran, on se limitera à une surface maximale de 1280 par 760 pixels pour la fenêtre de
visualisation du jeu.
CHAPITRE 7 Construction du jeu du Casse-Briques 257

Apparence de la fenêtre sous Windows 8 sur la tablette tactile

760 pixels

1280 pixels

Apparence de la fenêtre sous Windows 7 sur un ordinateur de bureau

760 pixels

1280 pixels

public partial class MainWindow : Window {


//constructeur
public MainWindow() {
InitializeComponent();
if (Utils.LargeurBureau() > 1280) {
this.Width = 1280;
}
258 Programmez des jeux vidéo 3D avec C#5 et WPF
else {
this.Width = Utils.LargeurBureau();
}
if (Utils.HauteurBureau() > 800) {
this.Height = 800 - 40;
}
else {
this.Height = Utils.HauteurBureau();
}
}
... }

2.2 - Les éléments de la scène 3D

Comme on l’a vu dans la première partie du livre, un objet Viewport3D sert à effectuer
un rendu 2D d’une scène 3D. On insère l’objet Viewport3D dans un contrôle Border,
d’épaisseur 1 pixel et de couleur noire, car cela permet d’ajouter un fond au Viewport3D
par transparence. Ici, au lieu d’utiliser un fond bleu clair, on va peindre le fond du
Border par une image dont le fichier graphique représente un ciel avec des nuages
(image fond_ciel_4.jpg ajoutée au dossier contenu/image_texture). Pour que le contrôle
Border s’étende sur toute la surface utile, on le positionne dans un contrôle Grid avec
une seule cellule. La figure 7.4 visualise ce que l’on obtient dans le concepteur de vue
XAML.
FIGURE 7.4

Copyright 2013 Patrice REY


CHAPITRE 7 Construction du jeu du Casse-Briques 259

<Grid Background=ˈ#FFD6F0FFˈ>
<Grid.RowDefinitions>
<RowDefinition Height=ˈ*ˈ/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=ˈ*ˈ/>
</Grid.ColumnDefinitions>
<!-- 3d -->
<Border BorderBrush=ˈBlackˈ BorderThickness=ˈ1ˈ MouseMove=ˈBorder_MouseMoveˈ>
<Border.Background>
<ImageBrush ImageSource=ˈcontenu/image_texture/fond_ciel_4.jpgˈ/>
</Border.Background>
<!-- zone de rendu 3d -->
<Viewport3D x:Name=ˈx_viewportˈ ClipToBounds=ˈTrueˈ>
...
</Viewport3D>
</Border>
...
</Grid>
Une caméra, de type perspective, est positionnée au point de coordonnées (0,2,6) par
la propriété Position, fixant l’origine par une propriété LookDirection (0,-2,-6), avec un
champs de vue de 70° (propriété FieldOfView), et une orientation de caméra vers le
haut (propriété UpDirection). Le volume de vue s’étend en distance de 0 (propriété
NearPlaneDistance) à 100 (propriété FarPlaneDistance).
<!-- cameras -->
<Viewport3D.Camera>
<PerspectiveCamera x:Name=ˈx_cameraˈ Position=ˈ0,2,6ˈ LookDirection=ˈ0,-2,-6ˈ
FieldOfView=ˈ70ˈ UpDirection=ˈ0 1 0ˈ NearPlaneDistance=ˈ0ˈ
FarPlaneDistance=ˈ100ˈ></PerspectiveCamera>
</Viewport3D.Camera>
Le contenu du Viewport3D est représenté par un objet ContainerUIElement3D intitulé
x_conteneur_3d. On positionne un éclairage général, de type AmbientLight, émettant
une lumière blanche (propriété Color fixée à White).
<ContainerUIElement3D x:Name=ˈx_conteneur_3dˈ>
<ModelUIElement3D x:Name=ˈx_lumiereˈ>
<AmbientLight Color=ˈWhiteˈ></AmbientLight>
</ModelUIElement3D>
...
</ContainerUIElement3D>
De façon à faciliter le positionnement des objets 3D dans le concepteur de vue XAML,
on va ajouter un repère de coordonnées 3D. Ce repère sera caché bien évidemment lors
de l’exécution du jeu. On procède ainsi car les objets 3D modélisés vont être ajoutés
directement en XAML, avec leur personnalisation au travers de leurs propriétés de
260 Programmez des jeux vidéo 3D avec C#5 et WPF
dépendances. La figure 7.5 visualise l’ajout d’un repère de coordonnées 3D dans le
concepteur XAML (repère 1) et avec la possibilité de personnaliser les propriétés de
dépendances (repère 2).
Dans le dossier modele3d, on ajoute une classe TRepere qui hérite de UIElement3D.
Cette classe TRepere expose les propriétés suivantes:
• P_Modele, de type Model3D, représente le modèle TRepere avec ses fonctionnalités,
• P_LongueurDemiAxe, de type double, représente la longueur d’un demi-axe à
appliquer pour les 3 axes (valeur par défaut 1d),
• P_EpaisseurAxe, de type double, représente l’épaisseur de la boite qui modélise
l’axe (valeur par défaut 0.01d),
• P_CouleurAxeX, de type Color, représente la couleur à appliquer pour matérialiser
l’axe X (valeur par défaut Red),
• P_CouleurAxeY, de type Color, représente la couleur à appliquer pour matérialiser
l’axe Y (valeur par défaut Green),
• P_CouleurAxeZ, de type Color, représente la couleur à appliquer pour matérialiser
l’axe Z (valeur par défaut Blue).
Ces propriétés de dépendances peuvent être personnalisées directement en XAML
(repère 1 et 2 de la figure 7.5). A noter que pour importer un objet TRepere appartenant
à l’espace de noms CasseBrique3D.modele3d, il faut ajouter une référence xmlns à cet
espace de noms, que l’on préfixe par un identifiant arbitraire comme modele3d par
exemple (xmlns:modele3d = «clr-namespace:CasseBrique3D.modele3d»).
FIGURE 7.5

Copyright 2013 Patrice REY

1
CHAPITRE 7 Construction du jeu du Casse-Briques 261

namespace CasseBrique3D.modele3d {
public class TRepere : UIElement3D {
//la propriete Modele pour le repere
private static readonly DependencyProperty ProprieteModele =
DependencyProperty.Register(«P_Modele»,
typeof(Model3D),
typeof(TRepere),
new PropertyMetadata(PM_Modele));
//la propriete LongueurDemiAxe
private static readonly DependencyProperty ProprieteLongueurDemiAxe =
DependencyProperty.Register(«P_LongueurDemiAxe»,
typeof(double),
typeof(TRepere),
new PropertyMetadata(1.0, PM_LongueurDemiAxe));
//la propriete EpaisseurAxe
private static readonly DependencyProperty ProprieteEpaisseurAxe =
DependencyProperty.Register(«P_EpaisseurAxe»,
typeof(double),
typeof(TRepere),
new PropertyMetadata(0.01, PM_EpaisseurAxe));
//la propriete CouleurAxeX
private static readonly DependencyProperty ProprieteCouleurAxeX =
DependencyProperty.Register(«P_CouleurAxeX»,
typeof(Color),
typeof(TRepere),
new PropertyMetadata(Colors.Red, PM_CouleurAxeX));
//la propriete CouleurAxeY
private static readonly DependencyProperty ProprieteCouleurAxeY =
DependencyProperty.Register(«P_CouleurAxeY»,
typeof(Color),
typeof(TRepere),
new PropertyMetadata(Colors.Green, PM_CouleurAxeY));
//la propriete CouleurAxeZ
private static readonly DependencyProperty ProprieteCouleurAxeZ =
DependencyProperty.Register(«P_CouleurAxeZ»,
typeof(Color),
typeof(TRepere),
new PropertyMetadata(Colors.Blue, PM_CouleurAxeZ));
//redefinition de la methode de rendu
protected override void OnUpdateModel() {
Model3DGroup groupe_axe = new Model3DGroup();
groupe_axe.Children.Add(GenererMaillage(P_LongueurDemiAxe, «x»));
groupe_axe.Children.Add(GenererMaillage(P_LongueurDemiAxe, «y»));
groupe_axe.Children.Add(GenererMaillage(P_LongueurDemiAxe, «z»));
P_Modele = groupe_axe;
}
...
}
}
262 Programmez des jeux vidéo 3D avec C#5 et WPF
Le plateau du jeu peut être matérialisé par exemple comme une simple face
rectangulaire, dont le centre de la face est positionné à l’origine du repère de
coordonnées (donc avec une coordonnée de valeur 0 selon l’axe Y), et dont on fixe
les dimensions selon les axes X et Z. Pour augmenter le réalisme de ce plateau, on
applique une image de texture à la face rectangulaire.
Y

P_LargeurXNeg P_LargeurXPos

P_ProfondeurZNeg

X
P_ProfondeurZPos

Z
Pour représenter le plateau, on déclare une classe TSol qui hérite de UIElement3D.
Cette classe TSol expose les propriétés suivantes:
• P_Modele, de type Model3D, représente le modèle TSol avec ses fonctionnalités,
• P_LargeurXPos et P_LargeurXNeg, de type double, représentent les distances des
côtés du plateau suivant l’axe X (valeur par défaut 1d),
• P_ProfondeurZPos et P_ProfondeurZNeg, de type double, représentent les distances
des côtés du plateau suivant l’axe Z (valeur par défaut 1d),
• P_CouleurUnie, de type Color, représente la couleur du plateau quand aucune
texture n’est appliquée (valeur par défaut Aqua),
• P_Texture, de type Material, représente la texture à appliquer sur la face du plateau
Copyright 2013 Patrice REY

(valeur par défaut null).


Pour ajouter un plateau dont les côtés selon X sont de longueurs 2 unités, et selon
Z, sont de longueurs 3.7 unités côté Z positif et 3.4 unités côté Z négatif, on écrira en
XAML:
<!-- sol du jeu -->
<modele3d:TSol x:Name=ˈx_solˈ P_ProfondeurZPos=ˈ3.7ˈ P_ProfondeurZNeg=ˈ3.4ˈ
CHAPITRE 7 Construction du jeu du Casse-Briques 263

P_LargeurXPos=ˈ2ˈ P_LargeurXNeg=ˈ2ˈ>
</modele3d:TSol>
Le repère 1 de la figure 7.6 visualise ce que l’on obtient dans le concepteur de vue XAML.
Le plateau est de couleur unie, celle choisie et fixée dans la propriété P_CouleurUnie.
Dans la méthode redéfinie OnUpdateModel, méthode qui dessine la modélisation 3D
de l’objet TSol (par héritage de UIElement3D), on ajoute une instruction conditionnelle
pour permettre l’application d’une texture sur la face rectangulaire si la propriété P_
Texture est différente de la valeur null. Le repère 2 de la figure 7.6 visualise le résultat
obtenu avec l’application d’une texture par l’intermédiaire d’une ressource image
(contenu/image_texture/texture_sol_1.png).
FIGURE 7.6

2
264 Programmez des jeux vidéo 3D avec C#5 et WPF
//redefinition de la methode de rendu
protected override void OnUpdateModel() {
GeometryModel3D geo = new GeometryModel3D();
geo.Geometry = Maillage(this.P_LargeurXPos, this.P_LargeurXNeg,
this.P_ProfondeurZPos, this.P_ProfondeurZNeg);
if (this.P_Texture == null) {
geo.Material = new DiffuseMaterial(new SolidColorBrush(this.P_CouleurUnie));
}
else {
geo.Material = this.P_Texture;
}
geo.BackMaterial = new DiffuseMaterial(new SolidColorBrush(Colors.Yellow));
P_Modele = geo;
}
Pour ajouter une texture, on affecte un objet de type Material à la propriété P_Texture
(en écrivant <modele3d:TSol.P_Texture>). On ajoute un objet de type DiffuseMaterial
(qui hérite de Material) et on applique à sa propriété Brush un objet ImageBrush,
dont la propriété ImageSource de ImageBrush référence une image texture_sol_1.png
stockée dans le dossier contenu/image_texture.
<!-- sol du jeu -->
<modele3d:TSol x:Name=ˈx_solˈ P_ProfondeurZPos=ˈ3.7ˈ P_ProfondeurZNeg=ˈ3.4ˈ
P_LargeurXPos=ˈ2ˈ P_LargeurXNeg=ˈ2ˈ>
<modele3d:TSol.P_Texture>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<ImageBrush ImageSource=ˈcontenu/image_texture/texture_sol_1.pngˈ >
</ImageBrush>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</modele3d:TSol.P_Texture>
</modele3d:TSol>
Il faut maintenant poser des bordures autour du plateau du jeu pour limiter l’espace
de jeu. On commence par ajouter un objet TAfficheMurale qui représente une affiche
murale et qui est positionnée au fond du plateau du jeu. C’est en fait un mur qui
permettra de renvoyer la balle quand cette dernière le touchera.
Cette affiche est constituée par le maillage simple d’une surface rectangulaire, aux
Copyright 2013 Patrice REY

dimensions choisies, sur laquelle on appliquera une texture sous forme d’image
graphique. De façon à positionner plus facilement cet objet dans l’espace 3D, on va
ajouter un point de référence Pr, qui est en sorte un point à partir duquel l’objet
sera modélisé. Si ce point est fixé aux coordonnées de l’origine, alors Pr se trouvera à
l’origine du repère et l’objet sera modélisé en rapport avec ce point de référence. De
cette façon, on peut directement modéliser l’objet en fixant simplement un point de
référence choisi.
CHAPITRE 7 Construction du jeu du Casse-Briques 265

P_Largeur

Y
TAfficheMurale x_affiche_murale_fond
P_Hauteur
Pr

Pr : point de référence

X
TSol x_sol

L’objet nommé x_affiche_murale_fond, de type TAfficheMurale, qui hérite de


UIElement3D, expose les propriétés suivantes:
• P_Modele, de type Model3D, représente le modèle TAfficheMurale avec ses
fonctionnalités,
• P_Largeur et P_Hauteur, de type double, représentent les distances des côtés de
l’affiche suivant l’axe X et l’axe Y (valeur par défaut 1d),
• P_CouleurUnie, de type Color, représente la couleur de l’affiche quand aucune
texture n’est appliquée (valeur par défaut Beige),
• P_Texture, de type Material, représente la texture à appliquer sur la face de l’affiche
(valeur par défaut null),
• P_PointReference, de type Point3D, représente le point à partir duquel l’affiche est
modélisée (valeur par défaut aux coordonnées 0,0,0).
//la propriete PointReference (point de reference 3d pour le calcul)
private static readonly DependencyProperty ProprietePointReference =
DependencyProperty.Register(«P_PointReference»,
typeof(Point3D),
typeof(TAfficheMurale),
new PropertyMetadata(new Point3D(0,0, 0), PM_PointReference));
private static void PM_PointReference(DependencyObject d,
DependencyPropertyChangedEventArgs e) {
TAfficheMurale affiche = (TAfficheMurale)d;
affiche.InvalidateModel(); }
266 Programmez des jeux vidéo 3D avec C#5 et WPF
public Point3D P_PointReference {
get { return (Point3D)GetValue(ProprietePointReference); }
set { SetValue(ProprietePointReference, value); }
}
Comme le visualise la figure 7.7, dans le concepteur de vue XAML, on ajoute un
objet <modele3d:TAfficheMurale>. On personnalise sa taille (propriétés P_Largeur et
P_Hauteur), son emplacement à partir d’un point de référence pour la modélisation
(propriété P_PointReference), et l’application d’une texture (propriété P_Texture).
Comme le point de référence est de type Point3D, on développe la propriété P_
PointReference par l’écriture <modele3d:TAfficheMurale.P_PointReference> et on lui
affecte un objet <Point3D> en personnalisant ses propriétés X, Y et Z (par des valeurs
de type double).
FIGURE 7.7

2
Copyright 2013 Patrice REY

<!-- affiche murale au fond -->


<modele3d:TAfficheMurale x:Name=ˈx_affiche_murale_fondˈ P_Largeur=ˈ4ˈ
P_Hauteur=ˈ1.3ˈ>
<modele3d:TAfficheMurale.P_PointReference>
<Point3D X=ˈ-2ˈ Y=ˈ0ˈ Z=ˈ-3.4ˈ></Point3D>
CHAPITRE 7 Construction du jeu du Casse-Briques 267

</modele3d:TAfficheMurale.P_PointReference>
<modele3d:TAfficheMurale.P_Texture>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<ImageBrush ImageSource=ˈcontenu/image_texture/texture_affiche_1.pngˈ />
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</modele3d:TAfficheMurale.P_Texture>
</modele3d:TAfficheMurale>
Pour terminer la limitation de la surface du jeu par des objets fixes, on va ajouter à
droite et à gauche un mur. Pour cela, on va modéliser un cube particulier pour lequel
on pourra lui donner une largeur, une hauteur, une profondeur et une texture pour
chaque face.
On déclare une classe TCubeDetailFace qui hérite de UIElement3D et qui expose les
propriétés suivantes:
• P_Modele, de type Model3D, représente le modèle TCubeDetailFace avec ses
fonctionnalités,
• P_PointReference, de type Point3D, représente le point à partir duquel l’affiche est
modélisée (valeur par défaut aux coordonnées 0,0,0).,
• P_LargeurX, de type double, représente la largeur du cube suivant l’axe X (valeur
par défaut 1d),
• P_HauteurY, de type double, représente la hauteur du cube suivant l’axe Y (valeur
par défaut 1d),
• P_ProfondeurZ, de type double, représente la profondeur du cube suivant l’axe Z
(valeur par défaut 1d),
• P_CouleurUnie, de type Color, représente la couleur du cube sur toutes ses faces
quand aucune texture n’est appliquée (valeur par défaut Red),
• les propriétés P_TextureFaceAvant, P_TextureFaceDroite, P_TextureFaceGauche, P_
TextureFaceArriere, P_TextureFaceDessus, P_TextureFaceDessous, de type Material,
représentent les textures à appliquer sur les faces du cube respectivement (valeur
par défaut null).
En XAML (figure 7.8), on ajoute un mur côté gauche nommé x_mur_gauche et un
mur côté droit nommé x_mur_droit. On personnalise le point de référence pour la
modélisation, les tailles des murs selon les 3 axes et les textures à appliquer en fonction
des faces choisies.
Pour le mur droit, on ajoutera une texture sur la face de gauche et la face du dessus.
Pour le mur gauche, on ajoutera une texture sur la face de droite et la face du dessus.
Les autres faces resteront avec la couleur par défaut (Red) car elles ne seront pas
visibles.
268 Programmez des jeux vidéo 3D avec C#5 et WPF
FIGURE 7.8

1 1

Y TAfficheMurale x_affiche_murale_fond

TCubeDetailFace
x_mur_gauche

TCubeDetailFace
x_mur_droit

TSol x_sol
Z Copyright 2013 Patrice REY

P_ProfondeurZ

P_HauteurY
Pr
Pr : point de référence

P_LargeurX
CHAPITRE 7 Construction du jeu du Casse-Briques 269

<!-- mur cote droit -->


<modele3d:TCubeDetailFace x:Name=ˈx_mur_droitˈ P_CouleurUnie=ˈ#FF0046FFˈ
P_LargeurX=ˈ0.5ˈ P_ProfondeurZ=ˈ7.2ˈ P_HauteurY=ˈ1ˈ>
<modele3d:TCubeDetailFace.P_PointReference>
<Point3D X=ˈ2ˈ Y=ˈ0ˈ Z=ˈ3.7ˈ></Point3D>
</modele3d:TCubeDetailFace.P_PointReference>
<modele3d:TCubeDetailFace.P_TextureFaceGauche>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<ImageBrush ImageSource=ˈcontenu/image_texture/texture_rectangle_mur_3.pngˈ
/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</modele3d:TCubeDetailFace.P_TextureFaceGauche>
<modele3d:TCubeDetailFace.P_TextureFaceDessus>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<ImageBrush ImageSource=ˈcontenu/image_texture/texture_rectangle_mur_1.pngˈ
/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</modele3d:TCubeDetailFace.P_TextureFaceDessus>
</modele3d:TCubeDetailFace>
<!-- mur cote gauche -->
<modele3d:TCubeDetailFace x:Name=ˈx_mur_gaucheˈ P_CouleurUnie=ˈ#FF0046FFˈ
P_LargeurX=ˈ0.5ˈ P_ProfondeurZ=ˈ7.2ˈ P_HauteurY=ˈ1ˈ>
<modele3d:TCubeDetailFace.P_PointReference>
<Point3D X=ˈ-2.5ˈ Y=ˈ0ˈ Z=ˈ3.7ˈ></Point3D>
</modele3d:TCubeDetailFace.P_PointReference>
<modele3d:TCubeDetailFace.P_TextureFaceDroite>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<ImageBrush ImageSource=ˈcontenu/image_texture/texture_rectangle_mur_2.
pngˈ/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</modele3d:TCubeDetailFace.P_TextureFaceDroite>
<modele3d:TCubeDetailFace.P_TextureFaceDessus>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<ImageBrush ImageSource=ˈcontenu/image_texture/texture_rectangle_mur_1.pngˈ
/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</modele3d:TCubeDetailFace.P_TextureFaceDessus>
</modele3d:TCubeDetailFace>
public class TCubeDetailFace : UIElement3D {
...
//redefinition de la methode de rendu
270 Programmez des jeux vidéo 3D avec C#5 et WPF
protected override void OnUpdateModel() {
Model3DGroup geo = new Model3DGroup();
//face gauche
GeometryModel3D geo_face_gauche = new GeometryModel3D();
geo_face_gauche.Geometry = MaillageFaceGauche(this.P_PointReference,
this.P_LargeurX, this.P_HauteurY, this.P_ProfondeurZ);
if (this.P_TextureFaceGauche != null) {
geo_face_gauche.Material = this.P_TextureFaceGauche;
}
else {
geo_face_gauche.Material = new DiffuseMaterial(
new SolidColorBrush(this.P_CouleurUnie));
}
geo_face_gauche.BackMaterial = new DiffuseMaterial(
new SolidColorBrush(Colors.Yellow));
//face avant
GeometryModel3D geo_face_avant = new GeometryModel3D();
...
//composition du modele
geo.Children.Add(geo_face_gauche);
geo.Children.Add(geo_face_avant);
geo.Children.Add(geo_face_droite);
geo.Children.Add(geo_face_arriere);
geo.Children.Add(geo_face_dessus);
geo.Children.Add(geo_face_dessous);
P_Modele = geo;
}
Pour décorer le plateau du jeu, on va insérer de chaque côté un cylindre surmonté par
un cône. On positionne à droite un cylindre, de type TCylindre, nommé x_cylindre_
droit, et un cône, de type TCone, nommé x_cone_droit. On procède de façon identique
à gauche.
Le cylindre TCylindre expose les propriétés suivantes:
• P_Modele, de type Model3D, représente le modèle TCylindre avec ses
fonctionnalités,
• P_PointReference, de type Point3D, représente le point à partir duquel le cylindre
est modélisé (valeur par défaut aux coordonnées 0,0,0).,
• P_Hauteur, de type double, représente la hauteur du cylindre suivant l’axe Y (valeur
Copyright 2013 Patrice REY

par défaut 1d),


• P_RayonExterne, de type double, représente le rayon de la face externe du cylindre
(valeur par défaut 1d),
• P_RayonInterne, de type double, représente le rayon de la face interne du cylindre
(valeur par défaut 1d),
• P_NombreDivision, de type int, représente le nombre de division pour les facettes
du cylindre (valeur par défaut 40),
CHAPITRE 7 Construction du jeu du Casse-Briques 271

• P_CouleurUnieFaceExterne, P_CouleurUnieFaceInterne, P_CouleurUnieFaceDessus,


P_CouleurUnieFaceDessous, de type Color, représentent les couleurs des faces
respectives du cylindre (valeur par défaut Blue, Orange, Red et Red),
• les propriétés P_TextureFaceExterne, P_TextureFaceInterne, P_TextureFaceDessus, P_
TextureFaceDessous, de type Material, représentent les textures à appliquer sur les
faces du cylindre respectivement (valeur par défaut null).
Le cône TCone expose les propriétés suivantes:
• P_Modele, de type Model3D, représente le modèle TCone avec ses fonctionnalités,
• P_PointReference, de type Point3D, représente le point à partir duquel le cône est
modélisé (valeur par défaut aux coordonnées 0,0,0).,
• P_Hauteur, de type double, représente la hauteur du cône suivant l’axe Y (valeur
par défaut 1d),
• P_RayonExterne, de type double, représente le rayon de la base du cône (valeur par
défaut 1d),
• P_NombreDivision, de type int, représente le nombre de division pour les facettes
du cône (valeur par défaut 40),
• P_CouleurUnieFaceDessous, de type Color, représente la couleur de la base du cône
(valeur par défaut Red),
• P_CouleurDegradeFaceExterne, de type LinearGradientBrush, représente le dégradé
de couleurs de la face externe du cône (valeur par défaut LinearGradientBrush
(Colors.Blue,Colors.LightYellow,0) ),
Les figures 7.9 et 7.10 visualisent les cylindres et les cônes qui sont positionnés en
XAML sur la scène avec leurs personnalisations respectives.
FIGURE 7.9

1 1

1
272 Programmez des jeux vidéo 3D avec C#5 et WPF
FIGURE 7.10

1
1

Maintenant nous allons ajouter la raquette. La raquette est l’objet qui sert à renvoyer
la balle. Pour cela, on va modéliser une raquette en forme de cube allongé pour lequel
on applique une texture unique pour le décorer. On déclare une classe TCube et on lui
ajoute les propriétés de dépendances suivantes:
• P_Modele, de type Model3D, représente le modèle TCube avec ses fonctionnalités,
• P_PointReference, de type Point3D, représente le point à partir duquel le cube est
modélisé (valeur par défaut aux coordonnées 0,0,0).,
• P_LargeurX, de type double, représente la largeur du cube suivant l’axe X (valeur
par défaut 1d),
• P_HauteurY, de type double, représente la hauteur du cube suivant l’axe Y (valeur
par défaut 1d),
• P_ProfondeurZ, de type double, représente la profondeur du cube suivant l’axe Z
(valeur par défaut 1d),
Copyright 2013 Patrice REY

• P_CouleurUnie, de type Color, représente la couleur du cube sur toutes ses faces
quand aucune texture n’est appliquée (valeur par défaut Red),
• P_Texture, de type Material, représente la texture à appliquer sur le cube dans son
intégralité (valeur par défaut null).
D’autres fonctionnalités seront ajoutées par la suite pour l’animation de la raquette. La
figure 7.11 visualise la raquette que l’on positionne dans la partie basse du plateau du
CHAPITRE 7 Construction du jeu du Casse-Briques 273

jeu. Cette raquette se déplacera horizontalement de droite à gauche et inversement


avec le déplacement de la souris.
FIGURE 7.11

1 1

<!-- raquette -->


<modele3d:TCube x:Name=ˈx_raquetteˈ P_LargeurX=ˈ0.5ˈ P_HauteurY=ˈ0.10ˈ
P_ProfondeurZ=ˈ0.10ˈ>
<modele3d:TCube.P_PointReference>
<Point3D X=ˈ-0.25ˈ Y=ˈ0.05ˈ Z=ˈ3.5ˈ></Point3D>
</modele3d:TCube.P_PointReference>
<modele3d:TCube.P_Texture>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<ImageBrush ImageSource=ˈcontenu/image_texture/texture_cube_3.pngˈ>
</ImageBrush>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</modele3d:TCube.P_Texture>
</modele3d:TCube>

2.3 - Le rendu d’animation

Quand le jeu démarre, l’utilisateur doit pouvoir bouger la raquette pour renvoyer la
balle. Intuitivement, on perçoit que, pour exécuter un déplacement de la raquette en
fonction d’un événement déclenché par la souris, il faudra effectuer des translations
dans l’espace 3D. Or, pour que le mouvement de la raquette soit visible à l’écran, il va
falloir réaliser un rendu image par image. De cette façon, dès que l’on effectuera une
translation de la raquette, celle-ci sera vue car on aura un rafraîchissement de l’écran
de jeu qui permettra de visualiser la position de la raquette en temps réel.
La classe abstraite CompositionTarget, dans l’espace de noms System.Windows.
274 Programmez des jeux vidéo 3D avec C#5 et WPF
Media, permet d’effectuer un rendu image par image de la surface de visualisation
de l’application. La figure 7.12 montre l’arbre d’héritage de la classe abstraite
CompositionTarget. L’objet CompositionTarget fournit la capacité à créer des
animations personnalisées qui sont basées sur un rendu image par image. Généralement
on constate que la vitesse de ce rendu image par image est de l’ordre de 60 images par
seconde, ce qui est très correct et qui apporte un excellent réalisme au jeu.
La classe CompositionTarget est une classe abstraite, et par conséquent non
instanciable. Elle possède une méthode statique CompositionTarget.Rendering pour
accéder à l’événement Rendering. Cet événement se produit juste avant que les objets
de l’arbre visuel soient rendus. Donc l’événement Rendering permet d’effectuer le
rendu de la scène (c’est la boucle de rendu).
FIGURE 7.12

Il faut un déclencheur pour permettre de démarrer le rendu d’animation. L’option la


meilleure consiste à positionner sur la fenêtre d’application un Canvas dont le fond est
semi-transparent pour laisser entrevoir l’agencement du jeu. Ce Canvas, nommé x_
cnv_lancer, contient un bouton x_btn_lancer. Le gestionnaire de l’événement Click pour
ce bouton déclenchera le début du rendu d’animation image par image (le Canvas x_
Copyright 2013 Patrice REY

cnv_lancer aura sa visibilité qui passe à l’énumération Visibility.Collapsed). Le jeu pourra


alors commencer.
//bouton de lancement du jeu
private void x_btn_lancer_Click(object sender, RoutedEventArgs e) {
x_cnv_lancer.Visibility = Visibility.Collapsed;
CompositionTarget.Rendering += CompositionTarget_Rendering;
...}
CHAPITRE 7 Construction du jeu du Casse-Briques 275

//rendu image par image


private void CompositionTarget_Rendering(object sender, EventArgs e) {
... }
FIGURE 7.13

2
276 Programmez des jeux vidéo 3D avec C#5 et WPF
La mesure du temps dans un jeu est essentielle. Il faut pouvoir démarrer un jeu
avec un relevé d’horloge, il faut pouvoir mesurer le temps écoulé entre deux relevés
d’horloge, et quand on veut mettre un jeu en pause, il faut pouvoir suspendre l’horloge
et l’animation du jeu pour pouvoir ensuite la reprendre. Nous allons utiliser pour cela
la classe Stopwatch qui se trouve dans l’espace de noms System.Diagnostics. La classe
Stopwatch fournit un ensemble de méthodes et de propriétés utilisables pour des
mesures pertinentes du temps. La figure 7.14 visualise l’arbre d’héritage de la classe
Stopwatch.
FIGURE 7.14

La classe Stopwatch expose principalement les propriétés et les méthodes suivantes:


Copyright 2013 Patrice REY

• la méthode Start permet de démarrer l’horloge et la méthode Stop permet de


suspendre l’horloge,
• la méthode StartNew permet de remettre l’horloge à zéro et de redémarrer le
comptage du temps,
• la propriété Elapsed permet d’obtenir le temps écoulé (sous forme d’un objet
TimeSpan) et la propriété ElapsedMilliseconds permet d’obtenir ce temps écoulé
CHAPITRE 7 Construction du jeu du Casse-Briques 277

en millisecondes,
• la méthode Reset stoppe l’horloge et remet à zéro le temps écoulé (propriété
Elapsed).
Sur la surface de l’application, en haut et à gauche (figure 7.15), on installe un
indicateur de temps en incrustation par l’intermédiaire d’un Grid x_grid_temps. Cette
grille comporte un TextBlock x_text_horloge pour indiquer le temps écoulé (au format
HH:MM:SS.MS), un TextBlock x_text_nb_img pour indiquer le nombre d’images rendues
par la boucle de rendu (c’est-à-dire le nombre de fois où l’événement Rendering a été
effectué), et un TextBlock x_text_fps pour indiquer le nombre d’image par seconde (la
cadence correspondant au nombre d’images rendues pour une unité de temps de une
seconde).
FIGURE 7.15

TextBlock x_text_horloge
TextBlock x_text_nb_img

TextBlock x_text_fps

Heure : Minute : Seconde . Milliseconde

On déclare une horloge v_stopwatch de type Stopwatch et un compteur d’images


rendues par une variable v_nb_image de type int. La méthode Start permet de démarrer
le comptage du temps lors du lancement du jeu.
//bouton de lancement du jeu
private void x_btn_lancer_Click(object sender, RoutedEventArgs e) {
x_cnv_lancer.Visibility = Visibility.Collapsed;
CompositionTarget.Rendering += CompositionTarget_Rendering;
v_stopwatch.Start();
...}
L’affichage du temps écoulé se fait par la propriété Elapsed de l’objet Stopwatch.
L’affichage du nombre d’images rendues se fait par la lecture de la variable v_nb_image.
Pour afficher le taux d’images par seconde, on utilise la variable locale taux_fps, de
type long, qui est le produit de v_nb_image par le temps écoulé exprimé en secondes
(propriété TotalSeconds du TimeSpan renvoyé par la propriété Elapsed du Stopwatch).
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
278 Programmez des jeux vidéo 3D avec C#5 et WPF
v_nb_image++;
x_text_horloge.Text = v_stopwatch.Elapsed.ToString();
x_text_nb_img.Text = v_nb_image.ToString();
long taux_fps = (long)(v_nb_image / this.v_stopwatch.Elapsed.TotalSeconds);
if (taux_fps > 0) {
x_text_fps.Text = taux_fps.ToString();
}
...
}

2.4 - Le déplacement de la raquette

Le déplacement de la raquette va se faire par l’intermédiaire du déplacement de la souris


sur le contrôle Border x_bordure. Comme la raquette ne se déplace qu’horizontalement
de droite à gauche et inversement (figure 7.16) en fonction de la position de la souris,
il faudra donc appliquer des translations nécessaires pour déplacer l’objet x_raquette le
long de l’axe X de l’espace 3D.
FIGURE 7.16

Copyright 2013 Patrice REY

On ajoute un gestionnaire pour l’événement MouseMove sur x_bordure. On relève


la position de la souris v_pos_souris sur x_bordure par la méthode GetPosition sur
l’élément x_bordure. Comme la largeur de la surface du jeu est de 1280 pixels, on
CHAPITRE 7 Construction du jeu du Casse-Briques 279

évalue, au travers de la variable v_deplac, la distance entre la position de la souris et


le milieu de la surface du jeu horizontalement (v_deplac = v_pos_souris.X - 640). Pour
déplacer effectivement la raquette, dans la boucle de rendu, on applique la méthode
AppliquerTranslation à x_raquette pour un déplacement selon l’axe X de v_deplac/320d.
La variable v_deplac est divisée par 320 pour faire correspondre le déplacement de la
souris en 2D au déplacement de la raquette en 3D.
private void Border_MouseMove(object sender, MouseEventArgs e) {
UIElement elem = (UIElement)sender;
v_pos_souris = e.GetPosition(elem);
//x_infos.Text = v_pos_souris.X.ToString(«.00»)
+ v_pos_souris.Y.ToString(«.00»);
v_deplac = v_pos_souris.X - 640;
}
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
if (v_etat_jeu == EtatJeu.en_cours) {
x_raquette.AppliquerTranslation(v_deplac / 320d, 0, 0);
...
}
La méthode TCube.AppliquerTranslation reçoit en paramètre une valeur selon X, une
valeur selon Y et une valeur selon Z. Pour effectuer une translation en 3D, on applique
à la propriété Transform de TCube un objet TranslateTransform3D avec les 3 valeurs de
déplacement. Et on invoque la méthode InvalidateModel pour redessiner la raquette.
public class TCube : UIElement3D {
...
//appliquer une translation au cube
public void AppliquerTranslation(double suiv_x, double suiv_y, double suiv_z) {
this.Transform = new TranslateTransform3D(suiv_x, suiv_y, suiv_z);
this.InvalidateModel();
}
...
}
Comme le jeu passe par plusieurs étapes d’activité et d’inactivité, on ajoute une
variable v_etat_jeu, de type EtatJeu, qui stocke l’étape d’activité ou d’inactivité du jeu.
L’énumération EtatJeu créée possède comme drapeaux les énumérations EtatJeu.arret,
EtatJeu.en_cours et EtatJeu.terminer.
Au début, la variable v_etat_jeu est initialisée à EtatJeu.arret. Quand le jeu démarre, elle
passe à l’énumération EtatJeu.en_cours. Dès que la partie est gagnée ou perdue, le jeu
prend fin et la variable passe à l’énumération EtatJeu.terminer. C’est pourquoi, dans la
boucle de rendu, la mise à jour du déplacement de la raquette par translation se fait
280 Programmez des jeux vidéo 3D avec C#5 et WPF
uniquement quand v_etat_jeu est à EtatJeu.en_cours.

2.5 - L’animation de la balle

Nous allons utiliser le maillage d’une sphère comme représentation de la balle. Pour
cela on déclare une classe TSphere, qui hérite de UIElement3D, dans l’espace de
noms CasseBrique3D.modele3d. L’objet TSphere expose principalement les propriétés
suivantes:
• P_Modele, de type Model3D, représente le modèle TSphere avec ses fonctionnalités,
• P_PointReference, de type Point3D, représente le point à partir duquel la sphère est
modélisée (valeur par défaut aux coordonnées 0,0,0),
• P_Rayon, de type double, représente le rayon de la sphère (valeur par défaut 1d),
• P_DivHori, de type int, représente le nombre de division horizontalement pour les
facettes de la sphère (valeur par défaut 40),
• P_DivVerti, de type int, représente le nombre de division verticalement pour les
facettes de la sphère (valeur par défaut 40),
• P_CouleurUnie, de type Color, représente la couleur appliquée sur les facettes qui
composent la sphère (valeur par défaut Bisque),
• P_Texture, de type Material, représente la texture à appliquer sur l’intégralité de la
sphère (valeur par défaut null).
Le point de référence utilisé pour la modélisation de la sphère est son centre. On utilise
la même technique de maillage que celle vue dans la première partie du livre.
//redefinition de la methode de rendu
protected override void OnUpdateModel() {
//face exterieure sphere
GeometryModel3D gp_geo_sphere = new GeometryModel3D();
gp_geo_sphere.Geometry = MaillageSphereFace(this.P_PointReference, this.P_Rayon,
this.P_DivHori, this.P_DivVerti);
if (this.P_Texture != null) {
gp_geo_sphere.Material = this.P_Texture;
}
else {
gp_geo_sphere.Material = new DiffuseMaterial(
Copyright 2013 Patrice REY

new SolidColorBrush(this.P_CouleurUnie));
}
gp_geo_sphere.BackMaterial = new DiffuseMaterial(
new SolidColorBrush(Colors.Yellow));
//
P_Modele = gp_geo_sphere;
...
}
CHAPITRE 7 Construction du jeu du Casse-Briques 281

Pour ajouter une balle x_balle (figure 7.17), de type TSphere, dont le rayon est de 0.1,
le nombre de division horizontal et vertical est de 20, et dont le point de référence
(centre de la sphère) est de coordonnées (0,0.1,3), on écrira en XAML:
<!-- balle sphere -->
<modele3d:TSphere x:Name=ˈx_balleˈ P_Rayon=ˈ0.1ˈ P_DivHori=ˈ20ˈ P_DivVerti=ˈ20ˈ>
<modele3d:TSphere.P_PointReference>
<Point3D X=ˈ0ˈ Y=ˈ0.1ˈ Z=ˈ3ˈ></Point3D>
</modele3d:TSphere.P_PointReference>
<modele3d:TSphere.P_Texture>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<ImageBrush ImageSource=ˈcontenu/image_texture/texture_sphere_1.pngˈ />
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</modele3d:TSphere.P_Texture>
</modele3d:TSphere>
FIGURE 7.17

1
1

L’animation de la balle se fait par déplacement de cette dernière en lui appliquant


des translations. La balle reste sur le plan du plateau du jeu, donc les translations à
effectuer se feront selon l’axe X et selon l’axe Z. La surface du plateau n’est pas très
grande en unités et comme la fréquence d’animation est de l’ordre de 60 images par
seconde, des petits déplacements selon X et Z suffiront à donner un réalisme aux
déplacements de la balle. Les variable v_vitesse_x et v_vitesse_z représenteront les
valeurs des translations à appliquer à la balle selon l’axe X et l’axe Z respectivement.
Elles seront affectées respectivement par la valeur des variables v_vitesse_taux_x et
v_vitesse_taux_z. Ces deux variables, v_vitesse_taux_x et v_vitesse_taux_z, possèderont
des valeurs en fonction de l’exécution du jeu.
282 Programmez des jeux vidéo 3D avec C#5 et WPF
Par exemple, pour déplacer une balle avec un angle de 45°, on peut affecter à v_vitesse_
taux_x et v_vitesse_taux_z une valeur de 1 unité. Si on affecte à ces deux variables une
valeur de 2 unités, l’angle de déplacement sera toujours de 45°, mais la vitesse effective
sera doublée puisque la translation appliquée sera deux fois plus grande.
Commençons par mettre en place le déplacement général de la balle. On définit une
variable v_vitesse_taux, de type double, que l’on initialise à 0.05d. Pour que la balle se
déplace au lancement du jeu vers le fond du plateau (c’est-à-dire selon l’axe Z), il faut
affecter à v_vitesse_taux_x une valeur de 0d et à v_vitesse_taux_z une valeur de -vitesse_
taux. Cela se traduit par une direction de déplacement selon l’axe Z du côté négatif.
//bouton de lancement du jeu
private void x_btn_lancer_Click(object sender, RoutedEventArgs e) {
x_cnv_lancer.Visibility = Visibility.Collapsed;
CompositionTarget.Rendering += CompositionTarget_Rendering;
v_etat_jeu = EtatJeu.en_cours;
//MessageBox.Show(AffichageDetailRect3D(x_mur_droit.P_BoiteEnglobante));
v_vitesse_taux_x = 0d;
v_vitesse_taux_z = -v_vitesse_taux;
v_stopwatch.Start();
... }
Pour rendre effectif ce déplacement, dans la boucle de rendu, on augmente la variable
v_vitesse_x de la valeur de v_vitesse_taux_x, et on augmente la variable v_vitesse_z de la
valeur de v_vitesse_taux_z. Puis on applique une translation à la balle, par la méthode
AppliquerTranslation, d’une valeur v_vitesse_x selon l’axe X et d’une valeur v_vitesse_z
selon l’axe Z. Le code de la boucle du rendu est donc le suivant:
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
if (v_etat_jeu == EtatJeu.en_cours) {
x_raquette.AppliquerTranslation(v_deplac / 320d, 0, 0);
v_vitesse_x += v_vitesse_taux_x;
v_vitesse_z += v_vitesse_taux_z;
x_balle.AppliquerTranslation(v_vitesse_x, 0, v_vitesse_z);
}
...
}
Copyright 2013 Patrice REY

La définition de la méthode TSphere.AppliquerTranslation est la même que celle vue


pour déplacer la raquette (TCube.AppliquerTranslation).
public class TSphere : UIElement3D {
...
//appliquer une translation a la sphere
public void AppliquerTranslation(double suiv_x, double suiv_y, double suiv_z) {
this.Transform = new TranslateTransform3D(suiv_x, suiv_y, suiv_z);
CHAPITRE 7 Construction du jeu du Casse-Briques 283

this.InvalidateModel();
}
...
}
En lançant le jeu, la balle se déplace vers le fond du plateau en fonction du temps et
cela indéfiniment pour l’instant.

Quand la balle arrivera au fond du plateau, elle tombera sur le mur du fond (x_affiche_
murale_fond de type TAfficheMurale). Une collision doit donc se produire et il faut
gérer cette collision pour déterminer les nouvelles valeurs de v_vitesse_taux_x et v_
vitesse_taux_z qui sont affectées à v_vitesse_x et v_vitesse_z.
Trois cas peuvent se présenter lors de cette collision. Dans le premier cas, la direction
de la balle est une direction de la gauche vers la droite, cela veut dire que la balle se
dirige avec une direction vers l’axe X positif et l’axe Z négatif. Le résultat de la collision
sera d’inverser le mouvement en redirigeant la balle vers l’axe Z positif. La variable
v_vitesse_taux_x sera inchangée et v_vitesse_taux_z sera inversée.

TAfficheMurale
x_affiche_murale_fond v_vitesse_taux_x = v_vitesse_taux_x
v_vitesse_taux_z = -v_vitesse_taux_z

TSol x_sol

Z
284 Programmez des jeux vidéo 3D avec C#5 et WPF
Dans le deuxième cas, la direction de la balle est une direction de la droite vers la
gauche, cela veut dire que la balle se dirige avec une direction vers l’axe X négatif et
l’axe Z négatif. Le résultat de la collision sera d’inverser le mouvement en redirigeant la
balle vers l’axe Z positif. La variable v_vitesse_taux_x sera inchangée et v_vitesse_taux_z
sera inversée.
TAfficheMurale
x_affiche_murale_fond v_vitesse_taux_x = v_vitesse_taux_x
v_vitesse_taux_z = -v_vitesse_taux_z

TSol x_sol

Z
Dans le troisième cas, la direction de la balle est une direction parallèle à l’axe Z, cela
veut dire que la balle se dirige avec une direction vers l’axe Z négatif. Le résultat de la
collision sera d’inverser le mouvement en redirigeant la balle vers l’axe Z positif. La
variable v_vitesse_taux_x sera inchangée et v_vitesse_taux_z sera inversée.
TAfficheMurale
x_affiche_murale_fond v_vitesse_taux_x = v_vitesse_taux_x
v_vitesse_taux_z = -v_vitesse_taux_z

TSol x_sol

On ajoute une méthode booléenne TSphere.CollisionDetecter recevant en paramètre


Copyright 2013 Patrice REY

un objet TAfficheMurale et retournant une valeur booléenne indiquant si une collision


s’est produite. Si la collision s’est bien produite, alors on applique les nouvelles valeurs
de v_vitesse_taux_x et v_vitesse_taux_z.
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
if (v_etat_jeu == EtatJeu.en_cours) {
CHAPITRE 7 Construction du jeu du Casse-Briques 285

x_raquette.AppliquerTranslation(v_deplac / 320d, 0, 0);


v_vitesse_x += v_vitesse_taux_x;
v_vitesse_z += v_vitesse_taux_z;
x_balle.AppliquerTranslation(v_vitesse_x, 0, v_vitesse_z);
if (x_balle.CollisionDetecter(x_affiche_murale_fond) == true) {
v_vitesse_taux_z = -v_vitesse_taux_z;
}
}
...
}
Voyons maintenant comment définir concrètement la collision entre un objet TSphere
et un objet TAfficheMurale. Si on considère la boite qui englobe la balle et la boite qui
englobe l’affiche murale, on pourra dire qu’il y a collision quand ces deux boites auront
une partie commune.
Quand on définit une modélisation 3D de type Model3D, la propriété Model3D.
Bounds retourne la boite englobante du modèle sous la forme d’une structure Rect3D
(structure appartenant à l’espace de noms System.Windows.Media.Media3D). Il s’agit
d’une boite englobante centrée sur le modèle. Dans le cas d’une sphère TSphere,
la boite englobante sera centrée sur le centre de la sphère et ses côtés auront pour
valeur le rayon de la sphère. Pour un cube de type TCube, la boite englobante sera de
dimensions égales à celle du cube.
La structure Rect3D expose principalement les propriétés et méthodes suivantes:
• Location, de type Point3D, représente le point de référence pour la construction de
la boite englobante,
• SizeX, SizeY et SizeZ représentent les longueurs des côtés de la boite englobante
selon les axes X, Y et Z,
• la méthode booléenne IntersectWith, reçoit en paramètre un Rect3D, et retourne
une valeur indiquant s’il y a une intersection commune entre les deux Rect3D,
• la méthode statique Intersect(Rect3D,Rect3D) retourne un Rect3D correspondant à
l’intersection,
• la propriété booléenne isEmpty retourne une valeur indiquant si l’intersection est
vide ou non,
• la méthode Offset(double,double,double) translate un Rect3D selon les axes X, Y et
Z.

Location de
type Point3D
SizeY
SizeZ
SizeX
286 Programmez des jeux vidéo 3D avec C#5 et WPF
De ce fait, pour la balle de type TSphere, on ajoute un champ v_boite_englobante,
de type Rect3D, et une propriété P_BoiteEnglobante pour fixer et obtenir ce Rect3D.
Dans la méthode de rendu du modèle, on affecte à v_boite_englobante la valeur de la
propriété P_Modele.Bounds.
//champs
private Rect3D v_boite_englobante;
//proprietes
public Rect3D P_BoiteEnglobante {
get { return v_boite_englobante; }
private set { v_boite_englobante = value; }
}
//redefinition de la methode de rendu
protected override void OnUpdateModel() {
//face exterieure sphere
GeometryModel3D gp_geo_sphere = new GeometryModel3D();
...
//
P_Modele = gp_geo_sphere;
v_boite_englobante = P_Modele.Bounds;
}
On procède de la même manière pour TAfficheMurale ainsi que tous les autres modèles
utilisés (TCube, TCone, TCylindre, TCubeDetailFace, etc.).
Quand on applique une translation à la sphère, il ne faut pas oublier de recalculer
les valeurs de la boite englobante. En effet, la boite englobante de type Rect3D est
centrée sur le modèle mais ne tient pas compte des transformations appliquées. D’où
un nouveau calcul à effectuer qui consiste à relever le Point3D de la boite englobante
par sa propriété Location, puis d’actualiser les déplacements de translation par la
méthode Point3D.Offset, enfin d’affecter ce nouveau Point3D à la propriété Location
de la boite englobante.
//appliquer une translation a la sphere
public void AppliquerTranslation(double suiv_x, double suiv_y, double suiv_z) {
this.Transform = new TranslateTransform3D(suiv_x, suiv_y, suiv_z);
this.InvalidateModel();
Point3D pt3d = v_boite_englobante.Location;
pt3d.Offset(suiv_x, suiv_y, suiv_z);
Copyright 2013 Patrice REY

v_boite_englobante.Location = pt3d;
}
Maintenant que nos objets possèdent une boite englobante, la détection de collision
consiste à voir si la méthode Rect3D.IntersectWith retourne un Rect3D vide ou non vide.
//determiner la possibilité d’une collision avec un TAfficheMurale
public bool CollisionDetecter(TAfficheMurale obj_affiche_murale) {
bool collision = false;
CHAPITRE 7 Construction du jeu du Casse-Briques 287

Rect3D rect3d_obj_affiche_murale = obj_affiche_murale.P_BoiteEnglobante;


if (v_boite_englobante.IntersectsWith(rect3d_obj_affiche_murale) == true) {
collision = true;
}
return collision;
}
Le principe de la détection de collision avec les murs x_mur_gauche et x_mur_droit, de
type TCubeDetailFace, reste identique sur le principe. La variable v_vitesse_taux_x est
inversée et la variable v_vitesse_taux_z est inchangée.
TAfficheMurale
TCubeDetailFace x_affiche_murale_fond
x_mur_gauche
v_vitesse_taux_x = -v_vitesse_taux_x
v_vitesse_taux_z = v_vitesse_taux_z

TSol x_sol

TAfficheMurale
TCubeDetailFace x_affiche_murale_fond
x_mur_gauche
v_vitesse_taux_x = -v_vitesse_taux_x
v_vitesse_taux_z = v_vitesse_taux_z

TSol x_sol

Z
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
if (v_etat_jeu == EtatJeu.en_cours) {
x_raquette.AppliquerTranslation(v_deplac / 320d, 0, 0);
v_vitesse_x += v_vitesse_taux_x;
v_vitesse_z += v_vitesse_taux_z;
x_balle.AppliquerTranslation(v_vitesse_x, 0, v_vitesse_z);
if (x_balle.CollisionDetecter(x_affiche_murale_fond) == true) {
288 Programmez des jeux vidéo 3D avec C#5 et WPF
v_vitesse_taux_z = -v_vitesse_taux_z;
}
if (x_balle.CollisionDetecter(x_mur_droit) == true) {
v_vitesse_taux_x = -v_vitesse_taux_x;
}
if (x_balle.CollisionDetecter(x_mur_gauche) == true) {
v_vitesse_taux_x = -v_vitesse_taux_x;
}
}
...
}
//determiner la possibilité d’une collision avec un TCubeDetailFace
public bool CollisionDetecter(TCubeDetailFace obj_cube_detail_face) {
bool collision = false;
Rect3D rect3d_cube_detail_face = obj_cube_detail_face.P_BoiteEnglobante;
if (v_boite_englobante.IntersectsWith(rect3d_cube_detail_face) == true) {
collision = true;
}
return collision;
}

3 - Les collisions

Dans un jeu, la détection des collisions est une notion essentielle. On a vu dans le
paragraphe précédent une détection des collisions entre la balle et les murs (x_mur_
gauche et x_mur_droit de type TCubeDetailFace, et x_affiche_murale_fond de type
TAfficheMurale). Ces détections de collisions étaient simples puisqu’elles consistaient
à réfléchir la direction de parcours de la balle.
Maintenant nous allons aborder la détection des collisions, dans un premier temps,
entre la balle et la raquette, et dans un deuxième temps, entre la balle et les briques.
Ces détections de collisions seront basées sur un principe plus évolué pour donner un
intérêt certain au jeu.

3.1 - Collision entre la balle et la raquette


Copyright 2013 Patrice REY

Lors de la collision entre la balle et la raquette, on pourrait se contenter de réfléchir


la direction de parcours de la balle. D’un point de vue de la logique du jeu, cela ne
serait pas très intéressant. On va plutôt préférer le choix qui consiste à donner à la
balle, lors d’une collision raquette balle, une direction de parcours qui est fonction
de la position d’impact de la balle sur la raquette. La figure 7.18 visualise différentes
étapes de collisions entre la balle et la raquette. Quand la balle entre en collision au
CHAPITRE 7 Construction du jeu du Casse-Briques 289

milieu de la raquette, alors sa direction de parcours est parallèle à l’axe Z et orientée


vers l’axe Z négatif. Quand la balle entre en collision au coin de la raquette, sa direction
de parcours est définie par un angle spécifique. La flèche noire indique la direction
donnée à la balle.
FIGURE 7.18

2
290 Programmez des jeux vidéo 3D avec C#5 et WPF
Entre ces deux cas spécifiques, la balle aura une orientation de direction qui sera
proportionnelle à l’emplacement d’impact de la balle sur la raquette. Pour l’instant,
on a défini une variable v_vitesse_taux égale à 0.05d et qui correspond à une vitesse
moyenne correcte de déplacement de la balle sur le plateau. De plus, en démarrant le
jeu, on a initialisé l’orientation de cette direction par v_vitesse_taux_x = 0 et v_vitesse_
taux_z = -v_vitesse_taux, ce qui est équivalent à une orientation parallèle à l’axe Z et
orientée vers l’axe Z négatif. D’où, au début, la balle se dirige vers le fond du plateau.
Sur le schèma ci-dessous, on projette en 2D le plan OXZ qui est le plan de déplacement
de la balle. La flèche noire représente l’orientation à donner à la balle, et sa longueur
est égale à 0.05 (valeur de v_vitesse_taux). L’angle entre la flèche et l’axe X est de 90°.
On peut donc en déduire les projections dx et dz qui représentent respectivement les
projections suivant l’axe X et suivant l’axe Z. On a donc les relations suivantes (avec
d=0.05 correspondant à la longueur de la flèche noire):
cos(90°) = dx / d soit dx = d * cos(90°) = 0
et
sin(90°) = -dz / d soit dz = -d * sin(90°) = -0.05

0.06

P
0.05
cos(90°) = dx / d
0.04 soit
dx = d * cos(90°)
et
0.03 sin(90°) = -dz / d
dz = -0.05 soit
d = 0.05
dz = -d * sin(90°)
0.02

angle = 90°
0.01
Copyright 2013 Patrice REY

O
X
dx = 0 0.01 0.02 0.03 0.04 0.05 0.06

Z
Si l’on souhaite donner une orientation différente à la balle par modification de l’angle,
il faut que le calcul de cette orientation donne l’impression que la balle possède la
CHAPITRE 7 Construction du jeu du Casse-Briques 291

même vitesse. Intuitivement, on en déduit que pour donner une orientation spécifique
suivant un angle donné, il faudra calculer les projections dx et dz de la flèche noire de
longueur d=0.05 c’est-à-dire le long de l’arc de cercle de rayon d centré sur O.
Par exemple, pour donner une orientation de 45° (schéma ci-dessous), on obtiendra
les valeurs de projection dx = 0.035 et dz = -0.035 :
cos(45°) = dx / d soit dx = d * cos(45°) = 0.035
et
sin(45°) = -dz / d soit dz = -d * sin(45°) = -0.035

0.06

0.05
cos(45°) = dx / d
soit
0.04 dx = d * cos(45°)
P
et
sin(45°) = -dz / d
0.03 soit
dz = -d * sin(45°)

dz = -0.035 0.02

0.01
angle = 45°

X
O 0.01 0.02 0.03 0.04 0.05 0.06
dx = 0.035
Z

x = 0.035
z = -0.035
angle de 45°
292 Programmez des jeux vidéo 3D avec C#5 et WPF
Autre exemple, pour donner une orientation de 135°, on obtiendra les valeurs de
projection dx = -0.035 et dz = -0.035 :
cos(135°) = dx / d soit dx = d * cos(135°) = -0.035
et
sin(135°) = -dz / d soit dz = -d * sin(135°) = -0.035

0.06

0.05

0.04
P

0.03
dz = -0.035
0.02
angle
= 135°
0.01

X
O 0.01 0.02 0.03 0.04 0.05
dx = -0.035
Z

x = -0.035
z = -0.035
angle de 135°
Copyright 2013 Patrice REY

Comme nous venons de voir comment calculer les projections dx et dz en fonction


de l’angle, on va faire en sorte que l’angle de rebond de la balle côté droit au bord de
la raquette soit de 20° et côté gauche au bord de la raquette soit de 160°, soit une
répartition de 70° de part et d’autre de l’axe Z. Entre les deux côtés, l’angle sera calculé
CHAPITRE 7 Construction du jeu du Casse-Briques 293

proportionnellement à la position d’impact.

angle de 20°

angle de 160°

Dans la boucle du rendu, on ajoute un test pour voir s’il y a une collision entre les
boites englobantes de la raquette et de la balle, en utilisant la méthode TSphere.
CollisionDetecter qui reçoit en paramètre un TCube (type de la raquette).
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
if (v_etat_jeu == EtatJeu.en_cours) {
x_raquette.AppliquerTranslation(v_deplac / 320d, 0, 0);
v_vitesse_x += v_vitesse_taux_x;
v_vitesse_z += v_vitesse_taux_z;
x_balle.AppliquerTranslation(v_vitesse_x, 0, v_vitesse_z);
if (x_balle.CollisionDetecter(x_affiche_murale_fond) == true) {
v_vitesse_taux_z = -v_vitesse_taux_z;
}
if (x_balle.CollisionDetecter(x_mur_droit) == true) {
v_vitesse_taux_x = -v_vitesse_taux_x;
}
if (x_balle.CollisionDetecter(x_mur_gauche) == true) {
v_vitesse_taux_x = -v_vitesse_taux_x;
}
if (x_balle.CollisionDetecter(x_raquette) == true) {
...
}
}
...
}
Le détail de la méthode CollisionDetecter reste identique à ce que l’on a déjà vu
concernant l’intersection entre deux boites englobantes.
294 Programmez des jeux vidéo 3D avec C#5 et WPF
public class TSphere : UIElement3D {
//determiner la possibilité d’une collision avec un TCube
public bool CollisionDetecter(TCube obj_cube) {
bool collision = false;
Rect3D rect3d_obj_cube = obj_cube.P_BoiteEnglobante;
if (v_boite_englobante.IntersectsWith(rect3d_obj_cube) == true) {
collision = true;
}
return collision;
}
}
Si une collision s’est bien produite, il faut donc, dans un premier temps, apprécier la
position de la balle par rapport à la raquette selon l’axe X, et dans un second temps,
calculer l’angle à appliquer pour déterminer la direction de la balle. Pour cela on va
introduire la notion de point de gravité. Un point de gravité sera un point calculé qui
correspondra au centre du modèle. Le point de gravité ne sera rien d’autre que le
centre de la boite englobante.
On ajoute donc pour la balle x_balle (de type TSphere) et pour la raquette x_raquette (de
type TCube) un champ v_point_gravite de type Point3D et une propriété P_PointGravite
comme accesseurs.
public class TSphere : UIElement3D {
//champs
private Point3D v_point_gravite;
//proprietes
public Point3D P_PointGravite {
get { return v_point_gravite; }
private set { v_point_gravite = value; }
}
//redefinition de la methode de rendu
protected override void OnUpdateModel() {
//
P_Modele = gp_geo_sphere;
v_boite_englobante = P_Modele.Bounds;
v_point_gravite = CalculPointGraviteParBoite(v_boite_englobante);
}
//calculer le point de gravité a partir de la boite englobante
private Point3D CalculPointGraviteParBoite(Rect3D v_boite_englobante) {
Copyright 2013 Patrice REY

Point3D pt_gravite = new Point3D();


pt_gravite.X = v_boite_englobante.Location.X + (v_boite_englobante.SizeX / 2d);
pt_gravite.Y = v_boite_englobante.Location.Y + (v_boite_englobante.SizeY / 2d);
pt_gravite.Z = v_boite_englobante.Location.Z + (v_boite_englobante.SizeZ / 2d);
return pt_gravite;
}
...
}
CHAPITRE 7 Construction du jeu du Casse-Briques 295

Cette variable v_point_gravite est calculée à partir de la variable v_boite_englobante


par la méthode privée CalculPointGraviteParBoite. Et chaque fois que l’on applique
un déplacement à la balle et à la raquette par la méthode AppliquerTranslation, on
recalcule le point de gravité.
public class TSphere : UIElement3D {
...
//appliquer une translation a la sphere
public void AppliquerTranslation(double suiv_x, double suiv_y, double suiv_z) {
this.Transform = new TranslateTransform3D(suiv_x, suiv_y, suiv_z);
this.InvalidateModel();
Point3D pt3d = v_boite_englobante.Location;
pt3d.Offset(suiv_x, suiv_y, suiv_z);
v_boite_englobante.Location = pt3d;
v_point_gravite = CalculPointGraviteParBoite(v_boite_englobante);
}
...
}

boite englobante avec son point de référence


S (pour la balle) et R (pour la raquette)

S R

point de gravité Gb (pour la balle) et Gr (pour


la raquette)

R Gr
S
Gb

L’appréciation de la position de la balle par rapport à la raquette se fait en comparant la


position TSphere.P_PointGravite.X à la position TCube.P_PointGravite.X (c’est-à-dire en
comparant la coordonnée des deux points de gravité selon l’axe X). Si la coordonnée
296 Programmez des jeux vidéo 3D avec C#5 et WPF
selon X de la balle est supérieure à la coordonnée selon X de la raquette, alors la balle
se trouve sur le côté droit de la raquette, et inversement.
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
if (v_etat_jeu == EtatJeu.en_cours) {
x_raquette.AppliquerTranslation(v_deplac / 320d, 0, 0);
v_vitesse_x += v_vitesse_taux_x;
v_vitesse_z += v_vitesse_taux_z;
x_balle.AppliquerTranslation(v_vitesse_x, 0, v_vitesse_z);
...
if (x_balle.CollisionDetecter(x_raquette) == true) {
Point3D pt_gravite_raq = x_raquette.P_PointGravite;
Point3D pt_gravite_balle = x_balle.P_PointGravite;
double dist_max = (x_raquette.P_BoiteEnglobante.SizeX / 2d) +
(x_balle.P_BoiteEnglobante.SizeX / 2d);
double dist_x_gravite = Math.Abs(x_balle.P_PointGravite.X
- x_raquette.P_PointGravite.X);
double angle_degree = 90;
if (pt_gravite_balle.X >= pt_gravite_raq.X) {
double pourcentage = dist_x_gravite / dist_max;
//amplitude de 90 degres a 20 degres soit 70 degres
angle_degree = 90 - ((90 - 20) * pourcentage);
}
if (pt_gravite_balle.X < pt_gravite_raq.X) {
double pourcentage = dist_x_gravite / dist_max;
//amplitude de 90 degres a 160 degres soit 70 degres
angle_degree = 90 + ((160 - 90) * pourcentage);
}
double angle_radian = Math.PI * angle_degree / 180d;
v_vitesse_taux_x = v_vitesse_taux * Math.Cos(angle_radian);
v_vitesse_taux_z = -v_vitesse_taux * Math.Sin(angle_radian);
}
}
...
}
Il suffit alors de calculer le pourcentage correspondant à la position du point de gravité
par rapport à la distance qui existe entre le milieu de la raquette et le bord de la raquette.
En fonction de ce pourcentage, on détermine un angle angle_degree compris entre 90°
Copyright 2013 Patrice REY

et 20° si la balle est du côté droit de la raquette, et un angle compris entre 90° et 160°
si la balle est du côté gauche de la raquette. A partir de angle_degree, on effectue la
conversion en radians pour obtenir l’angle en radians angle_radian. Et on calcule les
nouvelles directions selon X et selon Z à appliquer à la balle à savoir v_vitesse_taux_x
= v_vitesse_taux * Math.Cos(angle_radian) et v_vitesse_taux_z = -v_vitesse_taux * Math.
Sin(angle_radian). La figure 7.19 visualise les deux cas extrêmes et la cas du milieu.
CHAPITRE 7 Construction du jeu du Casse-Briques 297
FIGURE 7.19

coordonnée x de Gb x=0
z = -0.05
angle de 90°

coordonnée x de Gr
coordonnée x de Gb
x = -0.017
z = -0.046
angle de 160°

coordonnée x de Gr
coordonnée x de Gb
x = 0.017
z = -0.046
angle de 20°

coordonnée x de Gr

3.2 - Ajouter les briques

Le moment est venu d’ajouter des briques à détruire pour notre Casse-briques. Nous
allons modéliser la brique par un cube sur lequel on va appliquer une texture affichant
une valeur de points à gagner en cas de destruction. Le repère 1 de la figure 7.20
montre le plateau rempli avec 16 briques dans le concepteur XAML. Le repère 2 de la
figure 7.20 montre de plus près la texture appliquée à la brique, affichant une valeur
de points à gagner.
298 Programmez des jeux vidéo 3D avec C#5 et WPF
FIGURE 7.20

On déclare une classe TBriqueCube, qui hérite de UIElement3D, et qui expose les
propriétés suivantes:
Copyright 2013 Patrice REY

• P_Modele, de type Model3D, représente le modèle TBriqueCube avec ses


fonctionnalités,
• P_PointReference, de type Point3D, représente le point à partir duquel le cube est
modélisé (valeur par défaut aux coordonnées 0,0,0).,
• P_LargeurX, de type double, représente la largeur du cube suivant l’axe X (valeur
par défaut 1d),
CHAPITRE 7 Construction du jeu du Casse-Briques 299

• P_HauteurY, de type double, représente la hauteur du cube suivant l’axe Y (valeur


par défaut 1d),
• P_ProfondeurZ, de type double, représente la profondeur du cube suivant l’axe Z
(valeur par défaut 1d),
• P_Texture, de type Material, représente la texture à appliquer sur le cube dans son
intégralité (valeur par défaut null).
• P_Gains, de type TBriqueCube.ApportGains, qui représente le nombre de points
que rapportera la brique (valeur par défaut ApportGains.non_defini).
L’énumération BriqueCube.ApportGains représente la valeur de points qui sera attribuée
à la brique (ApportGains.non_defini, ApportGains. val_050, ApportGains. val_100,
ApportGains. val_150 et ApportGains. val_200), allant de 50 points à 200 points par pas
de 50 points (valeur par défaut ApportGains.non_defini).
Une collection générique sous forme d’une liste, de type List<ImageBrush>, nommée
v_liste_pinceau, stocke les fichiers graphiques des textures à appliquer sur la brique.
Dans le constructeur de la brique, on stocke les pinceaux de type ImageBrush dont
la propriété ImageSource pointe vers les fichiers graphiques des textures qui sont
stockées dans le dossier contenu/brique/.
public class TBriqueCube : UIElement3D {
//constructeur
public TBriqueCube() {
v_liste_pinceau = new List<ImageBrush>();
for (int xx = 0; xx < 4; xx++) {
ImageBrush img_brush = new ImageBrush();
switch (xx) {
case 0:
img_brush.ImageSource = new BitmapImage(
new Uri(«pack://application:,,,/CasseBrique3D;component/contenu/brique/
texture_brique_50_pt.png», UriKind.Absolute));
break;
case 1:
img_brush.ImageSource = new BitmapImage(
new Uri(«pack://application:,,,/CasseBrique3D;component/contenu/brique/
texture_brique_100_pt.png», UriKind.Absolute));
break;
case 2:
img_brush.ImageSource = new BitmapImage(
new Uri(«pack://application:,,,/CasseBrique3D;component/contenu/brique/
texture_brique_150_pt.png», UriKind.Absolute));
break;
case 3:
img_brush.ImageSource = new BitmapImage(
new Uri(«pack://application:,,,/CasseBrique3D;component/contenu/brique/
texture_brique_200_pt.png», UriKind.Absolute));
300 Programmez des jeux vidéo 3D avec C#5 et WPF
break;
}
v_liste_pinceau.Add(img_brush);
}
}
Si la propriété P_Gains est non défini (valeur par défaut ApportGains.non_defini),
alors la brique est peinte en rouge. Dans les autres cas, la brique est texturée par
l’intermédiaire de la propriété P_Texture.
//redefinition de la methode de rendu
protected override void OnUpdateModel() {
GeometryModel3D geo = new GeometryModel3D();
geo.Geometry = Maillage(this.P_PointReference, this.P_LargeurX, this.P_HauteurY,
this.P_ProfondeurZ);
if (this.P_Gains == ApportGains.non_defini) {
geo.Material = new DiffuseMaterial(new SolidColorBrush(Colors.Red));
}
else {
geo.Material = this.P_Texture;
}
geo.BackMaterial = new DiffuseMaterial(new SolidColorBrush(Colors.Yellow));
this.P_Modele = geo;
this.v_boite_englobante = this.P_Modele.Bounds;
this.v_point_gravite = CalculPointGraviteParBoite(v_boite_englobante);
}
La modification de la propriété de dépendance P_Gains invoque la méthode statique
PM_Gains. Cette méthode regarde l’énumération stockée dans la propriété P_Gains, et
par une instruction switch, elle affecte à P_Texture un objet Material dont la propriété
Brush reçoit un ImageBrush stocké dans v_liste_pinceau.
//la propriete Gains
private static readonly DependencyProperty ProprieteGains =
DependencyProperty.Register(«P_Gains»,
typeof(ApportGains),
typeof(TBriqueCube),
new PropertyMetadata(ApportGains.non_defini, PM_Gains));
private static void PM_Gains(DependencyObject d, DependencyPropertyChangedEventArgs
e) {
Copyright 2013 Patrice REY

TBriqueCube brique = (TBriqueCube)d;


switch (brique.P_Gains) {
case ApportGains.val_050:
brique.P_Texture = new DiffuseMaterial(brique.v_liste_pinceau[0]);
break;
case ApportGains.val_100:
brique.P_Texture = new DiffuseMaterial(brique.v_liste_pinceau[1]);
break;
CHAPITRE 7 Construction du jeu du Casse-Briques 301

case ApportGains.val_150:
brique.P_Texture = new DiffuseMaterial(brique.v_liste_pinceau[2]);
break;
case ApportGains.val_200:
brique.P_Texture = new DiffuseMaterial(brique.v_liste_pinceau[3]);
break;
}
brique.InvalidateModel();
}
public ApportGains P_Gains {
get { return (ApportGains)GetValue(ProprieteGains); }
set { SetValue(ProprieteGains, value); }
}
Pour ajouter une brique en XAML avec un nombre de points voulu, il suffira
d’instancier une brique par <modele3d:TBriqueCube>, de lui donner des dimensions
voulues (P_LargeurX, P_Hauteur_Y et P_Profondeur_Z), de la positionner sur le plateau
par la propriété P_PointReference (<modele3d:TBriqueCube.P_PointReference>), et de
personnaliser son nombre de points par la propriété P_Gains. Par exemple pour une
brique rapportant 50 points, on écrira:
<modele3d:TBriqueCube x:Name=ˈx_brique_1_2ˈ P_LargeurX=ˈ0.20ˈ P_HauteurY=ˈ0.20ˈ
P_ProfondeurZ=ˈ0.20ˈ P_Gains=ˈval_050ˈ>
<modele3d:TBriqueCube.P_PointReference>
<Point3D X=ˈ-0.45ˈ Y=ˈ0ˈ Z=ˈ2ˈ></Point3D>
</modele3d:TBriqueCube.P_PointReference>
</modele3d:TBriqueCube>
Et par exemple pour une brique rapportant 200 points, on écrira:
<modele3d:TBriqueCube x:Name=ˈx_brique_1_2ˈ P_LargeurX=ˈ0.20ˈ P_HauteurY=ˈ0.20ˈ
P_ProfondeurZ=ˈ0.20ˈ P_Gains=ˈval_200ˈ>
<modele3d:TBriqueCube.P_PointReference>..</modele3d:TBriqueCube.P_PointReference>
</modele3d:TBriqueCube>
P_Gains = ApportGains.val_150 P_Gains = ApportGains.val_200

P_Gains = ApportGains.val_050 P_Gains = ApportGains.val_100


302 Programmez des jeux vidéo 3D avec C#5 et WPF
Sur le plateau, on positionne quatre lignes de quatre briques, et on les nomme x_
brique_1_1 à x_brique_4_4. Dans MainWindow.xaml.cs, on initialise une liste générique
v_liste_brique de type List<modele3d.TBriqueCube>, et on remplit la liste par la
méthode RemplirListeAvecLesBriques.
private void RemplirListeAvecLesBriques() {
v_liste_brique.Clear();
v_liste_brique.Add(x_brique_1_1);
v_liste_brique.Add(x_brique_1_2);
v_liste_brique.Add(x_brique_1_3);
v_liste_brique.Add(x_brique_1_4);
v_liste_brique.Add(x_brique_2_1);
v_liste_brique.Add(x_brique_2_2);
v_liste_brique.Add(x_brique_2_3);
v_liste_brique.Add(x_brique_2_4);
v_liste_brique.Add(x_brique_3_1);
v_liste_brique.Add(x_brique_3_2);
v_liste_brique.Add(x_brique_3_3);
v_liste_brique.Add(x_brique_3_4);
v_liste_brique.Add(x_brique_4_1);
v_liste_brique.Add(x_brique_4_2);
v_liste_brique.Add(x_brique_4_3);
v_liste_brique.Add(x_brique_4_4);
for (int xx = 0; xx < v_liste_brique.Count; xx++) {
modele3d.TBriqueCube brique = v_liste_brique[xx];
brique.Visibility = Visibility.Visible;
}
}

3.3 - Collision entre la balle et la brique

La collision entre une balle et une brique se fait en trois temps: en premier on détecte
avec quelle brique la balle est entrée en collision; en second on gère la direction à
donner à la balle en fonction de la face de la brique qui a été atteinte; en dernier on
retire la brique du plateau.
Pour détecter la possibilité d’une collision entre la balle et une brique, on parcourt la
liste v_liste_brique et on cherche si la méthode booléenne TSphere.CollisionDetecter
Copyright 2013 Patrice REY

retourne la valeur true pour une brique donnée. On ajoute donc une méthode
CollisionDetecter qui reçoit un objet TBriqueCube et qui retourne une valeur booléenne.
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
if (v_etat_jeu == EtatJeu.en_cours) {
x_raquette.AppliquerTranslation(v_deplac / 320d, 0, 0);
v_vitesse_x += v_vitesse_taux_x;
CHAPITRE 7 Construction du jeu du Casse-Briques 303

v_vitesse_z += v_vitesse_taux_z;
x_balle.AppliquerTranslation(v_vitesse_x, 0, v_vitesse_z);
...
//collision avec des briques
int index_collision = -1;
for (int xx = 0; xx < v_liste_brique.Count; xx++) {
modele3d.TBriqueCube brique = v_liste_brique[xx];
if (x_balle.CollisionDetecter(brique) == true) {
...
}
}
}
...
}
//determiner la possibilité d’une collision avec un TBriqueCube
public bool CollisionDetecter(TBriqueCube obj_brique) {
bool collision = false;
Rect3D rect3d_obj_brique = obj_brique.P_BoiteEnglobante;
if (v_boite_englobante.IntersectsWith(rect3d_obj_brique) == true) {
collision = true;
}
return collision;
}
Une brique est modélisée par un cube et comporte donc six faces. Les faces à prendre
en compte (schéma ci-dessous) sont la face avant (S2,S1,S5,S6), la face de droite
(S6,S5,S4,S7), la face de gauche (S3,S0,S1,S2) et la face arrière (S3,S0,S4,S7).
S3 S7
une brique et ses faces S2
S6

S4
S0

S1 S5
On ajoute une méthode TSphere.CollisionDetecterFaceBrique, qui reçoit en paramètre
une brique TBriqueCube, et qui retourne un tableau faces de 4 entiers de type int. Ce
tableau est initialisé par des valeurs égales à 0. Si la face de gauche est touchée, alors
la valeur 1 est affectée à l’indice 0. Si la face avant est touchée, alors la valeur 1 est
affectée à l’indice 1. Si la face de droite est touchée, alors la valeur 1 est affectée à
l’indice 2. Et si la face arrière est touchée, alors la valeur 1 est affectée à l’indice 3. De
ce fait on sait exactement les faces touchées (valeur 1) des faces non touchées.
304 Programmez des jeux vidéo 3D avec C#5 et WPF
//determiner les faces touchées par collision d’une brique
public int[] CollisionDetecterFaceBrique(TBriqueCube obj_brique) {
int[] faces = new int[4];
Rect3D rect3d_obj_brique = obj_brique.P_BoiteEnglobante;
//face gauche
Point3D s0 = new Point3D(rect3d_obj_brique.Location.X,
rect3d_obj_brique.Location.Y, rect3d_obj_brique.Location.Z);
Point3D s1 = new Point3D(rect3d_obj_brique.Location.X,
rect3d_obj_brique.Location.Y, rect3d_obj_brique.Location.Z +
rect3d_obj_brique.SizeZ);
if (v_boite_englobante.Contains(s0) == true || v_boite_englobante.Contains(s1) ==
true) {
faces[0] = 1;
}
else {
faces[0] = 0;
}
//face avant
Point3D s5 = new Point3D(rect3d_obj_brique.Location.X + rect3d_obj_brique.SizeX,
rect3d_obj_brique.Location.Y, rect3d_obj_brique.Location.Z +
rect3d_obj_brique.SizeZ);
if (v_boite_englobante.Contains(s1) == true || v_boite_englobante.Contains(s5) ==
true) {
faces[1] = 1;
}
else {
faces[1] = 0;
}
//face droite
Point3D s4 = new Point3D(rect3d_obj_brique.Location.X + rect3d_obj_brique.SizeX,
rect3d_obj_brique.Location.Y, rect3d_obj_brique.Location.Z);
if (v_boite_englobante.Contains(s5) == true || v_boite_englobante.Contains(s4) ==
true) {
faces[2] = 1;
}
else {
faces[2] = 0;
}
//face arriere
if (v_boite_englobante.Contains(s4) == true || v_boite_englobante.Contains(s0) ==
Copyright 2013 Patrice REY

true) {
faces[3] = 1;
}
else {
faces[3] = 0;
}
return faces;
}
On donne la priorité aux faces avant et arrière par rapport aux faces de droite et
CHAPITRE 7 Construction du jeu du Casse-Briques 305

de gauche. Si la face avant ou la face arrière d’une brique est touchée, on procède
au changement de direction de la balle, puis on quitte l’instruction de recherche de
collision par une instruction break. Si les faces avant et arrière ne sont pas touchées,
on vérifie si les faces de droite et de gauche sont touchées. Si c’est le cas, on procède
au changement de direction et on quitte l’instruction de recherche de collision par
une instruction break. A noter qu’une brique touchée voit sa visibilité qui passe à l’état
Collapsed. Par la variable index_collision, on stocke l’index de la brique touchée dans
la liste. Dans le cas d’une brique touchée, on la retire de la liste par la méthode List.
RemoveAt une fois la boucle for terminée.
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
if (v_etat_jeu == EtatJeu.en_cours) {
x_raquette.AppliquerTranslation(v_deplac / 320d, 0, 0);
v_vitesse_x += v_vitesse_taux_x;
v_vitesse_z += v_vitesse_taux_z;
x_balle.AppliquerTranslation(v_vitesse_x, 0, v_vitesse_z);
...
//collision avec des briques
int index_collision = -1;
for (int xx = 0; xx < v_liste_brique.Count; xx++) {
modele3d.TBriqueCube brique = v_liste_brique[xx];
if (x_balle.CollisionDetecter(brique) == true) {
int[] faces = x_balle.CollisionDetecterFaceBrique(brique);
//face avant seule ou face arriere seule
if (faces[1] == 1 || faces[3] == 1) {
v_vitesse_taux_z = -v_vitesse_taux_z;
brique.Visibility = Visibility.Collapsed;
index_collision = xx;
break;
}
//face gauche seule ou face droite seule
if (faces[0] == 1 || faces[2] == 1) {
v_vitesse_taux_x = -v_vitesse_taux_x;
brique.Visibility = Visibility.Collapsed;
index_collision = xx;
break;
}
}
}
if (index_collision != -1) {
v_liste_brique.RemoveAt(index_collision);
}
}
...
}
306 Programmez des jeux vidéo 3D avec C#5 et WPF
Le changement de direction de la balle se fait de façon simple (comme celle utilisée
pour la collision de la balle avec les murs):
• si la face avant ou arrière est touchée, on inverse v_vitesse_taux_z,
• si la face de droite ou de gauche est touchée, on inverse v_vitesse_taux_x.
S3 S7 S3 S7
S2 S2
S6 S6
S4 S4
S0 S0

S1 S5 S5
S1
face de droite ou
face de gauche
face avant ou
face arrière

4 - Affichage du temps et du score

Passons maintenant à l’étape de l’affichage du temps et du score obtenu. Comme le


montre la figure 7.21, le score est affiché à gauche et le temps est affiché à droite.
L’aspect de cet affichage ressemble à une incrustation dont les chiffres sont écrits
comme sur une montre électronique. Pour réaliser cela, on utilise une technique
particulière propre à WPF, qui est très puissante. On positionne l’un à côté de l’autre
deux modèles 3D de type TAfficheMurale, nommé x_affiche_score et x_affiche_temps.
On a vu qu’un objet TAfficheMurale n’est rien d’autre qu’une face rectangulaire
sur laquelle on applique une texture. Une texture de type Material (comme un
DiffuseMaterial, un EmissiveMaterial) a une propriété Brush qui reçoit le pinceau utilisé
Copyright 2013 Patrice REY

pour peindre. Jusqu’à présent on a utilisé un pinceau graphique de type ImageBrush


(c’est-à-dire peindre avec une image comme ressource graphique). On aurait aussi
pu utiliser un pinceau tel qu’un SolidColorBrush (pinceau avec une couleur unie), tel
qu’un LinearGradientBrush (pinceau avec une couleur dégradée), ou bien tel qu’un
VisualBrush. Un objet VisualBrush est un pinceau qui sert à peindre une région avec
un contenu. La classe Brush (figure 7.22) possède comme classes dérivées les classes
SolidColorBrush, LinearGradientBrush, ImageBrush, DrawingBrush et VisualBrush.
CHAPITRE 7 Construction du jeu du Casse-Briques 307

FIGURE 7.21

TAfficheMurale x_affiche_score TAfficheMurale x_affiche_temps

FIGURE 7.22
308 Programmez des jeux vidéo 3D avec C#5 et WPF
La propriété Visual du VisualBrush reçoit un contenu sous forme d’un ensemble de
contrôles. Pour afficher le score, on affecte à la propriété Visual un TextBlock x_affiche_
score_texte dont sa propriété Foreground est à White et sa propriété FontFamily est à
Quartz MS (police d’écriture avec une apparence dite d’écriture digitale).
<!-- affiche murale pour inscrire le score (à gauche) -->
<modele3d:TAfficheMurale x:Name=ˈx_affiche_scoreˈ P_Largeur=ˈ1.5ˈ P_Hauteur=ˈ0.5ˈ>
<modele3d:TAfficheMurale.P_PointReference>
<Point3D X=ˈ-1.7ˈ Y=ˈ1.35ˈ Z=ˈ-3ˈ></Point3D>
</modele3d:TAfficheMurale.P_PointReference>
<modele3d:TAfficheMurale.P_Texture>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush>
<VisualBrush.Visual>
<TextBlock x:Name=ˈx_affiche_score_texteˈ Text=ˈ00000ˈ Foreground=ˈWhiteˈ
FontFamily=ˈQuartz MSˈ Height=ˈ14ˈ Padding=ˈ0ˈ Canvas.Left=ˈ0ˈ
Canvas.Top=ˈ0ˈ/>
</VisualBrush.Visual>
</VisualBrush>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</modele3d:TAfficheMurale.P_Texture>
</modele3d:TAfficheMurale>
On procède de même pour afficher le temps avec un TextBlock x_affiche_temps_texte
affecté à la propriété Visual d’un VisualBrush.
<!-- affiche murale pour inscrire le temps (à droite) -->
<modele3d:TAfficheMurale x:Name=ˈx_affiche_tempsˈ P_Largeur=ˈ1.5ˈ P_Hauteur=ˈ0.5ˈ>
<modele3d:TAfficheMurale.P_PointReference>
<Point3D X=ˈ0.2ˈ Y=ˈ1.35ˈ Z=ˈ-3ˈ></Point3D>
</modele3d:TAfficheMurale.P_PointReference>
<modele3d:TAfficheMurale.P_Texture>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush>
<VisualBrush.Visual>
<TextBlock x:Name=ˈx_affiche_temps_texteˈ Text=ˈ00:00ˈ Foreground=ˈWhiteˈ
FontFamily=ˈQuartz MSˈ Height=ˈ14ˈ Padding=ˈ0ˈ Canvas.Left=ˈ0ˈ
Copyright 2013 Patrice REY

Canvas.Top=ˈ0ˈ/>
</VisualBrush.Visual>
</VisualBrush>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</modele3d:TAfficheMurale.P_Texture>
</modele3d:TAfficheMurale>
L’écriture du temps écoulé se fait alors très simplement: il suffit de remplacer le
contenu de la propriété Text de x_affiche_temps_texte par une chaîne représentant
CHAPITRE 7 Construction du jeu du Casse-Briques 309

le temps écoulé. On a vu que le temps écoulé s’obtient par la propriété Elapsed du


Stopwatch v_stopwatch.
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
x_affiche_temps_texte.Text = v_stopwatch.Elapsed.Minutes.ToString(«00») + «:» +
v_stopwatch.Elapsed.Seconds.ToString(«00»);
...
}
On procède de même pour l’écriture du score: dès qu’une brique est détruite, la
variable v_score est incrémentée du nombre de points attribué à la brique. On se sert
d’une méthode additionnelle CalculeGainsBriqueDetruite pour déterminer le nombre
de points.
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
if (v_etat_jeu == EtatJeu.en_cours) {
...
//collision avec des briques
int index_collision = -1;
for (int xx = 0; xx < v_liste_brique.Count; xx++) {
modele3d.TBriqueCube brique = v_liste_brique[xx];
if (x_balle.CollisionDetecter(brique) == true) {
int[] faces = x_balle.CollisionDetecterFaceBrique(brique);
//face avant seule ou face arriere seule
if (faces[1] == 1 || faces[3] == 1) {
v_vitesse_taux_z = -v_vitesse_taux_z;
brique.Visibility = Visibility.Collapsed;
index_collision = xx;
v_score += CalculGainsBriqueDetruite(brique);
break;
}
//face gauche seule ou face droite seule
if (faces[0] == 1 || faces[2] == 1) {
v_vitesse_taux_x = -v_vitesse_taux_x;
brique.Visibility = Visibility.Collapsed;
index_collision = xx;
v_score += CalculGainsBriqueDetruite(brique);
break;
}
}
}
...
}
...
}
310 Programmez des jeux vidéo 3D avec C#5 et WPF
La méthode additionnelle CalculeGainsBriqueDetruite consiste à lire la propriété P_
Gains de TBriqueCube, et de retourner le score correspondant.
//determiner valeur de gain pour une brique detruite
private int CalculGainsBriqueDetruite(modele3d.TBriqueCube brique) {
int score = 0;
switch (brique.P_Gains) {
case modele3d.TBriqueCube.ApportGains.val_050:
score += 50;
break;
case modele3d.TBriqueCube.ApportGains.val_100:
score += 100;
break;
case modele3d.TBriqueCube.ApportGains.val_150:
score += 150;
break;
case modele3d.TBriqueCube.ApportGains.val_200:
score += 200;
break;
}
return score;
}

5 - La fin de jeu

Le jeu peut se terminer de plusieurs façons. Si toutes les briques sont détruites alors le
jeu prend fin. Si la raquette échappe la balle et que cette dernière sort du terrain par
le bas, alors le jeu prend fin.
La vérification à effectuer, pour savoir si toutes les briques sont détruites, consiste à
vérifier si la liste v_liste_brique est vide ou pas (par sa propriété Count). Si la liste est
effectivement vide, alors on passe la variable v_etat_jeu à EtatJeu.terminer et on stoppe
l’horloge v_stopwatch.
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
if (v_etat_jeu == EtatJeu.en_cours) {
Copyright 2013 Patrice REY

...
if (v_liste_brique.Count == 0) {
v_etat_jeu = EtatJeu.terminer;
v_stopwatch.Stop();
}
...
}
...
}
CHAPITRE 7 Construction du jeu du Casse-Briques 311

Si la balle est sortie du jeu par le bas cela veut dire que la coordonnée selon Z de sa
boite englobante, à un moment donné, est supérieure à une valeur définie comme
étant hors zone (ici une valeur de 3.7 unités du côté de l’axe Z positif). On passe la
variable v_etat_jeu à EtatJeu.terminer et on stoppe l’horloge v_stopwatch.
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
if (v_etat_jeu == EtatJeu.en_cours) {
...
if (v_liste_brique.Count == 0) {
v_etat_jeu = EtatJeu.terminer;
v_stopwatch.Stop();
}
if (x_balle.P_BoiteEnglobante.Z >= 3.7d) {
v_etat_jeu = EtatJeu.terminer;
v_stopwatch.Stop();
}
...
}
...
}
Dès que v_etat_jeu se trouve à EtatJeu.terminer, on traite cet état de fin de jeu. La fin de
jeu consiste à stopper le rendu d’animation en retirant le gestionnaire de l’événement
Rendering de CompositionTarget. On rend visible le Canvas x_cnv_partie_fin qui
correspond à l’écran d’affichage quand le jeu est terminé. Il se compose d’un TextBlock
x_partie_fin_texte qui affiche un message: soit la partie est perdue, soit la partie est
gagnée en indiquant le nombre de points et le temps écoulé. Un bouton x_btn_rejouer
permet, par un clic, de remettre le jeu à zéro et de recommencer une nouvelle partie.
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
if (v_etat_jeu == EtatJeu.terminer) {
CompositionTarget.Rendering -= CompositionTarget_Rendering;
x_cnv_partie_fin.Visibility = Visibility.Visible;
string message = «»;
if (v_liste_brique.Count != 0) {
message = «Partie perdue !!!»;
}
else {
message = «Partie gagnée avec « + v_score.ToString() + « pts en «;
message += v_stopwatch.Elapsed.Minutes.ToString(«00») + «:» +
v_stopwatch.Elapsed.Seconds.ToString(«00»);
}
x_partie_fin_texte.Text = message;
} ... }
312 Programmez des jeux vidéo 3D avec C#5 et WPF
La figure 7.23 visualise le résultat obtenu quand la partie est terminée: sur le repère 1
la partie est gagnée, et sur le repère 2 la partie est perdue.
FIGURE 7.23

Copyright 2013 Patrice REY

Le gestionnaire de l’événement Click du bouton x_btn_rejouer permet de réinitialiser


un nouveau jeu. On crée une nouvelle liste de briques, on replace la balle à sa position
CHAPITRE 7 Construction du jeu du Casse-Briques 313

de départ, on réinitialise l’horloge par la méthode Restart, on remet à zéro les variables
v_score et v_nb_image, et on redémarre le rendu d’animation.
//clic sur bouton rejouer une partie
private void x_btn_rejouer_Click(object sender, RoutedEventArgs e) {
x_cnv_partie_fin.Visibility = Visibility.Hidden;
RemplirListeAvecLesBriques();
v_etat_jeu = EtatJeu.en_cours;
x_balle.ReinitialiserEmplacement();
v_vitesse_taux_x = 0d;
v_vitesse_taux_z = -v_vitesse_taux;
v_vitesse_x = v_vitesse_taux_x;
v_vitesse_z = v_vitesse_taux_z;
x_balle.AppliquerTranslation(v_vitesse_x, 0, v_vitesse_z);
v_stopwatch.Restart();
v_stopwatch.Start();
v_score = 0;
v_nb_image = 0;
x_affiche_score_texte.Text = v_score.ToString(«00000»);
x_affiche_temps_texte.Text = v_score.ToString(«00:00»);
x_partie_fin_texte.Text = «???»;
CompositionTarget.Rendering += CompositionTarget_Rendering;
}
8
Bonification du jeu par
l’animation

Au sommaire de ce chapitre
• Apprendre le concept des animations pour le passage entre différents
écrans du jeu
• Programmer un storyboard avec XAML et avec C#
• Programmer l’animation d’un composant par l’intermédiaire d’un
storyboard
• Utiliser et programmer les animations réalistes de façon à ajouter une
touche professionnelle au jeu
• Simuler le roulement d’une balle sur le plateau du jeu
• Apprendre à générer des surfaces de plateau qui ne sont pas planes
• Apprendre à importer des objets 3D au format Wavefront OBJ (objet
exporté par exemple depuis MAYA au format OBJ)
316 Programmez des jeux vidéo 3D avec C#5 et WPF
L’animation est essentielle dans un jeu. Elle permet de rendre le jeu beaucoup plus
attractif par l’ajout de composants animés et elle donne un aspect professionnel aux
transitions dans le passage entre les différents écrans.
Nous allons voir dans ce chapitre comment fonctionne l’animation au travers de ses
classes, et comment nous pouvons utiliser ces classes pour améliorer sensiblement le
jeu dans son fonctionnement.

1 - Les règles de l’animation

Généralement, une animation est considérée comme une succession d’images qui
sont visualisées les unes après les autres. L’animation dans WPF utilise un modèle
radicalement différent.
Le modèle d’animation de WPF permet d’animer certaines propriétés des éléments.
Leur progression se configure sur une durée donnée au moyen de différentes stratégies
qui sont:
• une progression linéaire entre une valeur de départ et une valeur cible.
• une application d’algorithmes d’interpolation prédéfinis entre une valeur de départ
et une valeur cible afin d’augmenter le réalisme.
• une progression parmi une série de valeurs intermédiaires au moyen de différents
algorithmes d’interpolation.
Le système d’animation s’applique aux propriétés de dépendances des objets héritant
de DependencyProperty. Une très grande partie des propriétés de la plupart des
objets, y compris non visuels, sont concernées.
Une animation est définie au moyen d’un objet qui dépend du type de la propriété
de dépendance ciblée. Différents types supportent l’animation: Boolean, Byte, Char,
Decimal, Double, Int, Single, Point, Color, Size, Rect, Thickness, String, Object, Vector,
Vector3D, Point3D, Matrix, Rotation3D et Quaternion. Les classes abstraites de
base d’animation correspondantes ont un préfixe qui correspond au type supporté:
DoubleAnimationBase est la classe abstraite de base pour animer des valeurs de type
double, StringAnimationBase est la classe abstraite de base pour animer des valeurs
Copyright 2013 Patrice REY

de type string, etc. La figure 8.1 visualise l’arbre d’héritage. La classe d’animation
dérivée et utilisée pour animer le type correspondant est préfixée par le type: par
exemple DoubleAnimation pour animer le type double, etc.
Toutes les classes abstraites de base d’animation héritent de la classe
AnimationTimeline. Ces classes abstraites de base sont: BooleanAnimationBase qui
anime des valeurs de type bool, ByteAnimationBase qui anime des valeurs de type
byte, CharAnimationBase qui anime des valeurs de type char, DecimalAnimationBase
CHAPITRE 8 Bonification du jeu par l’animation 317

qui anime des valeurs de type decimal, DoubleAnimationBase qui anime des valeurs
de type double, Int16AnimationBase, Int32AnimationBase et Int64AnimationBase qui
animent des valeurs de type int, SingleAnimationBase qui anime des valeurs de type
single, PointAnimationBase qui anime des valeurs de type Point,ColorAnimationBase
qui anime des valeurs de type Color, SizeAnimationBase qui anime des valeurs de type
Size, RectAnimationBase qui anime des valeurs de type Rect, ThicknessAnimationBase
qui anime des valeurs de type Thickness, StringAnimationBase qui anime des
valeurs de type string, ObjectAnimationBase qui anime des valeurs de type object,
VectorAnimationBase qui anime des valeurs de type Vector, Vector3DAnimationBase
qui anime des valeurs de type Vector3D, Point3DAnimationBase qui anime des
valeurs de type Point3D, MatrixAnimationBase qui anime des valeurs de type
Matrix, Rotation3DAnimationBase qui anime des valeurs de type Rotation3D, et
QuaternionAnimationBase qui anime des valeurs de type Quaternion.
FIGURE 8.1
318 Programmez des jeux vidéo 3D avec C#5 et WPF
Les animations basiques font varier sur une durée donnée (propriété Duration) la
valeur d’une propriété cible entre une valeur de départ (propriété From) et une valeur
finale (propriété To), ou à partir de la valeur courante en fonction d’un incrément
(propriété By). Ces animations sont qualifiées d’animation From/To/By. Ces classes
sont ByteAnimation, ColorAnimation, DecimalAnimation, DoubleAnimation,
Int16Animation, Int32Animation, Int64Animation, Point3DAnimation,
PointAnimation, QuaternionAnimation, RectAnimation, Rotation3DAnimation,
SingleAnimation, SizeAnimation, ThicknessAnimation, Vector3DAnimation et
VectorAnimation (figure 8.2).
Les animation d’images clé (animations dites KeyFrames) permettent de
définir des valeurs intermédiaires et leurs paramètres d’interpolation. Les
noms de classe sont préfixés par le type et sont suffixés de UsingKeyFrames
comme les classes (figure 8.2): BooleanAnimationUsingKeyFrames qui anime
des valeurs de type bool, ByteAnimationUsingKeyFrames qui anime des
valeurs de type byte, CharAnimationUsingKeyFrames qui anime des valeurs
de type char, DecimalAnimationUsingKeyFrames qui anime des valeurs
de type decimal, DoubleAnimationUsingKeyFrames qui anime des valeurs
de type double, IntAnimationUsingKeyFrames (Int16.., Int32.., Int64..) qui
anime des valeurs de type int, SingleAnimationUsingKeyFrames qui anime
des valeurs de type single, PointAnimationUsingKeyFrames qui anime des
valeurs de type Point, ColorAnimationUsingKeyFrames qui anime des valeurs
de type Color, SizeAnimationUsingKeyFrames qui anime des valeurs de
type Size, RectAnimationUsingKeyFrames qui anime des valeurs de type
Rect, ThicknessAnimationUsingKeyFrames qui anime des valeurs de type
Thickness, StringAnimationUsingKeyFrames qui anime des valeurs de type
string, ObjectAnimationUsingKeyFrames qui anime des valeurs de type
object, VectorAnimationUsingKeyFrames qui anime des valeurs de type
Vector, Vector3DAnimationUsingKeyFrames qui anime des valeurs de type
Vector3D, Point3DAnimationUsingKeyFrames qui anime des valeurs de type
Point3D, MatrixAnimationUsingKeyFrames qui anime des valeurs de type Matrix,
Rotation3DAnimationUsingKeyFrames qui anime des valeurs de type Rotation3D, et
Copyright 2013 Patrice REY

QuaternionAnimationUsingKeyFrames qui anime des valeurs de type Quaternion.


Enfin, il y a les animations qui utilisent un tracé pour obtenir leurs valeurs. Le
tracé utilisé est un objet PathGeometry. Ce type d’animation permet par exemple
de faire déplacer un objet visuel le long d’un tracé. C’est pourquoi ces classes
d’animation sont préfixées par le type et elles sont suffixées par UsingPath
comme les classes DoubleAnimationUsingPath, MatrixAnimationUsingPath et
CHAPITRE 8 Bonification du jeu par l’animation 319

PointAnimationUsingPath (figure 8.2).


FIGURE 8.2
320 Programmez des jeux vidéo 3D avec C#5 et WPF
La classe Storyboard définit un objet qui coordonne des animations et permet de
leur assigner une propriété cible au moyen des propriétés attachées Storyboard.
TargetProperty et Storyboard.TargetName. Un objet Storyboard ne peut cibler qu’une
propriété d’un seul objet FrameworkElement. La figure 8.3 visualise l’arbre d’héritage
de la classe Storyboard.
La traduction en français du terme storyboard est table de montage séquentiel. Le rôle
de l’objet Storyboard peut être comparé à celui d’un séquenceur, et les animations
peuvent être comparées aux pistes du séquenceur. Nous utiliserons le terme de
storyboard, plus souvent employé, plutôt que le terme de table de montage séquentiel.
Un objet Storyboard dispose de méthodes permettant de contrôler l’animation par le
code. Ces méthodes sont principalement:
• la méthode Begin qui démarre l’animation.
• la méthode Pause qui met l’animation en pause.
• la méthode Resume qui effectue une reprise de l’animation après la pause.
• la méthode Stop qui arrête l’animation.
• la méthode statique SetTargetName qui cible l’objet de dépendance Timeline par
son nom,
• la méthode statique SetTargetProperty qui cible l’objet de dépendance Timeline par
sa propriété de dépendance.
FIGURE 8.3

Copyright 2013 Patrice REY


CHAPITRE 8 Bonification du jeu par l’animation 321

Toutes les animations ainsi que la classe Storyboard, héritent de la classe Timeline.
On peut traduire le terme Timeline par chronologie. Un objet Timeline représente
un segment de temps correspondant à l’exécution d’une animation. Il dispose de
propriétés permettant de définir sa durée, son moment relatif au démarrage, s’il doit
être répété, sa réversibilité, son facteur d’accélération, sa vitesse et son comportement
en fin de séquence. De plus il supporte l’imbrication.
Le Storyboard est un conteneur d’objets Timeline par sa propriété Children. Il est donc
possible d’imbriquer plusieurs Storyboard. La durée de base d’un objet Timeline est
déterminée par la propriété Duration, qui peut être définie en XAML par une chaîne
au format:
[ jour.]heures:minutes[:secondes[.fractions]]
Duration = «0:0:0.8»
Duration = «00:5:20»
Un objet Duration peut être initialisé par le code au moyen d’un objet TimeSpan
passé au constructeur et obtenu à partir d’une méthode statique telle que TimeSpan.
FromSeconds.
Duration duree= new Duration(TimeSpan.FromSeconds(0.8));
La propriété BeginTime, de type TimeSpan, indique le temps à partir duquel l’exécution
commence. Ce temps est relatif au Storyboard conteneur, ou à son déclenchement s’il
s’agit de l’objet racine.
<Storyboard BeginTime= «0:0:8» />
La propriété RepeatBehavior permet de configurer la répétition d’un objet Timeline
ainsi que la façon de le répéter. Elle définit:
• soit le nombre d’exécutions de l’objet Timeline (propriété Count).
• soit la durée de l’exécution (propriété Duration); l’exécution est répétée tant que
cette durée n’est pas atteinte.
La propriété statique RepeatBehavior.Forever renvoie un objet RepeatBehavior qui
représente une durée de répétition infinie. Cet objet peut être assigné à la propriété
RepeatBehavior de l’objet Timeline pour répéter en boucle son exécution.
<Storyboard RepeatBehavior= «Forever» />
La propriété booléenne AutoReverse permet d’inverser l’exécution d’un objet Timeline
après une première exécution normale.
<Storyboard AutoReverse= «True» />
La propriété SpeedRatio permet de faire varier la vitesse d’exécution d’un objet Timeline
par un facteur correspondant à sa valeur (1.0 par défaut) et relatif au conteneur. Les
322 Programmez des jeux vidéo 3D avec C#5 et WPF
facteurs des différents niveaux se multiplient entre eux: si un conteneur a un SpeedRatio
de 3.0 et que le storyboard imbriqué a un facteur de 2.0, le facteur résultant sera de 6.
La propriété énumérée FillBehavior détermine ce qu’il advient de la valeur de la
propriété cible à l’issue d’une animation:
• la valeur HoldEnd (par défaut) indique que la valeur finale de l’animation est
conservée.
• la valeur Stop indique que la valeur initiale de la propriété cible avant l’animation
est restaurée.
<Storyboard FillBehavior= «Stop» />

2 - La transition entre deux écrans

L’animation est couramment employée pour réaliser des transitions dans le passage
d’un écran à l’autre. Comme au début du jeu, un bouton permet de démarrer le jeu.
Une animation contrôlée permet d’effectuer une transition dans laquelle un temps
court s’écoule, et durant lequel des actions se déroulent en arrière-plan (comme des
initialisations, une mise en place d’objet, le démarrage d’une horloge, etc.).

2.1 - La réalisation en image

Jusqu’à présent dans notre jeu, le lancement du jeu se faisait en cliquant sur le
bouton Lancer le jeu, l’écran perdait sa visibilité et le jeu démarrait automatiquement.
Maintenant nous allons ajouter des animations qui vont servir de transition entre le
passage des écrans.
La première animation ajoutée (figure 8.4) consiste à réduire l’écran de départ d’une
façon proportionnelle en largeur et en hauteur (repères 1 et 2 de la figure 8.4), durant
un temps déterminé, dévoilant ainsi en arrière-plan la surface du jeu.
La deuxième animation ajoutée (figure 8.5) consiste à réaliser un composant qui,
durant une chronologie de 4 secondes, affiche un décompte horaire allant de 3 à 0
avec un arc de cercle doté d’une épaisseur qui se déroule de 0° à 360°. Sur le repère 1
de la figure 8.5, le décompte affiche 3 et l’arc se déroule avec un angle de 0° à 90°. Sur
Copyright 2013 Patrice REY

le repère 2 de la figure 8.5, le décompte affiche 2 et l’arc se déroule avec un angle de


90° à 180°. Sur le repère 3 de la figure 8.5, le décompte affiche 1 et l’arc se déroule avec
un angle de 180° à 270°. Et sur le repère 4 de la figure 8.5, le décompte affiche 0 et l’arc
se déroule avec un angle de 270° à 360°. Le temps que cet arc se déroule lors de cette
chronologie, le déplacement de la raquette est activé pour que l’utilisateur se prépare
au lancement du jeu en positionnant sa raquette comme il le souhaite.
FIGURE 8.4

2
FIGURE 8.5

4
CHAPITRE 8 Bonification du jeu par l’animation 325

La troisième et dernière animation ajoutée (figure 8.6) consiste à faire apparaître l’écran
de fin de jeu par modification de son opacité. Le jeu se terminant (partie gagnée ou
partie perdue), l’écran de fin de jeu devient visible avec une opacité fixée à 0 (signifiant
que l’écran est complètement transparent). Puis l’animation consiste à faire passer, de
façon continue, l’opacité de l’écran de fin de jeu de la valeur 0 à la valeur 1 (signifiant
l’écran complètement opaque).
FIGURE 8.6

2.2 - Programmer un storyboard avec XAML

Le fait d’ajouter une animation entre l’écran de départ (jeu à l’arrêt) et l’écran de jeu
en cours nous oblige à rajouter une étape dans le cours du jeu qui est un prélude.
L’énumération EtatJeu est complétée par EtatJeu.prelude (elle possèdait déjà EtatJeu.
arret, EtatJeu.en_cours, et EtatJeu.terminer).
Jusqu’à présent, l’écran de départ, doté d’un bouton pour lancer le jeu, était représenté
par un Canvas x_cnv_lancer. Comme nous allons modifier la taille horizontale et verticale
de l’écran par une chronologie, on remplace le Canvas par un Grid. De cette façon,
quand le contrôle Grid verra ses dimensions réduites, tous les objets qu’il contient
resteront centrés sur le Grid. Avec un Canvas, comme les objets sont positionnés par
326 Programmez des jeux vidéo 3D avec C#5 et WPF
les propriétés attachées Canvas.Top et Canvas.Left, il aurait fallu procéder aussi à une
modification supplémentaire liée au positionnement sur le Canvas. C’est pourquoi il
est préférable d’opter pour un contrôle de type Grid. Le code XAML du Grid x_grid_
lancer est donc le suivant:
<!-- grid initial de lancement de jeu -->
<Grid x:Name=ˈx_grid_lancerˈ Background=ˈ#93FF9494ˈ Width=ˈ1274ˈ Height=ˈ732ˈ
HorizontalAlignment=ˈCenterˈ VerticalAlignment=ˈCenterˈ Visibility=ˈHiddenˈ>
<Button x:Name=ˈx_btn_lancerˈ Content=ˈLancer le jeuˈ Width=ˈ190ˈ Height=ˈ67ˈ
Style=ˈ{StaticResource style_bouton}ˈ FontSize=ˈ20ˈ Click=ˈx_btn_lancer_Clickˈ
HorizontalAlignment=ˈCenterˈ VerticalAlignment=ˈCenterˈ/>
</Grid>
Ce Grid x_grid_lancer possède un fond semi-transparent (propriété Background), il
est centré verticalement et horizontalement dans son conteneur hôte (propriétés
HorizontalAlignment et VerticalAlignment fixées à Center), sa visibilité est cachée
(propriété Visibility fixée à Hidden), et ses dimensions sont fixées explicitement
(propriétés Width = 1274 et Height = 732). Il est important de fixer explicitement les
propriétés Width et Height, de type double, si l’on veut pouvoir les modifier par une
animation, car par défaut elles sont définies à la valeur Auto. Pour le type double, la
valeur Auto est égale à la valeur Double.NaN. La valeur Double.NaN est une valeur
qui n’est pas un nombre mais qui représente le mode automatique par défaut. D’où
l’obligation de définir explicitement la valeur de type double pour pouvoir procéder à
sa modification par la suite.
L’ajout d’une animation par une chronologie se fait en ajoutant un objet Storyboard.
Comme l’objet Storyboard n’est pas un objet héritant de FrameworkElement,
directement ou indirectement, on ne peut donc pas l’ajouter en XAML comme un
contrôle ordinaire (Grid, Canvas, etc.). La solution consiste à le placer comme une
ressource dotée d’une clé de référence, et pour l’utiliser, il suffira de le rechercher par
un appel dans les ressources avec sa clé de référence.
Nous allons ajouter le storyboard aux ressources du Grid en lui notifiant une clé de
référence. Pour ajouter des ressources au Grid x_grid_lancer, on ajoute une balise <Grid.
Resources> qui va contenir toutes les ressources nécessaires. La propriété Resources
d’un contrôle est un dictionnaire contenant une collection de type ResourceDictionary.
Copyright 2013 Patrice REY

<!-- grid initial de lancement de jeu -->


<Grid x:Name=ˈx_grid_lancerˈ Background=ˈ#93FF9494ˈ Width=ˈ1274ˈ Height=ˈ732ˈ
HorizontalAlignment=ˈCenterˈ VerticalAlignment=ˈCenterˈ Visibility=ˈHiddenˈ>
<Grid.Resources> ... </Grid.Resources>
<Button x:Name=ˈx_btn_lancerˈ Content=ˈLancer le jeuˈ Width=ˈ190ˈ Height=ˈ67ˈ
Style=ˈ{StaticResource style_bouton}ˈ FontSize=ˈ20ˈ Click=ˈx_btn_lancer_Clickˈ
HorizontalAlignment=ˈCenterˈ VerticalAlignment=ˈCenterˈ/>
</Grid>
CHAPITRE 8 Bonification du jeu par l’animation 327

Pour se familiariser avec la programmation des animations, nous allons d’abord créer
une animation qui effectue une réduction de la dimension horizontale du Grid lors
d’une chronologie dont la durée est de 1 seconde. Le schéma ci-dessous visualise ce
que nous allons effectuer. Au temps 0 seconde, l’écran recouvre toute la surface, et au
temps 1 seconde, la dimension horizontale de l’écran est nulle.

l’écran de départ x_grid_lancer,


1 avec un fond semi-transparent,
recouvre toute la surface du jeu

temps = 0 seconde

avec une chronologie de durée


1 seconde, la dimension
2 horizontale de l’écran de départ
diminue proportionnellement

temps = 1 seconde
328 Programmez des jeux vidéo 3D avec C#5 et WPF
Dans les ressources du Grid, on ajoute un objet Storyboard doté d’une clé de référence
k_stb_reduction_hori. L’attribution d’une clé de référence se fait par x:Key en XAML. La
propriété à modifier est Width du contrôle Grid. Elle est de type double. Donc, comme
on a vu au début de ce chapitre, pour modifier une valeur de type double, on utilise
une animation de type DoubleAnimation. Dans le storyboard, on ajoute une piste de
type DoubleAnimation (les pistes sont ajoutées implicitement à la propriété Children
de Storyboard).
<!-- grid initial de lancement de jeu -->
<Grid x:Name=ˈx_grid_lancerˈ Background=ˈ#93FF9494ˈ Width=ˈ1274ˈ Height=ˈ732ˈ
HorizontalAlignment=ˈCenterˈ VerticalAlignment=ˈCenterˈ Visibility=ˈHiddenˈ>
<Grid.Resources>
<Storyboard x:Key=ˈk_stb_reduction_horiˈ >
<DoubleAnimation> ... </DoubleAnimation>
</Storyboard>
</Grid.Resources>
<Button x:Name=ˈx_btn_lancerˈ Content=ˈLancer le jeuˈ Width=ˈ190ˈ Height=ˈ67ˈ
Style=ˈ{StaticResource style_bouton}ˈ FontSize=ˈ20ˈ Click=ˈx_btn_lancer_Clickˈ
HorizontalAlignment=ˈCenterˈ VerticalAlignment=ˈCenterˈ/>
</Grid>
L’animation DoubleAnimation cible la propriété Width de x_grid_lancer. Donc on ajoute
la propriété attachée Storyboard.TargetName en lui affectant le nom du contrôle à
référencer (Storyboard.TargetName=ˈx_grid_lancerˈ), et on ajoute la propriété attachée
Storyboard.TargetProperty en lui affectant le nom de la propriété ciblée (Storyboard.
TargetProperty=ˈWidthˈ). La chaîne qui est affectée à la propriété TargetProperty est
du type PropertyPath. Un objet PropertyPath implémente une structure de donnée
représentant un chemin pour accéder à une propriété de dépendance choisie. Ici on
exprime ce chemin par la chaîne Width (propriété Width de Grid). On aurait pu écrire
aussi la chaîne (Grid.Width) pour exprimer la même chose (attention, les parenthèses
ouvrante et fermante sont obligatoires).
<!-- grid initial de lancement de jeu -->
<Grid x:Name=ˈx_grid_lancerˈ Background=ˈ#93FF9494ˈ Width=ˈ1274ˈ Height=ˈ732ˈ
HorizontalAlignment=ˈCenterˈ VerticalAlignment=ˈCenterˈ Visibility=ˈHiddenˈ>
<Grid.Resources>
<Storyboard x:Key=ˈk_stb_reduction_horiˈ >
Copyright 2013 Patrice REY

<DoubleAnimation Storyboard.TargetName=ˈx_grid_lancerˈ
Storyboard.TargetProperty=ˈWidthˈ>
</DoubleAnimation>
</Storyboard>
</Grid.Resources>
<Button x:Name=ˈx_btn_lancerˈ Content=ˈLancer le jeuˈ Width=ˈ190ˈ Height=ˈ67ˈ
Style=ˈ{StaticResource style_bouton}ˈ FontSize=ˈ20ˈ Click=ˈx_btn_lancer_Clickˈ
HorizontalAlignment=ˈCenterˈ VerticalAlignment=ˈCenterˈ/>
</Grid>
CHAPITRE 8 Bonification du jeu par l’animation 329

Comme on est dans le cas simple d’une animation du type from/to/by, on exprime avec
la propriété From la valeur de départ de la cible (From = «1274»), avec la propriété To
la valeur de fin de la cible (To = «0»), et avec la propriété By la valeur de l’incrément
souhaité (By = «1» pour un incrément de 1 pixel). La propriété Duration exprime la
durée de la chronologie, donc pour une chronologie de 1 seconde on écrira Duration
= «00:00:01» (format heure:minute:seconde). On fixe la propriété AutoReverse à false de
façon à ce qu’elle ne revienne pas au départ par le chemin inverse.
<!-- grid initial de lancement de jeu -->
<Grid x:Name=ˈx_grid_lancerˈ Background=ˈ#93FF9494ˈ Width=ˈ1274ˈ Height=ˈ732ˈ
HorizontalAlignment=ˈCenterˈ VerticalAlignment=ˈCenterˈ Visibility=ˈHiddenˈ>
<Grid.Resources>
<Storyboard x:Key=ˈk_stb_reduction_horiˈ >
<DoubleAnimation Storyboard.TargetName=ˈx_grid_lancerˈ
Storyboard.TargetProperty=ˈWidthˈ From=ˈ1274ˈ To=ˈ0.00ˈ By=ˈ1.00ˈ
Duration=ˈ00:00:01ˈ AutoReverse=ˈFalseˈ>
</DoubleAnimation>
</Storyboard>
</Grid.Resources>
<Button x:Name=ˈx_btn_lancerˈ Content=ˈLancer le jeuˈ Width=ˈ190ˈ Height=ˈ67ˈ
Style=ˈ{StaticResource style_bouton}ˈ FontSize=ˈ20ˈ Click=ˈx_btn_lancer_Clickˈ
HorizontalAlignment=ˈCenterˈ VerticalAlignment=ˈCenterˈ/>
</Grid>
A ce stade l’animation est programmée dans ses fonctionnalités. Maintenant nous
allons voir comment l’exécuter.
Cette animation doit être lancée quand on clique sur le bouton x_btn_lancer. Dans
le gestionnaire de l’événement Click de ce bouton, on ajoute une variable story de
type Storyboard et on lui affecte le storyboard dont la clé de référence est k_stb_
reduction_hori. La propriété Resources du contrôle Grid x_grid_lancer est du type
ResourceDictionary (c’est-à-dire une collection de ressources sous forme d’une liste
générique). Pour accéder à une ressource identifiée par une clé de référence x:Key,
on appelle la propriété Resources en lui passant la chaîne représentant la clé de
référence (x_grid_lancer.Resources[«k_stb_reduction_hori»]), et on effectue un cast pour
le résultat obtenu (ici un cast en Storyboard). Comme on a stocké le storyboard dans
le dictionnaire des ressources de x_grid_lancer, on recherche donc la ressource dans ce
dictionnaire par x_grid_lancer.Resources[..].
Pour démarrer cette chronologie, on applique la méthode Storyboard.Begin.
L’événement Completed de Storyboard se déclenche quand la chronologie est terminée.
On ajoute alors un gestionnaire pour l’événement Completed. Dans ce gestionnaire
intitulé story_Completed, on inscrira les tâches à effectuer pour la poursuite du jeu.
Dès lors, le déclenchement de la chronologie s’effectue bien lors du clic sur le bouton.
330 Programmez des jeux vidéo 3D avec C#5 et WPF
//bouton de lancement du jeu
private void x_btn_lancer_Click(object sender, RoutedEventArgs e) {
x_btn_lancer.IsEnabled = false;
Storyboard story = (Storyboard)x_grid_lancer.Resources[«k_stb_reduction_hori»];
story.Completed += story_Completed;
story.Begin();
}
//evenement storyboard termine
private void story_Completed(object sender, EventArgs e) {
//MessageBox.Show(«storyboard terminé»);
x_grid_lancer.Visibility = Visibility.Hidden;
}
Supposons que l’on souhaite effectuer ce même type d’animation uniquement sur la
propriété Height du Grid (la dimension verticale du Grid), la visualisation de cet effet
serait donc la suivante:

l’écran de départ x_grid_lancer, avec un fond


semi-transparent, recouvre toute la surface du
jeu temps = 0
seconde

temps = 1
seconde

Copyright 2013 Patrice REY

Pour cela, on ajoute un storyboard nommé par la clé de référence k_stb_reduction_verti.


CHAPITRE 8 Bonification du jeu par l’animation 331

Une piste d’animation de type DoubleAnimation est ajoutée à la propriété Children du


Storyboard. Cette animation cible le contrôle x_grid_lancer par la propriété attachée
Storyboard.TargetName, et cible la propriété Height du Grid par la propriété attachée
Storyboard.TargetProperty. La durée de la chronologie est Duration = «00:00:01», en
faisant varier la propriété Height de sa valeur de départ à une valeur nulle par un
incrément de 1.
<!-- grid initial de lancement de jeu -->
<Grid x:Name=ˈx_grid_lancerˈ Background=ˈ#93FF9494ˈ Width=ˈ1274ˈ Height=ˈ732ˈ
HorizontalAlignment=ˈCenterˈ VerticalAlignment=ˈCenterˈ Visibility=ˈHiddenˈ>
<Grid.Resources>
<Storyboard x:Key=ˈk_stb_reduction_vertiˈ >
<DoubleAnimation Storyboard.TargetName=ˈx_grid_lancerˈ
Storyboard.TargetProperty= ˈ(Grid.Height)ˈ To=ˈ0.00ˈ By=ˈ1.00ˈ
Duration=ˈ00:00:01ˈ AutoReverse=ˈFalseˈ>
</DoubleAnimation>
</Storyboard>
</Grid.Resources>
<Button x:Name=ˈx_btn_lancerˈ Content=ˈLancer le jeuˈ Width=ˈ190ˈ Height=ˈ67ˈ
Style=ˈ{StaticResource style_bouton}ˈ FontSize=ˈ20ˈ Click=ˈx_btn_lancer_Clickˈ
HorizontalAlignment=ˈCenterˈ VerticalAlignment=ˈCenterˈ/>
</Grid>
On procède de même pour le lancement de la chronologie k_stb_reduction_verti et pour
la gestion de l’événement Completed qui survient quand cette dernière est terminée.
//bouton de lancement du jeu
private void x_btn_lancer_Click(object sender, RoutedEventArgs e) {
x_btn_lancer.IsEnabled = false;
Storyboard story = (Storyboard)x_grid_lancer.Resources[«k_stb_reduction_verti»];
story.Completed += story_Completed;
story.Begin();
}
//evenement storyboard termine
private void story_Completed(object sender, EventArgs e) {
//MessageBox.Show(«storyboard terminé»);
x_grid_lancer.Visibility = Visibility.Hidden;
}
Maintenant nous allons voir la combinaison de ces deux effets dans un même objet
Storyboard. La combinaison de ces deux effets permet de réduire l’écran de départ
proportionnellement en largeur et en hauteur pour disparaître complètement.
Comme les propriétés de centrage du Grid x_grid_lancer sont fixées à Center par
rapport à son conteneur hôte (propriétés HorizontalAlignment et VerticalAlignment),
le x_grid_lancer reste centré sur la surface du jeu tout au long de la chronologie. La
figure suivante visualise l’effet obtenu.
332 Programmez des jeux vidéo 3D avec C#5 et WPF

l’écran de départ x_grid_lancer,


1 avec un fond semi-transparent,
recouvre toute la surface du jeu

temps = 0 seconde

avec une chronologie de durée


1 seconde, les dimensions
2 horizontale et verticale de
l’écran de départ diminuent
proportionnellement

temps = 1 seconde
Copyright 2013 Patrice REY

On ajoute pour cela un Storyboard avec la clé de référence k_stb_reduction. Comme


une animation de propriété ne peut cibler qu’une propriété à la fois, il suffit d’ajouter
à la propriété implicite Children de Storyboard deux pistes de type DoubleAnimation.
La première piste cible la propriété Width de x_grid_lancer, et la deuxième piste cible la
CHAPITRE 8 Bonification du jeu par l’animation 333

propriété Height de x_grid_lancer.


<!-- grid initial de lancement de jeu -->
<Grid x:Name=ˈx_grid_lancerˈ Background=ˈ#93FF9494ˈ Width=ˈ1274ˈ Height=ˈ732ˈ
HorizontalAlignment=ˈCenterˈ VerticalAlignment=ˈCenterˈ Visibility=ˈHiddenˈ>
<Grid.Resources>
<Storyboard x:Key=ˈk_stb_reductionˈ >
<DoubleAnimation Storyboard.TargetName=ˈx_grid_lancerˈ
Storyboard.TargetProperty=ˈWidthˈ From=ˈ1274ˈ To=ˈ0.00ˈ By=ˈ1.00ˈ
Duration=ˈ00:00:01ˈ AutoReverse=ˈFalseˈ>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetName=ˈx_grid_lancerˈ
Storyboard.TargetProperty= ˈ(Grid.Height)ˈ To=ˈ0.00ˈ By=ˈ1.00ˈ
Duration=ˈ00:00:01ˈ AutoReverse=ˈFalseˈ>
</DoubleAnimation>
</Storyboard>
</Grid.Resources>
<Button x:Name=ˈx_btn_lancerˈ Content=ˈLancer le jeuˈ Width=ˈ190ˈ Height=ˈ67ˈ
Style=ˈ{StaticResource style_bouton}ˈ FontSize=ˈ20ˈ Click=ˈx_btn_lancer_Clickˈ
HorizontalAlignment=ˈCenterˈ VerticalAlignment=ˈCenterˈ/>
</Grid>
La procédure de lancement (Storyboard.Begin) et la gestion de l’événement Completed
reste identique.
//bouton de lancement du jeu
private void x_btn_lancer_Click(object sender, RoutedEventArgs e) {
x_btn_lancer.IsEnabled = false;
Storyboard story = (Storyboard)x_grid_lancer.Resources[«k_stb_reduction»];
story.Completed += story_Completed;
story.Begin();
}
//evenement storyboard termine
private void story_Completed(object sender, EventArgs e) {
//MessageBox.Show(«storyboard terminé»);
x_grid_lancer.Visibility = Visibility.Hidden;
}

2.3 - Animer un composant par un storyboard

Après avoir vu le mécanisme de fermeture de l’écran de départ, nous allons voir le


mécanisme d’animation d’un composant qui affiche un compte à rebours. Quand
l’écran de départ est fermé, le compte à rebours démarre pour une chronologie de 4
secondes. Durant ce temps, on active le déplacement de la raquette, ce qui permet à
l’utilisateur de se positionner correctement avant le lancement du jeu proprement dit.
334 Programmez des jeux vidéo 3D avec C#5 et WPF
On positionne un Grid x_grid_cpt_rebours centré sur la surface du jeu. Dans ce conteneur,
on y ajoute un composant de type Arc, nommé x_arc_rebours et un TextBlock nommé
x_arc_rebours_text. Comme le montre la figure 8.7 visualisant la chronologie sur 4
secondes, un arc, avec une épaisseur prédéfinie, grandit en passant d’un angle de 0° à
un angle de 360° soit 90° de parcours durant une seconde de temps horaire. A chaque
seconde, le TextBlock affiche le décompte.
FIGURE 8.7

3
2
3
t=1

2
t=0
DEBUT

0 FIN
t=4

t=2

1
0 t=3
Copyright 2013 Patrice REY

0 1
CHAPITRE 8 Bonification du jeu par l’animation 335

L’arc du compte à rebours est un composant de type Arc, dérivant de la classe abstraite
Shape. La classe Shape est une classe de base qui sert à dessiner des éléments d’une
figure comme le rectangle, l’ellipse, le polygone, etc.
On ajoute une classe Arc qui hérite de Shape (fichier Arc.cs). Pour animer la rotation de
l’arc, il faudra pouvoir modifier à un instant donné la valeur de l’angle de fin en fonction
de l’angle de début, de façon à dessiner l’arc entre ces deux angles. On dote alors l’arc
de deux propriétés de dépendance qui sont StartAngle et EndAngle, que l’on initialise
à une valeur nulle de type double. Pour permettre à une modification automatique de
l’arc en cas de modification de la valeur des propriétés de dépendances StartAngle et
EndAngle, on utilise l’énumération FrameworkPropertyMetadataOptions.AffectsRender
dans la déclaration de la propriété de dépendance.
public class Arc : Shape {
//propriete de dependance StartAngle
public static readonly DependencyProperty StartAngleProperty =
DependencyProperty.Register(
«StartAngle», typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(0d,
FrameworkPropertyMetadataOptions.AffectsRender));
public double StartAngle {
get { return (double)GetValue(StartAngleProperty); }
set { SetValue(StartAngleProperty, value); }
}
//propriete de dependance EndAngle
public static readonly DependencyProperty EndAngleProperty =
DependencyProperty.Register(
«EndAngle», typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(0d,
FrameworkPropertyMetadataOptions.AffectsRender));
public double EndAngle {
get { return (double)GetValue(EndAngleProperty); }
set { SetValue(EndAngleProperty, value); }
}
...
}
La propriété héritée DefiningGeometry retourne la géométrie de la figure. C’est cette
propriété qui dessine l’arc. Il faut donc redéfinir cette propriété pour y implémenter
le dessin de notre arc. Le dessin de l’arc consiste à trouver la position du point pour
l’angle StartAngle, la position du point pour l’angle EndAngle, et alors dessiner un arc
entre ces deux points. A noter que les valeurs, de type double, que l’on assigne aux
propriétés StartAngle et EndAngle représentent des valeurs d’angles en degrés. Avec
ce niveau d’abstraction, on peut alors aisément modifier les valeurs des angles des
propriétés de dépendances même avec un incrément non entier.
336 Programmez des jeux vidéo 3D avec C#5 et WPF
public class Arc : Shape {
...
//redefinition de la propriete qui represente la geometrie de la figure
protected override Geometry DefiningGeometry {
get { return ConstructionArc(); }
}
//definir la geometrie de la forme
private Geometry ConstructionArc() {
Point startPoint = PointAtAngle(Math.Min(StartAngle, EndAngle));
Point endPoint = PointAtAngle(Math.Max(StartAngle, EndAngle));
Size arcSize = new Size(Math.Max(0, (RenderSize.Width - StrokeThickness) / 2),
Math.Max(0, (RenderSize.Height - StrokeThickness) / 2));
bool isLargeArc = Math.Abs(EndAngle - StartAngle) > 180;
StreamGeometry geom = new StreamGeometry();
using (StreamGeometryContext context = geom.Open()) {
context.BeginFigure(startPoint, false, false);
context.ArcTo(endPoint, arcSize, 0, isLargeArc,
SweepDirection.Counterclockwise, true, false);
}
geom.Transform = new TranslateTransform(StrokeThickness / 2,
StrokeThickness / 2);
return geom;
}
//
private Point PointAtAngle(double angle) {
double radAngle = angle * (Math.PI / 180);
double xRadius = (RenderSize.Width - StrokeThickness) / 2;
double yRadius = (RenderSize.Height - StrokeThickness) / 2;
double x = xRadius + xRadius * Math.Cos(radAngle);
double y = yRadius - yRadius * Math.Sin(radAngle);
return new Point(x, y);
}
...
}
D’où la définition XAML du Grid x_grid_cpt_rebours est la suivante, avec un arc dont
les propriétés StartAngle et EndAngle sont initialisées à 0°. On personnalise l’arc en lui
donnant une épaisseur (propriété StrokeThickness) et une couleur de pinceau pour le
remplissage (propriété Stroke avec un dégradé de couleurs ici).
Copyright 2013 Patrice REY

<!-- grid compteur a rebours avant lancement -->


<Grid x:Name=ˈx_grid_cpt_reboursˈ Background=ˈTransparentˈ Width=ˈ300ˈ
Height=ˈ300ˈ>
<composant:Arc x:Name=ˈx_arc_reboursˈ Width=ˈ300ˈ Height=ˈ300ˈ
StrokeThickness=ˈ20ˈ StartAngle=ˈ0ˈ EndAngle=ˈ0ˈ>
<composant:Arc.Stroke>
<LinearGradientBrush EndPoint=ˈ0.5,1ˈ MappingMode=ˈRelativeToBoundingBoxˈ
StartPoint=ˈ0.5,0ˈ>
<GradientStop Color=ˈ#FFFF0505ˈ Offset=ˈ0ˈ/>
CHAPITRE 8 Bonification du jeu par l’animation 337

<GradientStop Color=ˈ#FF75A1FFˈ Offset=ˈ1ˈ/>


</LinearGradientBrush>
</composant:Arc.Stroke>
</composant:Arc>
<TextBlock x:Name=ˈx_arc_rebours_textˈ Text=ˈ3ˈ FontFamily=ˈImpactˈ FontSize=ˈ24ˈ
Foreground=ˈ#FFFFFEFEˈ Width=ˈ22ˈ Height=ˈ30ˈ HorizontalAlignment=ˈCenterˈ
VerticalAlignment=ˈCenterˈ TextAlignment=ˈCenterˈ >
<TextBlock.RenderTransform>
<ScaleTransform CenterX=ˈ11ˈ CenterY=ˈ15ˈ ScaleX=ˈ5ˈ ScaleY=ˈ5ˈ>
</ScaleTransform>
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
Maintenant il faut programmer en XAML le storyboard pour l’animation du compte
à rebours. Dans le dictionnaire des ressources du x_grid_cpt_rebours, on ajoute un
Storyboard avec une clé de référence stb_arc_rebours.
<!-- grid compteur a rebours avant lancement -->
<Grid x:Name=ˈx_grid_cpt_reboursˈ Background=ˈTransparentˈ Width=ˈ300ˈ
Height=ˈ300ˈ>
<Grid.Resources>
<Storyboard x:Key=ˈstb_arc_reboursˈ>
...
</Storyboard>
</Grid.Resources>
...
</Grid>
Les propriétés à animer sont la propriété EndAngle (de type double) de x_arc_rebours (de
type Arc) et la propriété Text (de type string) de x_arc_rebours_text (de type TextBlock).
La propriété EndAngle de x_arc_rebours est de type double, et sa valeur doit varier de
0 à 360 durant la chronologie d’une durée de 4 secondes. On ajoute alors une piste de
type DoubleAnimation avec:
• la propriété attachée Storyboard.TargetName est la chaîne x_arc_rebours,
• la propriété attachée Storyboard.TargetProperty est la chaîne EndAngle,
• une valeur de départ de 0 pour la propriété From,
• une valeur de fin de 359.99 pour la propriété To (on met 359.99 au lieu de 360 car
l’angle 360 correspond à un angle de 0 pour le dessin de l’arc),
• la propriété AutoReverse à false,
• la propriété BeginTime à «00:00:00» pour signifier un départ immédiat de
l’animation.
<!-- grid compteur a rebours avant lancement -->
<Grid x:Name=ˈx_grid_cpt_reboursˈ Background=ˈTransparentˈ Width=ˈ300ˈ
Height=ˈ300ˈ>
338 Programmez des jeux vidéo 3D avec C#5 et WPF
<Grid.Resources>
<Storyboard x:Key=ˈstb_arc_reboursˈ>
<DoubleAnimation Storyboard.TargetName=ˈx_arc_reboursˈ
Storyboard.TargetProperty=ˈEndAngleˈ From=ˈ0.00ˈ To=ˈ359.99ˈ
Duration=ˈ00:00:04ˈ AutoReverse=ˈFalseˈ BeginTime=ˈ00:00:00ˈ>
</DoubleAnimation>
...
</Storyboard>
</Grid.Resources>
...
</Grid>
La propriété Text de x_arc_rebours_text à animer est de type string. Il est possible
de l’animer en utilisant une classe dérivée de la classe de base abstraite
StringAnimationBase. Cette classe de base abstraite ne possède qu’une classe dérivée
qui est StringAnimationUsingKeyFrames (classe qui représente une animation par
image clé).
Comme le visualise le graphe de la figure 8.8, un objet StringAnimationUsingKeyFrames
possède une propriété KeyFrames qui est une collection de type
StringKeyFrameCollection. La collection de type StringKeyFrameCollection contient
des objets (propriété Items) de type StringKeyFrame. Comme la classe StringKeyFrame
est une classe abstraite et donc non instanciable, il faut se reporter à sa classe
dérivée DiscreteStringKeyFrame. Donc il faut ajouter, à la propriété KeyFrames
(propriété implicite en XAML) de StringAnimationUsingKeyFrames, des objets
DiscreteStringKeyFrame. Un objet DiscreteStringKeyFrame possède une propriété
héritée KeyTime qui indique l’heure à laquelle la propriété héritée Value doit être prise
en compte.
Pour animer la propriété Text, de type string, de x_arc_rebours_text, on ajoute une
piste de type StringAnimationUsingKeyFrames qui cible l’objet x_arc_rebours_text
(Storyboard.TargetName=ˈx_arc_rebours_textˈ) et qui cible la propriété Text (Storyboard.
TargetProperty=ˈ(TextBlock.Text)ˈ). On fixe à cette animation une durée de 4 secondes
(Duration = «00:00:04») et un horaire de lancement immédiat (BeginTime =
«00:00:00»). Ensuite, dans la propriété implicite KeyFrames en XAML, on ajoute des
objets DiscreteStringKeyFrame en spécifiant leur propriété Value et KeyTime. Ici on
Copyright 2013 Patrice REY

aura 4 objets DiscreteStringKeyFrame qui seront:


• le premier avec une valeur Value = «3» et une valeur Keytime = «00:00:00»,
• le deuxième avec une valeur Value = «2» et une valeur Keytime = «00:00:01»,
• le troisième avec une valeur Value = «1» et une valeur Keytime = «00:00:02»,
• le quatrième avec une valeur Value = «0» et une valeur Keytime = «00:00:03».
CHAPITRE 8 Bonification du jeu par l’animation 339
FIGURE 8.8

<Grid.Resources>
<Storyboard x:Key=ˈstb_arc_reboursˈ>
<DoubleAnimation Storyboard.TargetName=ˈx_arc_reboursˈ
Storyboard.TargetProperty=ˈEndAngleˈ From=ˈ0.00ˈ To=ˈ359.99ˈ
Duration=ˈ00:00:04ˈ AutoReverse=ˈFalseˈ BeginTime=ˈ00:00:00ˈ>
</DoubleAnimation>
<StringAnimationUsingKeyFrames
Storyboard.TargetName=ˈx_arc_rebours_textˈ
Storyboard.TargetProperty=ˈ(TextBlock.Text)ˈ Duration=ˈ00:00:04ˈ
BeginTime=ˈ00:00:00ˈ>
<DiscreteStringKeyFrame Value=ˈ3ˈ KeyTime=ˈ0:0:0ˈ />
<DiscreteStringKeyFrame Value=ˈ2ˈ KeyTime=ˈ0:0:1ˈ />
<DiscreteStringKeyFrame Value=ˈ1ˈ KeyTime=ˈ0:0:2ˈ />
<DiscreteStringKeyFrame Value=ˈ0ˈ KeyTime=ˈ0:0:3ˈ />
</StringAnimationUsingKeyFrames>
</Storyboard>
</Grid.Resources>
...
</Grid>
Pour déclencher cette animation du compte à rebours, on place dans le gestionnaire de
l’événement Completed de la précédente animation un déclencheur qui va rechercher
le storyboard de référence stb_arc_rebours dans le dictionnaire des ressources de x_
grid_cpt_rebours, et qui lui applique la méthode Storyboard.Begin.
340 Programmez des jeux vidéo 3D avec C#5 et WPF
On en profite pour activer le rendu image par image (avec CompositionTarget.Rendering).
Cela permet d’activer le rendu et donc de pouvoir déplacer la raquette avec la souris.
Et on passe le jeu dans l’état EtatJeu.prelude.
La question que l’on pourrait se poser concerne l’utilité de l’énumération EtatJeu.prelude.
Le fait de passer par cette énumération permet d’activer ou non des fonctionnalités.
Dans le gestionnaire du rendu image par image (gestionnaire CompositionTarget_
Rendering), une instruction conditionnelle teste la variable v_etat_jeu, et si sa valeur
est EtatJeu.prelude, alors on active la possibilité de déplacement de la raquette par la
méthode AppliquerTranslation appliquée à la raquette.
//evenement storyboard termine
private void story_Completed(object sender, EventArgs e) {
//MessageBox.Show(«storyboard terminé»);
x_grid_lancer.Visibility = Visibility.Hidden;
x_grid_cpt_rebours.Visibility = Visibility.Visible;
CompositionTarget.Rendering += CompositionTarget_Rendering;
v_etat_jeu = EtatJeu.prelude;
Storyboard stb_arc_rebours =
(Storyboard)x_grid_cpt_rebours.Resources[«stb_arc_rebours»];
stb_arc_rebours.Completed += stb_arc_rebours_Completed;
stb_arc_rebours.Begin();
}
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
if (v_etat_jeu == EtatJeu.prelude) {
x_raquette.AppliquerTranslation(v_deplac / 320d, 0, 0);
}
...
}
On ajoute un gestionnaire pour l’événement Completed du compte à rebours de façon
à procéder aux initialisations nécessaires pour le démarrage proprement dit du jeu (le
jeu passe à l’état EtatJeu.en_cours).
//evenement storyboard arc termine
private void stb_arc_rebours_Completed(object sender, EventArgs e) {
x_grid_cpt_rebours.Visibility = Visibility.Hidden;
v_etat_jeu = EtatJeu.en_cours;
Copyright 2013 Patrice REY

v_vitesse_taux_x = 0d;
v_vitesse_taux_z = -v_vitesse_taux;
v_stopwatch.Start();
v_score = 0;
v_apparition_boom_balle = 0;
x_affiche_score_texte.Text = v_score.ToString(«00000»);
x_affiche_temps_texte.Text = v_score.ToString(«00:00»);
x_partie_fin_texte.Text = «???»;
}
CHAPITRE 8 Bonification du jeu par l’animation 341

2.4 - Programmer un storyboard avec C#

Nous avons vu comment programmer un storyboard avec XAML lors du passage de


l’écran de départ au compte à rebours, puis lors du passage du compte à rebours
au démarrage du jeu proprement dit. Maintenant, nous allons nous intéresser à la
programmation d’un storyboard par un code C#, et notamment en ce qui concerne
l’apparition de l’écran de fin de jeu.
Comme le visualise le schéma ci-dessous, on a une chronologie d’une durée de 0.5
seconde. A l’instant t=0s, l’écran de fin de jeu x_cnv_partie_fin est affichée à l’écran (sa
propriété Visible est fixée à Visibility.Visible) mais son opacité est fixée à 0 (propriété
Opacity = 0). A l’instant t=0.5s, son opacité est fixée à 1 (propriété Opacity = 1).

1 temps = 0 seconde

avec une chronologie de durée 0.5


seconde, l’opacité de l’écran de fin de
jeu augmente proportionnellement,
passant d’une opacité de 0 (transparent)
à une opacité de 1 (opaque)

2 temps = 0.5 seconde


342 Programmez des jeux vidéo 3D avec C#5 et WPF
Entre l’instant t=0s et l’instant t=0.5s, l’opacité de l’écran de fin de jeu va varier
proportionnellement pour passer de la valeur 0 à la valeur 1. Il faut alors programmer
un storyboard pour animer la propriété Opacity quand le jeu est dans la phase EtatJeu.
terminer. Dans la gestionnaire du rendu d’animation (gestionnaire CompositionTarget_
Rendering), lors du passage du jeu par l’état EtatJeu.terminer, on stoppe le rendu
d’animation en retirant l’abonnement au gestionnaire CompositionTarget_Rendering
(par CompositionTarget.Rendering -= CompositionTarget_Rendering), et on rend visible
le Canvas x_cnv_partie_fin en lui affectant une opacité de 0 (x_cnv_partie_fin.Opacity =
0d).
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
if (v_etat_jeu == EtatJeu.terminer) {
CompositionTarget.Rendering -= CompositionTarget_Rendering;
x_cnv_partie_fin.Visibility = Visibility.Visible;
x_cnv_partie_fin.Opacity = 0d;
x_btn_rejouer.IsEnabled = false;
string message = «»;
if (v_liste_brique.Count != 0) {
message = «Partie perdue !!!»;
}
else {
message = «Partie gagnée avec « + v_score.ToString() + « pts en «;
message += v_stopwatch.Elapsed.Minutes.ToString(«00») + «:»
+ v_stopwatch.Elapsed.Seconds.ToString(«00»);
}
x_partie_fin_texte.Text = message;
...
}
...
}
Et là, on instancie un nouvel objet Storyboard intitulé stb_apparition. Pour cibler le nom
du Canvas x_cnv_partie_fin, on utilise la méthode statique Storyboard.SetTargetName
qui reçoit en paramètre le storyboard stb_apparition ainsi que le nom du Canvas
concerné (x_cnv_partie_fin.Name). Pour cibler la propriété du Canvas à animer, on
utilise la méthode statique Storyboard.SetTargetProperty qui reçoit en paramètre le
Copyright 2013 Patrice REY

storyboard stb_apparition ainsi qu’un chemin, de type PropertyPath, pour accéder


à la propriété concernée (propriété Opacity). Le chemin qui permet d’accéder à la
propriété à animer se fait par l’instanciation d’un objet PropertyPath qui reçoit en
paramètre la propriété concernée qui est ici Canvas.OpacityProperty. A noter que les
propriétés de dépendances des contrôles sont toutes suffixées par le mot Property
(comme WidthProperty, OpacityProperty, etc.).
CHAPITRE 8 Bonification du jeu par l’animation 343

//rendu image par image


private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
if (v_etat_jeu == EtatJeu.terminer) {
...
Storyboard stb_apparition = new Storyboard();
Storyboard.SetTargetName(stb_apparition, x_cnv_partie_fin.Name);
Storyboard.SetTargetProperty(stb_apparition,
new PropertyPath(Canvas.OpacityProperty));
...
}
...
}
La propriété à animer est l’opacité de type double. On utilise donc une piste
d’animation nommée anim_opacite de type DoubleAnimation. On lui fixe sa valeur
de départ (propriété From = 0d pour une opacité nulle), sa valeur de fin (propriété To
= 1d pour une opacité complète), sa valeur d’incrément (propriété By = 0.2d pour un
incrément de valeur 0.2), et sa durée de chronologie par la propriété Duration qui reçoit
l’instanciation d’un nouvel objet Duration, recevant en paramètre un objet TimeSpan
de valeur 0.5 seconde. La méthode statique TimeSpan.FromMilliseconds retourne un
intervalle de temps de 500 millisecondes soit 0.5 seconde.
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
if (v_etat_jeu == EtatJeu.terminer) {
...
Storyboard stb_apparition = new Storyboard();
Storyboard.SetTargetName(stb_apparition, x_cnv_partie_fin.Name);
Storyboard.SetTargetProperty(stb_apparition,
new PropertyPath(Canvas.OpacityProperty));
DoubleAnimation anim_opacite = new DoubleAnimation();
anim_opacite.From = 0d;
anim_opacite.To = 1d;
anim_opacite.By = 0.2d;
anim_opacite.Duration = new Duration(TimeSpan.FromMilliseconds(500));
...
}
...
}
On a vu que les pistes d’animation pour un storyboard sont ajoutées à la propriété
Children de Storyboard (collection générique de pistes d’animation). Par la méthode
Add, on ajoute la piste anim_opacite à la propriété Children de stb_apparition par
l’écriture stb_apparition.Children.Add(anim_opacite). Pour pouvoir utiliser ce storyboard,
il faut l’ajouter au dictionnaire des ressources d’un contrôle: ici on va l’ajouter au
344 Programmez des jeux vidéo 3D avec C#5 et WPF
dictionnaire des ressources du Grid x_grid_root par x_grid_root.Resources.Add(«stb_
apparition», stb_apparition). Là encore, on voit que pour ajouter une ressource, on lui
attribue une clé de référence (clé stb_apparition). A noter qu’une clé de référence est
unique donc il faut faire attention à lui donner un nom de référence qui n’existe pas
encore. Un gestionnaire pour l’événement Completed est ajouté au storyboard (pour
effectuer la suite du cours du jeu quand le storyboard sera terminé). Et par la méthode
Storyboard.Begin, on démarre le Storyboard stb_apparition pour exécuter l’animation
de la propriété Opacity.
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
if (v_etat_jeu == EtatJeu.terminer) {
...
Storyboard stb_apparition = new Storyboard();
Storyboard.SetTargetName(stb_apparition, x_cnv_partie_fin.Name);
Storyboard.SetTargetProperty(stb_apparition,
new PropertyPath(Canvas.OpacityProperty));
DoubleAnimation anim_opacite = new DoubleAnimation();
anim_opacite.From = 0d;
anim_opacite.To = 1d;
anim_opacite.By = 0.2d;
anim_opacite.Duration = new Duration(TimeSpan.FromMilliseconds(500));
stb_apparition.Children.Add(anim_opacite);
x_grid_root.Resources.Add(«stb_apparition», stb_apparition);
stb_apparition.Completed += stb_apparition_Completed;
stb_apparition.Begin();
}
...
}
Quand l’événement Completed se produit (ce qui veut dire que la chronologie du
storyboard est terminée), il faut retirer la ressource stb_apparition au dictionnaire
des ressources du Grid x_grid_root par la méthode Remove appliquée à la collection
générique. Si on n’effectue pas ce retrait, un erreur se produira puisque, lors de
l’apparition de l’écran de fin de jeu, on ajoute la ressource stb_apparition avec une
clé de référence qui doit être unique (d’où une erreur indiquant l’inscription d’une
Copyright 2013 Patrice REY

ressource dont la clé de référence existe déjà).


//fin de storyboard pour opacité
private void stb_apparition_Completed(object sender, EventArgs e) {
x_grid_root.Resources.Remove(«stb_apparition»);
x_btn_rejouer.IsEnabled = true;
}
L’écran de fin de jeu x_cnv_partie_fin est alors opérationnel. Un clic sur le bouton x_btn_
rejouer permet de relancer un nouveau jeu. Cela consiste à procéder aux réinitialisations
CHAPITRE 8 Bonification du jeu par l’animation 345

nécessaires concernant les briques, la position de la balle, la vitesse de la balle, la


mise à zéro de l’horloge et du compteur de points. Et cela permet aussi de lancer le
storyboard stb_arc_rebours (concernant le compte à rebours) en allant le récupérer
dans le dictionnaire des ressources où il est stocké (dans x_grid_cpt_rebours.Resources).
De cette façon, on peut de nouveau rejouer une partie complète avec tout ce qui a déjà
été programmé.
//clic sur bouton rejouer une partie
private void x_btn_rejouer_Click(object sender, RoutedEventArgs e) {
x_cnv_partie_fin.Visibility = Visibility.Hidden;
RemplirListeAvecLesBriques();
x_grid_cpt_rebours.Visibility = Visibility.Visible;
CompositionTarget.Rendering += CompositionTarget_Rendering;
v_etat_jeu = EtatJeu.prelude;
Storyboard stb_arc_rebours_rejouer =
(Storyboard)x_grid_cpt_rebours.Resources[«stb_arc_rebours»];
stb_arc_rebours_rejouer.Completed += stb_arc_rebours_rejouer_Completed;
stb_arc_rebours_rejouer.Begin();
x_balle.ReinitialiserEmplacement();
v_vitesse_taux_x = 0d;
v_vitesse_taux_z = -v_vitesse_taux;
v_vitesse_x = v_vitesse_taux_x;
v_vitesse_z = v_vitesse_taux_z;
x_balle.AppliquerTranslation(v_vitesse_x, 0, v_vitesse_z);
v_score = 0;
v_nb_image = 0;
x_affiche_score_texte.Text = v_score.ToString(«00000»);
x_affiche_temps_texte.Text = v_score.ToString(«00:00»);
x_partie_fin_texte.Text = «???»;
}

3 - Ajouter l’effet d’un éclair électrique

Dans ce paragraphe, nous allons nous intéresser à la façon de réaliser une apparition
brève d’un effet d’éclair électrique lorsque la balle entre en collision avec la raquette.
Chaque fois qu’une collision se produit entre la balle et la raquette, il faut faire
apparaître un éclair électrique pendant une courte durée pour donner l’impression
que la balle cogne fort sur la raquette.
Un éclair est par définition une lumière intense et brève, en général sinueuse et ramifiée,
provoquée par une décharge disruptive entre deux nuages ou entre un nuage et le sol,
lors d’un orage. Comme le visualise la figure 8.9, un éclair apparaît pendant quelques
millisecondes lors de la collision entre la balle et la raquette. Rien qu’en image, on voit
bien l’impact professionnel qu’un éclair électrique apporte au jeu.
FIGURE 8.9

1 visualisation de l’éclair avec l’effet de la perspective

2 visualisation de l’éclair avec l’effet de la perspective


CHAPITRE 8 Bonification du jeu par l’animation 347

De façon à apporter un réalisme certain avec cet éclair électrique, on s’arrange pour que
ce dernier se produise à partir de la raquette dans le plan horizontal (OXZ). Le schéma
ci-dessous montre, d’un point de vue conceptuel, la façon de réaliser simplement cet
effet d’éclair électrique.

effet de l’éclair électrique lors


de la collision de la balle sur la
raquette

fichier image «boom_balle_2.png»

TBoom x_boom

On modélise une affichette sur laquelle on applique une texture qui représente un
éclair électrique. La texture doit être au format PNG avec la transparence activée. Ici
on utilise le fichier image boom_balle_2.png (stocké dans le dossier contenu/boom/)
dont les parties blanches du fond sont transparentes.
Cette affichette est du type TBoom. On ajoute une classe TBoom qui hérite de
UIElement3D dans l’espace de noms CasseBrique3D.modele3d, et on lui ajoute les
propriétés de dépendances P_Modele, P_PointReference, P_Largeur, P_Profondeur,
P_CouleurUnie et P_Texture. Sa modélisation est une simple surface rectangulaire
parallèle au plan OXZ. La méthode ajoutée AppliquerTranslation réalise, comme à
l’habitude, une translation de l’objet en fonction des paramètres passés. En XAML, on
ajoute une instance de TBoom nommée x_boom, et on la positionne devant la raquette
348 Programmez des jeux vidéo 3D avec C#5 et WPF
et de façon centrée. A son initialisation, ce contrôle est caché.
<!-- le boom sur la raquette -->
<modele3d:TBoom x:Name=ˈx_boomˈ P_Largeur=ˈ2ˈ P_Profondeur=ˈ3ˈ
Visibility=ˈHiddenˈ >
<modele3d:TBoom.P_PointReference>
<Point3D X=ˈ-1ˈ Y=ˈ0ˈ Z=ˈ3.4ˈ></Point3D>
</modele3d:TBoom.P_PointReference>
<modele3d:TBoom.P_Texture>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<ImageBrush ImageSource=ˈcontenu/boom/boom_balle_2.pngˈ></ImageBrush>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</modele3d:TBoom.P_Texture>
</modele3d:TBoom>
En cas de collision entre la balle et la raquette, l’éclair électrique doit être visible
pendant un temps bref. Le rendu d’animation a une fréquence de l’ordre de 60 images
par seconde. Si on fait apparaître l’éclair électrique pendant la visualisation de 5 images
consécutives, cela correspond à une durée moyenne de 60/5 soit 0.12 seconde (12
millisecondes).
D’un point de vue programmation, on ajoute une variable v_apparition_boom_balle
que l’on initialise à 0. Quand une collision entre la balle et la raquette est détectée
(x_balle.CollisionDetecter(x_raquette) == true), l’éclair x_boom devient visible (propriété
Visibility.Visible), on le positionne en lui appliquant une translation identique à celle
appliquée à la raquette par x_boom.AppliquerTranslation(v_deplac / 320d, 0, 0), et on
incrémente la variable v_apparition_boom_balle.
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
if (v_etat_jeu == EtatJeu.en_cours) {
...
if (x_balle.CollisionDetecter(x_raquette) == true) {
...
//affichage de l’eclair de choc
x_boom.Visibility = Visibility.Visible;
Copyright 2013 Patrice REY

x_boom.AppliquerTranslation(v_deplac / 320d, 0, 0);


v_apparition_boom_balle++;
}
...
}
}
Une instruction conditionnelle permet de continuer à incrémenter la variable v_
apparition_boom_balle tant que sa valeur n’est pas nulle. Quand sa valeur atteint le
CHAPITRE 8 Bonification du jeu par l’animation 349

seuil de 5 (correspondant à une présence de 5 images consécutives dans le rendu


d’animation), l’éclair x_boom perd sa visibilité (Visibility.Hidden) et la variable v_
apparition_boom_balle est remise à zéro.
//rendu image par image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
if (v_etat_jeu == EtatJeu.en_cours) {
...
if (x_balle.CollisionDetecter(x_raquette) == true) {
...
//affichage de l’eclair de choc
x_boom.Visibility = Visibility.Visible;
x_boom.AppliquerTranslation(v_deplac / 320d, 0, 0);
v_apparition_boom_balle++;
}
...
if (v_apparition_boom_balle != 0) {
v_apparition_boom_balle++;
}
if (v_apparition_boom_balle >= 5) {
x_boom.Visibility = Visibility.Hidden;
v_apparition_boom_balle = 0;
}
}
}
Par ce procédé, en cas de collision entre la raquette et la balle, l’éclair électrique
apparaît durant un temps très bref.

4 - Utilisation des animations réalistes

WPF contient aussi un ensemble de classes qui permettent de programmer des


animations dites réalistes. Les animations réalistes intègrent un mécanisme
d’atténuation ou d’accélération progressive permettant de simuler des phénomènes
physiques tels que les rebonds d’une balle ou la chute d’une feuille. Ce mécanisme
nommé fonction d’accélération permet de configurer la courbe d’interpolation d’une
animation.

4.1 - Principe des animations réalistes

Les animations exposent une propriété EasingFunction de type IEasingFunction.


L’interface IEasingFunction définit la fonctionnalité de base d’une fonction
d’accélération, en exposant la méthode Ease. L’implémentation de cette méthode
350 Programmez des jeux vidéo 3D avec C#5 et WPF
définit la fonction d’interpolation de l’animation, c’est-à-dire la façon dont la valeur
animée évolue durant l’animation. Par exemple, dans une interpolation linéaire
(figure 8.10), l’abscisse représente le temps de l’animation, et l’ordonnée représente
la différence entre la valeur initiale et la valeur finale de la propriété cible. Les valeurs
de l’abscisse et de l’ordonnée sont normalisées entre 0 et 1. L’interpolation linéaire est
symbolisée par une droite qui implique que l’évolution de la valeur animée est réalisée
à temps constant.
FIGURE 8.10

Valeurs
= f(t)

Temps t

L’interface IEasingFunction est implémentée par la classe abstraite EasingFunctionBase


dont héritent des classes qui implémentent des interpolations, au nombre de 11
classes (figure 8.11). La classe abstraite EasingFunctionBase fournit la classe de base
pour toutes les fonctions d’accélération. Vous pouvez créer aussi vos propres fonctions
d’accélération personnalisées en héritant de cette classe (nous en verrons un exemple
détaillé au paragraphe 4.2).
Les classes qui héritent de la classe abstraite de base EasingFunctionBase sont
BounceEase, BackEase, CircleEase, ElasticEase, SineEase, ExponentialEase, CubicEase,
PowerEase, QuadraticEase, QuinticEase et QuarticEase.
La classe EasingFunctionBase expose la propriété énumérée EasingMode qui peut
prendre les valeurs EaseIn, EaseOut et EaseInOut. Dans le mode EaseIn, l’interpolation
suit le modèle physique modélisé par la classe. Dans le mode EaseOut, l’interpolation est
inversée par rapport au modèle physique (mode par défaut). Dans le mode EaseInOut,
l’interpolation est en mode EaseIn pour la première moitié et en mode EaseOut pour
Copyright 2013 Patrice REY

la seconde moitié.
La classe SineEase représente une fonction d’accélération qui crée une animation
qui accélère et/ou ralentit à l’aide d’une formule sinus (figure 8.12). Cette fonction
d’accélération est construite d’après la formule sinus suivante:
f(t) = 1 - [ sin(1-t) * π/2 ]
CHAPITRE 8 Bonification du jeu par l’animation 351
FIGURE 8.11

FIGURE 8.12
Valeurs Valeurs
mode EaseIn mode EaseOut
= f(t) = f(t)

Temps t Temps t

Valeurs
= f(t)
mode EaseInOut

Temps t
352 Programmez des jeux vidéo 3D avec C#5 et WPF
La classe BounceEase représente une fonction d’accélération qui crée un effet de
rebondissement animé. Sa propriété Bounces (de type int) fixe le nombre de rebonds et
sa propriété Bounciness (de type double) fixe le dégré d’élasticité des rebonds (facteur
de décroissance des rebonds). La figure 8.13 visualise la représentation de sa courbe.
FIGURE 8.13

Valeurs Valeurs
mode EaseIn mode EaseOut
= f(t) = f(t)

Temps t Temps t

Valeurs
= f(t)

mode EaseInOut

Temps t
La classe BackEase représente une fonction d’accélération qui rétracte le mouvement
d’une animation un peu avant qu’elle ne commence à s’animer (figure 8.14). Sa propriété
Amplitude, de type double, fixe la valeur de l’amplitude de la rétractation (cette valeur
doit être supérieure ou égale à 0, avec une valeur de 1 par défaut). La formule de
représentation de cette fonction est du type (avec A représentant le coefficient de
l’amplitude): f(t) = t3 - t * A * ( sin(t) * π )
Copyright 2013 Patrice REY
CHAPITRE 8 Bonification du jeu par l’animation 353
FIGURE 8.14

Valeurs Valeurs
mode EaseIn mode EaseOut
= f(t) = f(t)

Temps t Temps t

Valeurs
= f(t)

mode EaseInOut

Temps t
La classe CircleEase représente une fonction d’accélération qui crée une animation qui
accélère et/ou ralentit à l’aide d’une fonction circulaire (figure 8.15).
FIGURE 8.15
Valeurs Valeurs
mode EaseIn mode EaseOut
= f(t) = f(t)

Temps t Temps t

Valeurs
= f(t)
mode EaseInOut

Temps t
354 Programmez des jeux vidéo 3D avec C#5 et WPF
La classe ElasticEase représente une fonction d’accélération qui crée une animation qui
ressemble à un ressort oscillant d’avant en arrière jusqu’à ce qu’il se stabilise (figure
8.16). Sa propriété Oscillations, de type int, fixe le nombre d’aller-retour sous forme
d’oscillation, et sa propriété Springiness, de type double, fixe le facteur de décroissance
des oscillations.
FIGURE
8.16
Valeurs Valeurs
= f(t) = f(t) mode EaseOut

mode EaseIn

Temps t Temps t

Valeurs
= f(t)
mode EaseInOut

Temps t

Copyright 2013 Patrice REY


CHAPITRE 8 Bonification du jeu par l’animation 355

La classe ExponentialEase représente une fonction d’accélération qui crée une


animation qui accélère et/ou ralentit à l’aide d’une formule exponentielle (figure 8.17).
Sa propriété Exponent, de type double (valeur 2 par défaut), fixe l’exposant utilisé. La
formule de la fonction d’accélération est:
f(t) = ( exp(a*t) - 1 ) / ( exp(a*t) )
FIGURE
8.17
Valeurs Valeurs
mode EaseIn mode EaseOut
= f(t) = f(t)

Temps t Temps t

Valeurs
= f(t)
mode EaseInOut

Temps t
356 Programmez des jeux vidéo 3D avec C#5 et WPF
La classe QuadraticEase (figure 8.18) représente une fonction d’accélération qui crée
une animation qui accélère et/ou ralentit à l’aide de la formule f(t) = t2 .
FIGURE
8.18
Valeurs Valeurs
mode EaseIn mode EaseOut
= f(t) = f(t)

Temps t Temps t
Valeurs
= f(t)
mode EaseInOut

Temps t
La classe CubicEase (figure 8.19) représente une fonction d’accélération qui crée une
animation qui accélère et/ou ralentit à l’aide de la formule f(t) = t3 .
FIGURE
8.19 Valeurs Valeurs
mode EaseIn mode EaseOut
= f(t) = f(t)

Temps t Temps t
Copyright 2013 Patrice REY

Valeurs
= f(t)
mode EaseInOut

Temps t
CHAPITRE 8 Bonification du jeu par l’animation 357

La classe QuarticEase (figure 8.20) représente une fonction d’accélération qui crée une
animation qui accélère et/ou ralentit à l’aide de la formule f(t) = t4 .
FIGURE
8.20 Valeurs Valeurs
mode EaseIn mode EaseOut
= f(t) = f(t)

Temps t Temps t

Valeurs
= f(t)
mode EaseInOut

Temps t
La classe QuinticEase (figure 8.21) représente une fonction d’accélération qui crée une
animation qui accélère et/ou ralentit à l’aide de la formule f(t) = t5 .
FIGURE
8.21
Valeurs Valeurs
mode EaseIn mode EaseOut
= f(t) = f(t)

Temps t Temps t
Valeurs
= f(t)
mode EaseInOut

Temps t
358 Programmez des jeux vidéo 3D avec C#5 et WPF
La classe PowerEase (figure 8.22) représente une fonction d’accélération qui crée une
animation qui accélère et/ou ralentit à l’aide de la formule f(t) = tP (avec P représentant
l’exposant de la puissance). Sa propriété Power fixe l’exposant de la puissance, de type
double, avec une valeur de 2 par défaut et supérieure ou égale à 0.
FIGURE
8.22
Valeurs Valeurs
mode EaseIn mode EaseOut
= f(t) = f(t)

Temps t Temps t

Valeurs
= f(t)
mode EaseInOut

Temps t

4.2 - Animation réaliste appliquée à une brique

Nous allons mettre en application le principe des animations réalistes lors du déplacement
d’une brique sur le plateau du jeu (dossier 04_VS2012_3d_anim_realiste_1). Pour cela,
une simple application 3D à part (figure 8.23) permet de mettre en évidence l’utilisation
des animations réalistes au travers des 11 classes décrites précédemment plus une
animation personnalisée (pour en comprendre l’implémentation et l’utilisation) et plus
une animation de type classique. Un sélecteur sous forme d’un ComboBox (repère 1
Copyright 2013 Patrice REY

de la figure 8.23) permet de choisir une fonction d’accélération parmi les 11 fonctions
prédéfinies plus une fonction personnalisée et une fonction classique, puis le bouton
«Jouer l’animation» permet d’appliquer l’animation réaliste au déplacement d’une
brique (de type TCube) le long de l’axe X et de façon parallèle. Un cylindre (de type
TCylindre) est positionné au milieu de la brique pour visualiser plus facilement l’impact
de l’effet produit par l’animation réaliste (repère 2 de la figure 8.23).
FIGURE 8.23

2
360 Programmez des jeux vidéo 3D avec C#5 et WPF
Sur la scène, on positionne un plateau de jeu x_sol, de type TSol, texturé avec une
image graphique, une brique x_cube, de type TCube, texturée avec une image
graphique, et un cylindre x_cylindre, de type TCylindre, texturé également. On ajoute à
la propriété Transform de la brique x_cube une transformation x_cube_translate de type
TranslateTransform3D, dont les propriétés OffsetX, OffsetY et OffsetZ sont fixées à 0.
La variable v_choix stocke le choix de l’animation choisie au travers d’une énumération
de type ChoixAnim (ChoixAnim.classique, ChoixAnim.sine, ChoixAnim.back, ChoixAnim.
bounce, ChoixAnim.circle, ChoixAnim.cubic, ChoixAnim.elastic, ChoixAnim.exponent,
ChoixAnim.power, ChoixAnim.quadratic, ChoixAnim.quartic, ChoixAnim.quintic,
ChoixAnim.fonction_palier).
Quand un choix est effectué par le sélecteur, le contrôle Image x_img_inter affiche
l’image de la représentation de l’interpolation choisie. Quand survient un clic sur le
bouton x_btn_anim, le storyboard correspondant à l’animation est chargé depuis le
dictionnaire des ressources du x_grid_root dans lequel il est stocké, puis il est exécuté. On
se contente d’animer uniquement la valeur de la propriété OffsetX de la transformation
x_cube_translate. Comme c’est une propriété de type double, on utilisera une piste
d’animation de type DoubleAnimation. Pour cibler cette transformation, l’animation
aura une propriété attachée Storyboard.TargetName = «x_cube_translate». Pour cibler
la propriété OffsetX de cette transformation, l’animation aura une propriété attachée
Storyboard.TargetProperty = «(TranslateTransform3D.OffsetX)». A chaque fois, la valeur
de la propriété OffsetX passera de la valeur 0 à 2.5 par incrément de 0.1 pour une
chronologie de 5 secondes. Le storyboard k_stb_classique effectue le rendu d’une
animation classique comme on a déjà pu le voir auparavant.
<Grid.Resources>
<Storyboard x:Key=ˈk_stb_classiqueˈ>
<DoubleAnimation Storyboard.TargetName=ˈx_cube_translateˈ
Storyboard.TargetProperty=ˈ(TranslateTransform3D.OffsetX)ˈ From=ˈ0ˈ To=ˈ2.5ˈ
By=ˈ0.1ˈ Duration=ˈ00:00:05ˈ AutoReverse=ˈFalseˈ>
</DoubleAnimation>
</Storyboard>
</Grid.Resources>
Le storyboard k_stb_sineease effectue une animation réaliste selon une fonction
Copyright 2013 Patrice REY

d’accélération de type SineEase. Pour ajouter une fonction d’accélération, on édite la


propriété EasingFunction de DoubleAnimatio (par <DoubleAnimation.EasingFunction>),
et on lui affecte un objet SineEase. Cet objet SineEase a sa propriété EasingMode que
l’on fixe à EaseInOut.
<Grid.Resources>
<Storyboard x:Key=ˈk_stb_sineeaseˈ>
<DoubleAnimation Storyboard.TargetName=ˈx_cube_translateˈ
CHAPITRE 8 Bonification du jeu par l’animation 361

Storyboard.TargetProperty=ˈ(TranslateTransform3D.OffsetX)ˈ From=ˈ0ˈ To=ˈ2.5ˈ


By=ˈ0.1ˈ Duration=ˈ00:00:05ˈ AutoReverse=ˈFalseˈ>
<DoubleAnimation.EasingFunction>
<SineEase EasingMode=ˈEaseInOutˈ></SineEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</Grid.Resources>
Le storyboard k_stb_backease effectue une animation réaliste selon une fonction
d’accélération de type BackEase. L’objet BackEase a sa propriété EasingMode que l’on
fixe à EaseInOut et sa propriété Amplitude que l’on fixe à 2 (propriété qui fixe la valeur
de l’amplitude de la rétractation).
<Grid.Resources>
<Storyboard x:Key=ˈk_stb_backeaseˈ>
<DoubleAnimation Storyboard.TargetName=ˈx_cube_translateˈ
Storyboard.TargetProperty=ˈ(TranslateTransform3D.OffsetX)ˈ From=ˈ0ˈ To=ˈ2.5ˈ
By=ˈ0.1ˈ Duration=ˈ00:00:05ˈ AutoReverse=ˈFalseˈ>
<DoubleAnimation.EasingFunction>
<BackEase EasingMode=ˈEaseInOutˈ Amplitude=ˈ2ˈ></BackEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</Grid.Resources>
Le storyboard k_stb_bounceease effectue une animation réaliste selon une fonction
d’accélération de type BounceEase. L’objet BounceEase a sa propriété EasingMode que
l’on fixe à EaseInOut, sa propriété Bounces que l’on fixe à 3 (propriété qui fixe la valeur
du nombre de rebonds) et sa propriété Bounciness que l’on fixe à 1 (degré d’élasticité
des rebonds).
<Grid.Resources>
<Storyboard x:Key=ˈk_stb_bounceeaseˈ>
<DoubleAnimation Storyboard.TargetName=ˈx_cube_translateˈ
Storyboard.TargetProperty=ˈ(TranslateTransform3D.OffsetX)ˈ From=ˈ0ˈ To=ˈ2.5ˈ
By=ˈ0.1ˈ Duration=ˈ00:00:05ˈ AutoReverse=ˈFalseˈ>
<DoubleAnimation.EasingFunction>
<BounceEase EasingMode=ˈEaseInOutˈ Bounces=ˈ3ˈ Bounciness=ˈ1ˈ></BounceEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</Grid.Resources>
Le storyboard k_stb_circleease effectue une animation réaliste selon une fonction
d’accélération de type CircleEase. L’objet CircleEase a sa propriété EasingMode que l’on
fixe à EaseInOut.
362 Programmez des jeux vidéo 3D avec C#5 et WPF
<Grid.Resources>
<Storyboard x:Key=ˈk_stb_circleeaseˈ>
<DoubleAnimation Storyboard.TargetName=ˈx_cube_translateˈ
Storyboard.TargetProperty=ˈ(TranslateTransform3D.OffsetX)ˈ From=ˈ0ˈ To=ˈ2.5ˈ
By=ˈ0.1ˈ Duration=ˈ00:00:05ˈ AutoReverse=ˈFalseˈ>
<DoubleAnimation.EasingFunction>
<CircleEase EasingMode=ˈEaseInOutˈ></CircleEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</Grid.Resources>
Le storyboard k_stb_cubicease effectue une animation réaliste selon une fonction
d’accélération de type CubicEase. L’objet CubicEase a sa propriété EasingMode que l’on
fixe à EaseInOut.
<Grid.Resources>
<Storyboard x:Key=ˈk_stb_cubiceaseˈ>
<DoubleAnimation Storyboard.TargetName=ˈx_cube_translateˈ
Storyboard.TargetProperty=ˈ(TranslateTransform3D.OffsetX)ˈ From=ˈ0ˈ To=ˈ2.5ˈ
By=ˈ0.1ˈ Duration=ˈ00:00:05ˈ AutoReverse=ˈFalseˈ>
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode=ˈEaseInOutˈ></CubicEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</Grid.Resources>
Le storyboard k_stb_electicease effectue une animation réaliste selon une fonction
d’accélération de type ElasticEase. L’objet ElasticEase a sa propriété EasingMode que
l’on fixe à EaseInOut, sa propriété Oscillations que l’on fixe à 3 (propriété qui fixe la
valeur du nombre d’oscillations) et sa propriété Springiness que l’on fixe à 3 (facteur de
décroissance des oscillations).
<Grid.Resources>
<Storyboard x:Key=ˈk_stb_elasticeaseˈ>
<DoubleAnimation Storyboard.TargetName=ˈx_cube_translateˈ
Storyboard.TargetProperty=ˈ(TranslateTransform3D.OffsetX)ˈ From=ˈ0ˈ To=ˈ2.5ˈ
By=ˈ0.1ˈ Duration=ˈ00:00:05ˈ AutoReverse=ˈFalseˈ>
<DoubleAnimation.EasingFunction>
Copyright 2013 Patrice REY

<ElasticEase EasingMode=ˈEaseInOutˈ Oscillations=ˈ3ˈ Springiness=ˈ3ˈ>


</ElasticEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</Grid.Resources>
Le storyboard k_stb_exponentease effectue une animation réaliste selon une fonction
d’accélération de type ExponentialEase. L’objet ExponentialEase a sa propriété
CHAPITRE 8 Bonification du jeu par l’animation 363

EasingMode que l’on fixe à EaseInOut et sa propriété Exponent que l’on fixe à 12
(propriété qui fixe la valeur de l’exposant).
<Grid.Resources>
<Storyboard x:Key=ˈk_stb_exponenteaseˈ>
<DoubleAnimation Storyboard.TargetName=ˈx_cube_translateˈ
Storyboard.TargetProperty=ˈ(TranslateTransform3D.OffsetX)ˈ From=ˈ0ˈ To=ˈ2.5ˈ
By=ˈ0.1ˈ Duration=ˈ00:00:05ˈ AutoReverse=ˈFalseˈ>
<DoubleAnimation.EasingFunction>
<ExponentialEase EasingMode=ˈEaseInOutˈ Exponent=ˈ12ˈ></ExponentialEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</Grid.Resources>
Le storyboard k_stb_powerease effectue une animation réaliste selon une fonction
d’accélération de type PowerEase. L’objet PowerEase a sa propriété EasingMode que
l’on fixe à EaseInOut et sa propriété Power que l’on fixe à 3 (propriété qui fixe la valeur
de l’exposant de la puissance).
<Grid.Resources>
<Storyboard x:Key=ˈk_stb_powereaseˈ>
<DoubleAnimation Storyboard.TargetName=ˈx_cube_translateˈ
Storyboard.TargetProperty=ˈ(TranslateTransform3D.OffsetX)ˈ From=ˈ0ˈ To=ˈ2.5ˈ
By=ˈ0.1ˈ Duration=ˈ00:00:05ˈ AutoReverse=ˈFalseˈ>
<DoubleAnimation.EasingFunction>
<PowerEase EasingMode=ˈEaseInOutˈ Power=ˈ3ˈ></PowerEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</Grid.Resources>
Le storyboard k_stb_quadraticease effectue une animation réaliste selon une fonction
d’accélération de type QuadraticEase. L’objet QuadraticEase a sa propriété EasingMode
que l’on fixe à EaseInOut.
<Grid.Resources>
<Storyboard x:Key=ˈk_stb_quadraticeaseˈ>
<DoubleAnimation Storyboard.TargetName=ˈx_cube_translateˈ
Storyboard.TargetProperty=ˈ(TranslateTransform3D.OffsetX)ˈ From=ˈ0ˈ To=ˈ2.5ˈ
By=ˈ0.1ˈ Duration=ˈ00:00:05ˈ AutoReverse=ˈFalseˈ>
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode=ˈEaseInOutˈ></QuadraticEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</Grid.Resources>
Le storyboard k_stb_quarticease effectue une animation réaliste selon une fonction
d’accélération de type QuarticEase. L’objet QuarticEase a sa propriété EasingMode que
364 Programmez des jeux vidéo 3D avec C#5 et WPF
l’on fixe à EaseInOut.
<Grid.Resources>
<Storyboard x:Key=ˈk_stb_quarticeaseˈ>
<DoubleAnimation Storyboard.TargetName=ˈx_cube_translateˈ
Storyboard.TargetProperty=ˈ(TranslateTransform3D.OffsetX)ˈ From=ˈ0ˈ To=ˈ2.5ˈ
By=ˈ0.1ˈ Duration=ˈ00:00:05ˈ AutoReverse=ˈFalseˈ>
<DoubleAnimation.EasingFunction>
<QuarticEase EasingMode=ˈEaseInOutˈ></QuarticEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</Grid.Resources>
Le storyboard k_stb_quinticease effectue une animation réaliste selon une fonction
d’accélération de type QuinticEase. L’objet QuinticEase a sa propriété EasingMode que
l’on fixe à EaseInOut.
<Grid.Resources>
<Storyboard x:Key=ˈk_stb_quinticeaseˈ>
<DoubleAnimation Storyboard.TargetName=ˈx_cube_translateˈ
Storyboard.TargetProperty=ˈ(TranslateTransform3D.OffsetX)ˈ From=ˈ0ˈ To=ˈ2.5ˈ
By=ˈ0.1ˈ Duration=ˈ00:00:05ˈ AutoReverse=ˈFalseˈ>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=ˈEaseInOutˈ></QuinticEase>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</Grid.Resources>
Le storyboard k_stb_fonction_palier effectue une animation réaliste selon une fonction
d’accélération personnalisée. On inscrit en XAML le code de l’animation.
<Grid.Resources>
<Storyboard x:Key=ˈk_stb_fonction_palierˈ>
<DoubleAnimation x:Name=ˈx_anim_palierˈ Storyboard.TargetName=ˈx_cube_translateˈ
Storyboard.TargetProperty=ˈ(TranslateTransform3D.OffsetX)ˈ From=ˈ0ˈ To=ˈ2.5ˈ
By=ˈ0.1ˈ Duration=ˈ00:00:05ˈ AutoReverse=ˈFalseˈ>
</DoubleAnimation>
</Storyboard>
</Grid.Resources>
Copyright 2013 Patrice REY

Nous allons créer une fonction d’accélération dite «par palier», qui aura comme
résultat de faire avancer le cube d’une certaine quantité durant un temps donné puis
d’arrêter son déplacement durant le temps suivant, et cela un certain nombre de fois.
La figure 8.24 visualise cette fonction d’accélération par palier, qui compte une suite
de 4 enchaînements déplacement/arrêt. Cette séquence de 4 enchaînements est
une séquence par défaut. Une modification de propriété permet d’augmenter ou de
diminuer la séquence.
CHAPITRE 8 Bonification du jeu par l’animation 365
FIGURE 8.24

Valeurs
= f(t) mode EaseIn

fonction d’accélération personnalisée


dite «par palier» dans son mode
EaseIN

Temps t

La démarche consiste à déclarer une fonction d’accélération FonctionPalier qui


hérite de la classe de base abstraite EasingFunctionBase. A l’intérieur de cette classe
FonctionPalier, deux méthodes doivent être redéfinies: la méthode CreateInstanceCore
et la méthode EaseInCore. La méthode EaseInCore a une implémentation qui consiste
à retourner une instance de la classe FonctionPalier.
public class FonctionPallier : EasingFunctionBase {
//redefinition
protected override Freezable CreateInstanceCore() {
return new FonctionPallier();
}
...
}//end class
La méthode CreateInstanceCore, quant à elle, contient la logique de construction
d’une fonction avec des paliers dans le mode EaseIn. Pour réaliser une fonction avec
des paliers, nous avons besoin de savoir le nombre de paliers que l’on souhaite, ainsi
que le temps d’arrêt voulu pour le palier. On définit une propriété de dépendance
NombrePalier, de type int, avec une valeur de 4 par défaut, pour stocker le nombre des
paliers.
public class FonctionPallier : EasingFunctionBase {
...
//propriete NombrePalier
private static readonly DependencyProperty NombrePalierPropriete =
DependencyProperty.Register(
«NombrePalier»,
typeof(int),
typeof(FonctionPallier),
new PropertyMetadata(4));
public int NombrePalier {
get { return (int)base.GetValue(NombrePalierPropriete); }
set { base.SetValue(NombrePalierPropriete, value); }
} ...}//end class
366 Programmez des jeux vidéo 3D avec C#5 et WPF
On définit une propriété de dépendance SeuilPalier, de type double, avec une valeur
de 0.25 par défaut, qui représente la durée d’un palier. On ajoute à ces deux propriétés
les accesseurs get et set.
public class FonctionPallier : EasingFunctionBase {
...
//propriete SeuilPalier (duree d’arret dans un palier)
private static readonly DependencyProperty SeuilPalierPropriete =
DependencyProperty.Register(
«SeuilPalier»,
typeof(double),
typeof(FonctionPallier),
new PropertyMetadata(0.25));
public double SeuilPalier {
get { return (double)base.GetValue(SeuilPalierPropriete); }
set { base.SetValue(SeuilPalierPropriete, value); }
}
...
}//end class
La signature de la méthode redéfinie EaseInCore est protected override double
EaseInCore(double NormalizedTime). Elle reçoit une variable NormalizedTime, de type
double, qui correspond au temps normalisé, et elle retourne une valeur, de type
double, correspondant à une valeur comprise entre 0 et 1 (c’est-à-dire f(t) pour un
temps t normalisé). A partir des propriétés NombrePalier et SeuilPalier, on peut établir
la correspondance entre t et f(t).
public class FonctionPallier : EasingFunctionBase {
...
protected override double EaseInCore(double NormalizedTime) {
double valeur_cible_normalise = 1.0;
for (int i = 0; i < NombrePalier; i++) {
double v_palier_depart = (double)i / NombrePalier;
double v_palier_longeur = 1.0 / NombrePalier;
double depart_du_palier = v_palier_depart + SeuilPalier * v_palier_longeur;
double v_palier_fin = v_palier_depart + v_palier_longeur;
if ((NormalizedTime >= v_palier_depart) &&
(NormalizedTime < depart_du_palier)) {
valeur_cible_normalise = v_palier_depart +
Copyright 2013 Patrice REY

(NormalizedTime - v_palier_depart) / SeuilPalier;


break;
}
if ((NormalizedTime >= depart_du_palier) && (NormalizedTime < v_palier_fin)) {
valeur_cible_normalise = v_palier_fin;
break;
}
}
return valeur_cible_normalise;
CHAPITRE 8 Bonification du jeu par l’animation 367

}
... } //end class
Maintenant que la fonction d’accélération FonctionPalier est implémentée, on instancie
un objet FonctionPalier nommé fonction_palier. On choisit son mode voulu par la
propriété EasingMode (que l’on fixe ici par exemple à EaseIn). La propriété NombrePalier
détermine le nombre des paliers, et la propriété SeuilPalier détermine la durée d’un
palier. Pour affecter cette fonction d’accélération fonction_palier à la piste d’animation
DoubleAnimation, on instancie une variable anim_palier, de type DoubleAnimation,
qui récupère la piste d’animation depuis le Storyboard k_stb_fonction_palier (cette
piste est le premier et unique enfant du storyboard par sa propriété Children qui est
une collection générique de Timeline). Et on affecte à sa propriété EasingFunction la
fonction d’accélération fonction_palier.
stb_anim = (Storyboard)x_grid_root.Resources[«k_stb_fonction_palier»];
FonctionPallier fonction_palier = new FonctionPallier();
fonction_palier.EasingMode = EasingMode.EaseIn;
fonction_palier.NombrePalier = 4;
fonction_palier.SeuilPalier = 0.25;
DoubleAnimation anim_palier = (DoubleAnimation)stb_anim.Children[0];
anim_palier.EasingFunction = fonction_palier;

5 - Simuler le roulement de la balle

Dans ce paragraphe, nous allons voir comment simuler l’effet de roulement de la


balle lors de son déplacement sur le plateau du jeu. Pour pouvoir apprécier le plus
complètement possible cet effet de roulement, nous allons créer une animation dans
un projet à part. Le projet se trouve dans le dossier 04_VS2012_3d_anim_realiste_2 du
code source de programmation.
Cette animation se compose d’un plateau de jeu (nommé x_sol et de type TSol) avec
sa dimension horizontale égale à sa dimension verticale, une balle texturée avec une
image (nommée x_balle et de type TSphere) et quatre cylindres (nommés x_cyl_bas_
droit , x_cyl_bas_gauche, x_cyl_haut_droit, x_cyl_haut_gauche, tous de type TCylindre)
positionnés aux quatre coins du plateau.
La figure 8.25 visualise le résultat obtenu de cette animation. Un bouton x_btn_jouer
lance l’exécution de l’animation et un bouton x_btn_stopper arrête l’animation en cours
(qui peut être relancée de nouveau par le bouton x_btn_jouer). Avec ces deux boutons,
on peut apprécier plus facilement la configuration de la balle à un moment donné,
dans la mesure où la balle est représentée par une sphère et est texturée par une
image graphique.
FIGURE 8.25
CHAPITRE 8 Bonification du jeu par l’animation 369

Une boucle de rendu image par image est ajoutée par la méthode statique
CompositionTarget.Rendering pour l’événement Rendering. La balle va rouler le long
des côtés du plateau, et cela parallèlement aux bords du plateau, en suivant le sens
contraire des aiguilles d’une montre. Quand la balle entre en collision avec un cylindre,
elle prend la direction du bord adjacent et elle suit de nouveau le bord adjacent
parallèlement. On introduit donc une énumération Direction dont les valeurs sont:
• Direction.bas_gauche_vers_bas_droit quand la balle se dirige dans la direction du
cylindre en bas à gauche vers le cylindre en bas à droite,
• Direction.bas_droit_vers_haut_droit quand la balle se dirige dans la direction du
cylindre en bas à droite vers le cylindre en haut à droite,
• Direction.haut_droit_vers_haut_gauche quand la balle se dirige dans la direction du
cylindre en haut à droite vers le cylindre en haut à gauche,
• Direction.haut_gauche_vers_bas_gauche quand la balle se dirige dans la direction du
cylindre en haut à gauche vers le cylindre en bas à gauche.
Au départ de l’animation, la balle se trouve à droite du cylindre x_cyl_bas_gauche. La
variable v_direction, qui stocke l’énumération de déplacement en cours, est initialisée
à Direction.bas_gauche_vers_bas_droit. Le gestionnaire de l’événement Click sur le
bouton x_btn_jouer consiste à démarrer l’horloge, à lancer le rendu image par image,
et à mettre à jour l’état de fonctionnalité des boutons. Le gestionnaire de l’événement
Click sur le bouton x_btn_stopper consiste à suspendre l’horloge, à suspendre le rendu
image par image, et à mettre à jour l’état de fonctionnalité des boutons.
//evenement clic sur btn jouer animation
private void x_btn_jouer_Click(object sender, RoutedEventArgs e) {
x_btn_jouer.IsEnabled = false;
x_btn_stopper.IsEnabled = true;
v_stopwatch.Start();
CompositionTarget.Rendering += CompositionTarget_Rendering;
}
//evenement clic sur btn stopper animation
private void x_btn_stopper_Click(object sender, RoutedEventArgs e) {
x_btn_jouer.IsEnabled = true;
x_btn_stopper.IsEnabled = false;
v_stopwatch.Stop();
CompositionTarget.Rendering -= CompositionTarget_Rendering;
}
La balle va se déplacer dans le plan OXZ avec une vitesse dont la composante suivant
l’axe X est v_vitesse_x et dont la composante suivant l’axe Z est v_vitesse_z (ce principe
a déjà été vu). Le taux estimé pour le déplacement est fourni par v_vitesse_taux = 0.01.
Les variables, v_deplac_x et v_deplac_z, sont affectées des valeurs à appliquer pour la
translation de la balle sur la scène. La méthode DeterminerVitesse permet, en fonction
370 Programmez des jeux vidéo 3D avec C#5 et WPF
de la variable de direction v_direction, de fixer les vitesses de déplacement de la balle
en fonction de l’endroit où la balle se trouve sur un bord à un moment donné.
//
private void DeterminerVitesse() {
switch (v_direction) {
case Direction.bas_gauche_vers_bas_droit:
v_vitesse_x = v_vitesse_taux;
v_vitesse_z = 0;
break;
case Direction.bas_droit_vers_haut_droit:
v_vitesse_x = 0;
v_vitesse_z = -v_vitesse_taux;
break;
case Direction.haut_droit_vers_haut_gauche:
v_vitesse_x = -v_vitesse_taux;
v_vitesse_z = 0;
break;
case Direction.haut_gauche_vers_bas_gauche:
v_vitesse_x = 0;
v_vitesse_z = v_vitesse_taux;
break;
}
}
De plus, pour simuler le roulement de la balle, il va falloir lui appliquer une rotation
de façon à ce qu’elle tourne autour d’un axe de rotation qui passe par le centre de la
sphère, avec un angle de rotation défini. La variable v_rot_angle sera l’angle de rotation
à effectuer pour la rotation, et cela en fonction d’un pas d’angle dont l’incrément est
v_rot_taux_angle défini à 5 degrés.
On ajoute donc une méthode TSphere.Appliquer qui devra effectuer le déplacement
complet de la balle (translation et rotation), et qui reçoit en paramètre:
• les valeurs suiv_x, suiv_y et suiv_z, de type double, qui indiquent les valeurs de
translation à effectuer,
• la valeur vecteur_axe, de type Vector3D, qui représente l’orientation de l’axe de
rotation à utiliser,
• la valeur angle, de type double, qui représente la valeur de l’angle de rotation.
Copyright 2013 Patrice REY

//transformation a effectuer rotation + translation


public void Appliquer(double suiv_x, double suiv_y, double suiv_z,
Vector3D vecteur_axe, double angle) {
...
...
}
Cette méthode TSphere.Appliquer est très importante sur son principe car elle
représente la simulation complète de l’effet de roulement de la balle. Elle se compose
CHAPITRE 8 Bonification du jeu par l’animation 371

des actions suivantes:


• la variable v_boite_englobante relève la boite englobante de la sphère par la
propriété Bounds appliquée à la propriété P_Modele (qui représente le Model3D),
• la variable v_point_gravite calcule la position du point, de type Point3D, situé au
centre de la boite englobante (point qui correspond au centre de la sphère),
• comme il faut effectuer plusieurs transformations, on instancie un
Transform3DGroup t3d et on lui ajoute, à sa propriété Children des transformations
avec un ordre précis, avec en premier une rotation RotateTransform3D pour
effectuer une rotation d’un angle angle autour d’un axe vecteur_axe, et en second
on lui ajoute une translation TranslateTransform3D en fonction des déplacements
suiv_x, suiv_y et suiv_z,
• on affecte le groupe de transformations t3d (rotation + translation) à la propriété
Transform de la sphère,
• et on effectue la mise à jour de la boite englobante v_boite_englobante.
A noter que l’ordre des transformations est très important: on effectue une rotation de
la sphère et on la translate. Si on inverse cet ordre, le résultat obtenu sera complètement
différent. En effet, à un moment donné, on calcule la boite englobante de la sphère.
Cela nous permet de déterminer le centre de la sphère, puis d’exécuter une rotation de
la sphère sur elle-même grâce à un axe de rotation qui passe par le centre de la sphère.
On lui ajoute ensuite une translation, ce qui donne bien un effet de roulement de la
sphère: une rotation suivie par une translation, et ainsi de suite.
//transformation a effectuer rotation + translation
public void Appliquer(double suiv_x, double suiv_y, double suiv_z,
Vector3D vecteur_axe, double angle) {
v_boite_englobante = this.P_Modele.Bounds;
v_point_gravite = CalculPointGraviteParBoite(v_boite_englobante);
Transform3DGroup t3d = new Transform3DGroup();
t3d.Children.Add(new RotateTransform3D(
new AxisAngleRotation3D(vecteur_axe, angle), v_point_gravite));
t3d.Children.Add(new TranslateTransform3D(suiv_x, suiv_y, suiv_z));
this.Transform = t3d;
Point3D pt3d = v_boite_englobante.Location;
pt3d.Offset(suiv_x, suiv_y, suiv_z);
v_boite_englobante.Location = pt3d;
}
La figure 8.26 schématise ce groupe de transformations qui est essentiel à cet effet de
roulement (rotation + translation). C’est un principe qui à priori semble être compliqué,
mais après réflexion, c’est un principe qui est loin d’être difficile dans la mesure où l’on
raisonne essentiellement avec l’application de transformations.
372 Programmez des jeux vidéo 3D avec C#5 et WPF
FIGURE 8.26
Y

Sc
X

La rotation s’effectue autour de l’axe de rotation d’orientation vecteur_axe et passant


par le centre de la sphère Sc, pour un angle de rotation donné. Ensuite la sphère est
translatée par un déplacement selon les axes X et Z.
//boucle de rendu image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
x_affiche_temps_texte.Text = v_stopwatch.Elapsed.Minutes.ToString(«00») + «:»
+ v_stopwatch.Elapsed.Seconds.ToString(«00»);
DeterminerVitesse();
v_deplac_x += v_vitesse_x;
v_deplac_z += v_vitesse_z;
v_rot_angle += v_rot_taux_angle;
Copyright 2013 Patrice REY

Vector3D vecteur_axe = new Vector3D();


switch (v_direction) {
case Direction.bas_gauche_vers_bas_droit:
vecteur_axe = new Vector3D(0, 0, -1);
break;
case Direction.bas_droit_vers_haut_droit:
vecteur_axe = new Vector3D(-1, 0, 0);
break;
case Direction.haut_droit_vers_haut_gauche:
CHAPITRE 8 Bonification du jeu par l’animation 373

vecteur_axe = new Vector3D(0, 0, 1);


break;
case Direction.haut_gauche_vers_bas_gauche:
vecteur_axe = new Vector3D(1, 0, 0);
break;
}
x_balle.Appliquer(v_deplac_x, 0, v_deplac_z, vecteur_axe, v_rot_angle);
...
}
Quand le déplacement (figure 8.27) s’effectue le long du bord en allant du cylindre
bas gauche vers le cylindre bas droit (Direction_bas_gauche_vers_bas_droit), le vecteur
représentant l’axe de rotation a pour direction l’axe Z négatif, en passant par le centre
de la sphère, et pour un angle de rotation positif.
FIGURE 8.27

vecteur_axe = Vector3D(0,0,-1)

v_rot_angle > 0
Direction.bas_gauche_vers_bas_droit

v_rot_angle > 0

Quand le déplacement (figure 8.28) s’effectue le long du bord en allant du cylindre


bas droit vers le cylindre haut droit (Direction_bas_droit_vers_haut_droit), le vecteur
représentant l’axe de rotation a pour direction l’axe X négatif, en passant par le centre
de la sphère, et pour un angle de rotation positif.
374 Programmez des jeux vidéo 3D avec C#5 et WPF

FIGURE 8.28
Direction.bas_droit_vers_haut_droit

1 2

v_rot_angle > 0

v_rot_angle > 0

vecteur_axe = Vector3D(-1,0,0)

Quand le déplacement (figure 8.29) s’effectue le long du bord en allant du cylindre haut
droit vers le cylindre haut gauche (Direction_haut_droit_vers_haut_gauche), le vecteur
représentant l’axe de rotation a pour direction l’axe Z positif, en passant par le centre
de la sphère, et pour un angle de rotation positif.

FIGURE
8.29 1 v_rot_angle > 0

Direction.haut_droit_vers_haut_gauche vecteur_axe = Vector3D(0,0,1)

Copyright 2013 Patrice REY

v_rot_angle > 0
CHAPITRE 8 Bonification du jeu par l’animation 375

Quand le déplacement (figure 8.30) s’effectue le long du bord en allant du cylindre


haut gauche vers le cylindre bas gauche (Direction_haut_gauche_vers_bas_gauche), le
vecteur représentant l’axe de rotation a pour direction l’axe X positif, en passant par le
centre de la sphère, et pour un angle de rotation positif.
FIGURE 8.30
Direction.haut_gauche_vers_bas_gauche

1 2

v_rot_angle > 0

vecteur_axe = Vector3D(1,0,0)
v_rot_angle > 0

Le changement de direction s’effectue lors d’une collision entre la sphère et le cylindre.


Une méthode TSphere.CollisionDetecter est ajoutée, prenant en paramètre un cylindre
TCylindre. Comme on se trouve dans une boucle de rendu image par image (avec une
cadence moyenne de 60 images par seconde), le risque est d’avoir plusieurs détections
de collisions relevées avec le même cylindre. Il faut donc faire en sorte que lorsque la
première collision avec un cylindre donné est détectée, cette même collision ne puisse
pas être de nouveau détectée dans le rendu d’une image suivante. On ajoute pour cela
une variable v_collision_detecter de type string et on lui affecte une chaîne (HD, HG, BD
et BG, identificateur de cylindre) pour indiquer avec quel cylindre il y a eu une collision.
Ainsi, une collision sera relevée une seule fois dans une série de rendus d’images si
les conditions sont remplies: la méthode CollisionDetecter retourne true pour un test
avec un cylindre donné, et la variable v_collision_detecter ne contient pas la chaîne
identifiant le cylindre pour lequel il y a eu le test.
Lors d’une collision détectée, la variable v_direction indique la nouvelle direction que
doit prendre la balle pour déterminer les vitesses adéquates, et la rotation d’angle
v_rot_angle est remise à zéro.
376 Programmez des jeux vidéo 3D avec C#5 et WPF
//boucle de rendu image
private void CompositionTarget_Rendering(object sender, EventArgs e) {
...
//colission avec cylindre bas droit
if (x_balle.CollisionDetecter(x_cyl_bas_droit) == true
&& v_collision_detecter!= «BD») {
//AfficherInfosCollision(x_balle, x_cyl_bas_droit, «BasDroit»);
v_collision_detecter = «BD»;
v_direction = Direction.bas_droit_vers_haut_droit;
v_rot_angle = 0;
}
//colission avec cylindre haut droit
if (x_balle.CollisionDetecter(x_cyl_haut_droit) == true
&& v_collision_detecter != «HD») {
//AfficherInfosCollision(x_balle, x_cyl_haut_droit, «HautDroit»);
v_collision_detecter = «HD»;
v_direction = Direction.haut_droit_vers_haut_gauche;
v_rot_angle = 0;
}
//colission avec cylindre haut gauche
if (x_balle.CollisionDetecter(x_cyl_haut_gauche) == true
&& v_collision_detecter != «HG») {
//AfficherInfosCollision(x_balle, x_cyl_haut_gauche, «HautGauche»);
v_collision_detecter = «HG»;
v_direction = Direction.haut_gauche_vers_bas_gauche;
v_rot_angle = 0;
}
//colission avec cylindre bas gauche
if (x_balle.CollisionDetecter(x_cyl_bas_gauche) == true
&& v_collision_detecter != «BG») {
//AfficherInfosCollision(x_balle, x_cyl_bas_gauche, «BasGauche»);
v_collision_detecter = «BG»;
v_direction = Direction.bas_gauche_vers_bas_droit;
v_rot_angle = 0;
}
...
}
Regardons maintenant l’implémentation pour la détermination d’une collision entre
une sphère et un cylindre. La figure 8.31 au repère 1 visualise une sphère (de rayon
Copyright 2013 Patrice REY

Rp) et un cylindre (de rayon Rc pour la face externe) dont les dimensions de la boite
englobante sont identiques. Dans ce cas précis, il y a collision (repère 2) si la distance D,
égale à la distance entre les points Sp et Sc, est inférieure ou égale à la somme Rp+Rc.
Si on prend un cylindre dont la hauteur de la boite englobante est bien supérieure
à la hauteur de la boite englobante de la sphère, la distance Rp+Rc constituera une
bonne appréciation de cette collision (cas de notre animation). Comme, pour nos deux
objets 3D, nous avons accès en permanence au calcul de la boite englobante par la
CHAPITRE 8 Bonification du jeu par l’animation 377

propriété TSphere.P_BoiteEnglobante et par la propriété TCylindre.P_BoiteEnglobante,


nous pouvons donc évaluer la distance entre les deux points de référence (Lp et Lc) des
deux boites englobantes (figure 8.31 repère 3), ou bien entre les deux points situés au
milieu des deux boites englobantes.
FIGURE 8.31

Rp : rayon Rc : rayon de la
Sp Rp Rc
de la face externe du
sphère Sc
cylindre

collision d’une sphère et


2 d’un cylindre dont les
boites englobantes sont
de mêmes dimensions

Sp Rp Rc
distance D = Rp + Rc
Sc

distance entre les deux points de


Lp Lc référence Lp et Lc
378 Programmez des jeux vidéo 3D avec C#5 et WPF
La méthode TSphere.CollisionDetecter exécute les actions suivantes:
• une détection des boites englobantes par la méthode IntersectsWith qui calcule
l’intersection entre deux structures Rect3D,
• si il y a bien une collision entre les boites englobantes, on calcule les points de
gravité (de type Point3D) des deux boites (pt_gravite_sph et pt_gravite_cyl),
• on définit la distance borne_distance comme étant la somme du rayon de la
sphère (propriété P_Rayon) et du rayon de la face externe du cylindre (propriété
P_RayonExterne),
• on définit le vecteur vect entre le point pt_gravite_sph et pt_gravite_cyl,
• on compare la distance borne_distance avec la longueur du vecteur vect (propriété
Vector3D.Length), et si borne_distance est supérieure ou égale à vect.Length alors il
y a une collision effective entre les deux volumes.
public class TSphere : UIElement3D {
...
//determiner la possibilité d’une collision avec un TCylindre
public bool CollisionDetecter(TCylindre obj_cylindre) {
bool collision = false;
Rect3D rec3d_obj_cylindre = obj_cylindre.P_BoiteEnglobante;
if (v_boite_englobante.IntersectsWith(rec3d_obj_cylindre) == true) {
Point3D pt_gravite_sph = CalculPointGraviteParBoite(this.v_boite_englobante);
Point3D pt_gravite_cyl =
CalculPointGraviteParBoite(obj_cylindre.P_BoiteEnglobante);
double borne_distance = this.P_Rayon + obj_cylindre.P_RayonExterne;
Vector3D vect = pt_gravite_cyl - pt_gravite_sph;
//vecteur orienté de sph vers cyl
if (vect.Length < borne_distance) {
collision = true;
}
}
return collision;
}
...
}

6 - Générer des surfaces de plateau


Copyright 2013 Patrice REY

Jusqu’à présent le plateau du jeu avait une forme rectangulaire plane. Nous allons
voir dans ce paragraphe comment modéliser et générer des surfaces de plateau qui
possèdent une élévation (c’est-à-dire une coordonnée y différente de 0 pour des
coordonnées x et z données). La figure 8.32 visualise le résultat obtenu pour une surface
(la surface numéro 5 parmi les 6 proposées plus celle par défaut). Ce projet à part se
trouve dans le dossier 04_VS2012_3d_anim_realiste_3 du code de programmation.
FIGURE 8.32
380 Programmez des jeux vidéo 3D avec C#5 et WPF
Le principe de la génération des surfaces avec élévation consiste à calculer, pour des
coordonnées x et z d’un point, la coordonnée y (qui représente l’élévation) en fonction
d’une équation dont les paramètres sont fonction de x et z. Par exemple, pour des
coordonnées x et z données, on aurait y = 3 * x2 + 2 * z comme équation.
On définit une classe TSolParametre qui hérite de UIElement3D. On lui ajoute les
propriétés P_Modele, P_DivisionX (nombre de division suivant l’axe X, de type int,
valeur par défaut 40), P_DivisionZ (nombre de division selon l’axe Z, de type int, valeur
par défaut 40), P_CouleurUnie (couleur appliquée en l’absence de texture, de type
Color, valeur par défaut Color.Aqua), P_Texture (texture à appliquer, de type Material,
valeur par défaut null), et P_SelecteurSurface (sélecteur de surface en fonction de
l’énumération ChoixSurface, valeur par défaut ChoixSurface.defaut).
public class TSolParametre : UIElement3D {
...
//donnees
public enum ChoixSurface { defaut, surface_1, surface_2, surface_3, surface_4,
surface_5, surface_6 };
//la propriete SelecteurSurface
private static readonly DependencyProperty ProprieteSelecteurSurface =
DependencyProperty.Register(«P_SelecteurSurface»,
typeof(ChoixSurface),
typeof(TSolParametre),
new PropertyMetadata(ChoixSurface.defaut, PM_SelecteurSurface));
private static void PM_SelecteurSurface(DependencyObject d,
DependencyPropertyChangedEventArgs e) {
TSolParametre objet = (TSolParametre)d;
objet.InvalidateModel();
}
public ChoixSurface P_SelecteurSurface {
get { return (ChoixSurface)GetValue(ProprieteSelecteurSurface); }
set { SetValue(ProprieteSelecteurSurface, value); }
}
...
}
La méthode redéfinie OnUpdateModel génère le maillage de l’objet TSolParametre. Si
la propriété P_Texture est différente de null, on applique la texture sur la face avant,
sinon on applique la couleur P_CouleurUnie. La face arrière est peinte en jaune. La
Copyright 2013 Patrice REY

méthode MaillageSurfaceParametree modélise le maillage de la surface proprement


dite.
//redefinition de la methode de rendu
protected override void OnUpdateModel() {
GeometryModel3D geo = new GeometryModel3D();
geo.Geometry = MaillageSurfaceParametree();
if (this.P_Texture == null) {
CHAPITRE 8 Bonification du jeu par l’animation 381

geo.Material = new DiffuseMaterial(new SolidColorBrush(this.P_CouleurUnie));


}
else {
geo.Material = this.P_Texture;
}
geo.BackMaterial = new DiffuseMaterial(new SolidColorBrush(Colors.Yellow));
P_Modele = geo;
}
Le maillage d’une surface avec élévation consiste à modéliser des facettes rectangulaires
composées de 4 points de type Point3D. Pour chaque point avec des coordonnées x et
z données, on calcule une élévation y en fonction d’une équation donnée.
On commence par établir un tableau Vector3D[,] pts = new Vector3D[P_DivisionX, P_
DivisionZ] pour stocker les sommets des facettes rectangulaires sous forme de vecteur
(figure 8.33). Deux boucles imbriquées permettent de délivrer la variable i en fonction
du nombre de division P_DivisionX, et la variable j en fonction du nombre de division
P_DivisionZ. La coordonnée x varie entre les bornes xmin et xmax par un incrément
égal à (xmax - xmin) / P_DivisionX. La coordonnée z varie entre les bornes zmin et zmax
par un incrément égal à (zmax - zmin) / P_DivisionZ.
On assigne à pts[i,j] donné l’élévation retournée par la méthode CalculerCoordonneePoint.
A pts[i,j] on ajoute un vecteur correspondant à l’origine (new Vector3D(0, 0, 0)) pour
ramener la valeur en fonction de l’origine. Enfin, par la méthode Normaliser, on ramène
la composante y calculée pour qu’elle se situe entre ymin et ymax.
FIGURE 8.33
Y
ymax

zmin

zmax

Z
ymin
xmin xmax

(i,j) ( i+1 , j )
facette avec ses 4 sommets
( i , j +1) ( i+1 , j+1 )
382 Programmez des jeux vidéo 3D avec C#5 et WPF
private MeshGeometry3D MaillageSurfaceParametree() {
MeshGeometry3D maillage = new MeshGeometry3D();
Vector3D[,] pts = new Vector3D[P_DivisionX, P_DivisionZ];
double xmin = 0, xmax = 0, zmin = 0, zmax = 0, ymin = 0, ymax = 0;
...
double dx = (xmax - xmin) / P_DivisionX;
double dz = (zmax - zmin) / P_DivisionZ;
for (int i = 0; i < P_DivisionX; i++) {
double x = xmin + i * dx;
for (int j = 0; j < P_DivisionZ; j++) {
double z = zmin + j * dz;
pts[i, j] = CalculCoordonneePoint(x, z);
pts[i, j] += new Vector3D(0, 0, 0);
pts[i, j] = Normaliser(pts[i, j], xmin, xmax, ymin, ymax, zmin, zmax);
}
}
...
}
private Vector3D CalculCoordonneePoint(double x, double z) {
Vector3D point3d = new Vector3D(0, 0, 0);
switch (P_SelecteurSurface) {
case ChoixSurface.defaut:
point3d.X = x;
point3d.Y = 0;//elevation nulle par défaut
point3d.Z = z;
break;
}
return point3d;
}
private Vector3D Normaliser(Vector3D v3d, double xmin, double xmax, double ymin,
double ymax, double zmin, double zmax) {
v3d.X = -1 + 2 * (v3d.X - xmin) / (xmax - xmin);
v3d.Y = -1 + 2 * (v3d.Y - ymin) / (ymax - ymin);
v3d.Z = -1 + 2 * (v3d.Z - zmin) / (zmax - zmin);
return v3d;
}
Il faut procéder maintenant à la réalisation des facettes rectangulaires. A partir du
tableau pts, avec deux boucles imbriquées qui délivre les variables i et j en fonction
Copyright 2013 Patrice REY

du nombre de division, on établit les coordonnées des 4 sommets pour une facette:
pt_a = pts[i,j], pt_b = pts[i+1,j], pt_c = pts[i+1,j+1] et pt_d = pts[i,j+1]. Pour établir une
face, on passe les points à la propriété Positions de MeshGeometry3D dans l’ordre
pt_c, pt_b, pt_a et pt_d (sens contraire des aiguilles d’une montre pour indiquer la face
vue) comme le montre la figure 8.34. Et on passe à la propriété TextureCoordinates de
MeshGeometry3D les coordonnées de texture à appliquer en fonction d’une image
graphique 2D (on s’aide d’un compteur cpt_div_h pour une division horizontale et d’un
CHAPITRE 8 Bonification du jeu par l’animation 383

compteur cpt_div_v pour une division verticale).


FIGURE 8.34

pt_a = ( i , j ) pt_b = ( i+1 , j )


facette avec
ses 4
sommets
+

pt_d = ( i , j +1) pt_c = ( i+1 , j+1 )

private MeshGeometry3D MaillageSurfaceParametree() {


MeshGeometry3D maillage = new MeshGeometry3D();
Vector3D[,] pts = new Vector3D[P_DivisionX, P_DivisionZ];
double xmin = 0, xmax = 0, zmin = 0, zmax = 0, ymin = 0, ymax = 0;
...
int cpt_div_v = 0;
int cpt_div_h = 0;
for (int i = 0; i < P_DivisionX - 1; i++) {
for (int j = 0; j < P_DivisionZ - 1; j++) {
Point3D pt_a = (Point3D)pts[i, j];
Point3D pt_b = (Point3D)pts[i + 1, j];
Point3D pt_c = (Point3D)pts[i + 1, j + 1];
Point3D pt_d = (Point3D)pts[i, j + 1];
maillage.Positions.Add(pt_c);
maillage.TextureCoordinates.Add(new Point((double)(cpt_div_h + 1)
/ (double)P_DivisionX, (double)(cpt_div_v + 1) / (double)P_DivisionZ));
maillage.Positions.Add(pt_b);
maillage.TextureCoordinates.Add(new Point((double)(cpt_div_h + 1)
/ (double)P_DivisionX, (double)cpt_div_v / (double)P_DivisionZ));
maillage.Positions.Add(pt_a);
maillage.TextureCoordinates.Add(new Point((double)cpt_div_h
/ (double)P_DivisionX, (double)cpt_div_v / (double)P_DivisionZ));
maillage.Positions.Add(pt_d);
maillage.TextureCoordinates.Add(new Point((double)cpt_div_h
/ (double)P_DivisionX, (double)(cpt_div_v + 1) / (double)P_DivisionZ));
cpt_div_v++;
}
cpt_div_v = 0;
cpt_div_h++;
}
GenererTriangleIndice(maillage);
return maillage;
}
L’énumération TSolParametre.ChoixSurface permet de choisir une surface prédéfinie
pour laquelle on fixe les bornes minimales et maximales (xmin, xmax, zmin, zmax, ymin,
384 Programmez des jeux vidéo 3D avec C#5 et WPF
ymax), ainsi que le calcul de l’élévation en fonction d’une équation. Ici on a comme
surface prédéfinie par défaut la surface ChoixSurface.defaut qui représente une surface
plane avec xmin=-1, xmax=+1, zmin=-1, zmax=+1, ymin=-1, ymax=+1 et l’équation
y=0.
La surface ChoixSurface.surface_1 représente une surface avec xmin=-0.75, xmax=+0.75,
zmin=-0.5, zmax=+0.5, ymin=-5, ymax=+5 et l’équation y = 3x2 + 7z2 (figure 8.35 repère
1). La surface ChoixSurface.surface_2 représente une surface avec xmin=-1, xmax=+1,
zmin=-1, zmax=+1, ymin=-5, ymax=+5 et l’équation y = 3x2 (figure 8.35 repère 2).
FIGURE 8.35

Copyright 2013 Patrice REY

La surface ChoixSurface.surface_3 représente une surface avec xmin=-1, xmax=+1,


zmin=-1, zmax=+1, ymin=-8, ymax=+8 et l’équation y = 2x2 - 5z2 (figure 8.36 repère
1). La surface ChoixSurface.surface_4 représente une surface avec xmin=-π, xmax=+π,
zmin=-2π, zmax=+2π, ymin=-5, ymax=+5 et l’équation y = 1.5 * sin(x) * cos(z) (figure
CHAPITRE 8 Bonification du jeu par l’animation 385

8.36 repère 2). La surface ChoixSurface.surface_5 représente une surface avec xmin=-2,
xmax=+2, zmin=-2, zmax=+2, ymin=-5, ymax=+5 et l’équation y = cos(x2 + z2) * sin(x)
(figure 8.36 repère 3). La surface ChoixSurface.surface_6 représente une surface avec
xmin=-10, xmax=+10, zmin=-10, zmax=+10, ymin=-0.85, ymax=+0.85 (figure 8.36
repère 4).
FIGURE 8.36

1 2

3 4

7 - Importer des objets 3D au format Wavefront OBJ

Les logiciels de modélisation 3D permettent l’exportation d’objet 3D selon différents


formats. Ces formats peuvent d’être de type propriétaire ou de type généraliste. Un
format standard ouvert très connu est le format Wavefront OBJ. Nous allons réaliser
une application toute simple qui consiste à importer dans la scène un maillage 3D au
format OBJ et à lui appliquer une texture. Ce maillage est au préalable réalisé dans le
386 Programmez des jeux vidéo 3D avec C#5 et WPF
logiciel MAYA (dans sa version 2013), puis il est exporté au format OBJ. Le but de cette
simple application est de vous montrer la façon de faire pour lire un fichier au format
OBJ, pour récupérer les données 3D, et pour visualiser l’objet sur la scène. Ce projet se
trouve dans le dossier 04_VS2012_3d_import_obj du code de programmation.

7.1 - Exporter depuis MAYA au format OBJ

Dans le logiciel MAYA, en version 2013, on modélise un simple maillage représenté par
un cube (figure 8.37). La base du cube a une dimension de 2 unités suivant l’axe X et
l’axe Z. La hauteur du cube a une dimension de 1 unité suivant l’axe Y.
FIGURE 8.37

Pour exporter au format OBJ dans MAYA, il faut activer le plug-in d’exportation. Dans
le menu Windows, puis Setting preferences, il faut sélectionner la rubrique Plug-in
Copyright 2013 Patrice REY

manager (figure 8.38 repère 1). Une fenêtre s’ouvre, intitulée Plug-in manager. Une
liste des formats d’importation et d’exportation s’affiche. A la ligne où est inscrit le
nom du format objExport.mll, il faut cocher les cases Loaded et Auto Load (figure 8.38
repère 2). Après avoir refermé la fenêtre, sélectionnez le cube en cliquant sur une de
ses arêtes et choisissez dans le menu File la rubrique Export selection. Dans la fenêtre
d’exportation qui s’ouvre, donnez un nom comme export_cube, sélectionnez dans la
CHAPITRE 8 Bonification du jeu par l’animation 387

liste déroulante le format d’exportation obj wavefront, et choisissez un emplacement


sur le disque pour stocker les fichiers exportés. Un clic sur le bouton Save et le fichier
est alors généré.
FIGURE 8.38

Le logiciel MAYA exporte deux fichiers export_cube.obj et export_cube.mtl


(figure 8.39 repère 1). Copiez ces deux fichiers dans le dossier de l’exécutable
ImportObjet3dFormatObj-exe (figure 8.39 repère 2).
FIGURE 8.39

1
2

7.2 - Le format Wavefront OBJ

Le format OBJ est un format de fichier contenant la description d’une géométrie 3D. Il
a été défini par la société Wavefront Technologies dans le cadre du développement de
son logiciel d’animation Advanced Visualizer. Ce format de fichier est ouvert et a été
adopté par d’autres logiciels 3D (tels que 3D Turbo, Poser, Maya, Blender, MeshLab, 3D
Studio Max, Lightwave, etc) pour des traitements d’importation et d’exportation de
données 3D. Les formes géométriques peuvent être définies par des polygones ou des
surfaces lisses telles que des surfaces rationnelles et non rationnelles.
388 Programmez des jeux vidéo 3D avec C#5 et WPF
Les fichiers OBJ sont au format ASCII (une version binaire existe, identifiée par
l’extension MOD). Un commentaire peut être placé en faisant commencer la ligne
par le caractère #. Une surface polygonale est décrite par un ensemble de sommets
(accompagné de coordonnées de texture et de normales en chaque sommet) et d’un
ensemble de faces. Un sommet est défini de la manière suivante: v 1.023 0.056 0.045
avec l’indicateur v (comme vertex) indiquant qu’il s’agit d’un sommet, puis une série
de 3 nombres avec une précision de chiffres après la virgule. Une coordonnée de
texture est définie de la manière suivante: vt 1.02 0.04 avec l’indicateur vt et une
série de 2 nombres. Une normale est définie de la manière suivante: vn 0.0 1.0 0.0
avec l’indicateur vn et une série de 3 nombres. Chaque face est ensuite définie par un
ensemble d’indices faisant référence aux coordonnées des points, de texture et des
normales définies précédemment. Par exemple, pour la face suivante f v1/vt1/vn1 v2/
vt2/vn2 v3/vt3/vn3, f est l’indicateur d’une face (pouvant être avec 3 sommets pour
un triangle ou bien 4 sommets pour deux triangles collés), et cette face est définie
un triangle constitué des sommets d’indices v1, v2 et v3 dans la liste des sommets v.
Chacun de ces sommets possède une coordonnée de texture identifiée par son indice
dans la liste des coordonnées de texture vt et une normale identifiée dans la liste des
normales vn.
Lorsque plusieurs objets cohabitent dans le même fichier, la section définissant l’objet
est définie par o [nom de l’objet]. Lorsque plusieurs groupes de faces cohabitent dans
le même objet, la section définissant chaque groupe est définie par g [nom du groupe].
Des matériaux peuvent être référencés en important des fichiers .mtl (Material Template
Library) représentés par la séquence usemtl [nom de matériau].
Par exemple, si on ouvre dans un éditeur de texte notre fichier export_cube.obj, on
obtient le contenu ci-dessous:
# This file uses centimeters as units for non-parametric coordinates.

mtllib export_cube.mtl
g default
v -0.986112 -0.000000 0.971138
v 0.985169 -0.000000 0.971138
v -0.986112 1.051467 0.971138
Copyright 2013 Patrice REY

v 0.985169 1.051467 0.971138


v -0.986112 1.051467 -1.000142
v 0.985169 1.051467 -1.000142
v -0.986112 -0.000000 -1.000142
v 0.985169 -0.000000 -1.000142
vt 0.375000 0.000000
vt 0.625000 0.000000
vt 0.375000 0.250000
vt 0.625000 0.250000
CHAPITRE 8 Bonification du jeu par l’animation 389

vt 0.375000 0.500000
vt 0.625000 0.500000
vt 0.375000 0.750000
vt 0.625000 0.750000
vt 0.375000 1.000000
vt 0.625000 1.000000
vt 0.875000 0.000000
vt 0.875000 0.250000
vt 0.125000 0.000000
vt 0.125000 0.250000
vn 0.000000 0.000000 1.000000
vn 0.000000 0.000000 1.000000
vn 0.000000 0.000000 1.000000
vn 0.000000 0.000000 1.000000
vn 0.000000 1.000000 0.000000
vn 0.000000 1.000000 0.000000
vn 0.000000 1.000000 0.000000
vn 0.000000 1.000000 0.000000
vn 0.000000 0.000000 -1.000000
vn 0.000000 0.000000 -1.000000
vn 0.000000 0.000000 -1.000000
vn 0.000000 0.000000 -1.000000
vn 0.000000 -1.000000 0.000000
vn 0.000000 -1.000000 0.000000
vn 0.000000 -1.000000 0.000000
vn 0.000000 -1.000000 0.000000
vn 1.000000 0.000000 0.000000
vn 1.000000 0.000000 0.000000
vn 1.000000 0.000000 0.000000
vn 1.000000 0.000000 0.000000
vn -1.000000 0.000000 0.000000
vn -1.000000 0.000000 0.000000
vn -1.000000 0.000000 0.000000
vn -1.000000 0.000000 0.000000
s off
g pCube1
usemtl initialShadingGroup
f 1/1/1 2/2/2 4/4/3 3/3/4
f 3/3/5 4/4/6 6/6/7 5/5/8
f 5/5/9 6/6/10 8/8/11 7/7/12
f 7/7/13 8/8/14 2/10/15 1/9/16
f 2/2/17 8/11/18 6/12/19 4/4/20
f 7/13/21 1/1/22 3/3/23 5/14/24
On voit bien que le fichier contient 8 indicateurs de points v, et un ensemble d’indicateurs
de texture vt et de normales vn. De plus il y a 6 faces d’indicateur f, composées de 4
points (2 triangles collés). A noter que l’ordre des points pour les faces est celui du sens
contraire des aiguilles d’une montre.
390 Programmez des jeux vidéo 3D avec C#5 et WPF

7.3 - Importation au format OBJ

Maintenant nous allons procéder à l’importation du maillage d’un cube qui a été
exporté depuis MAYA au format OBJ. Une fois le maillage importé, nous appliquerons
notre texture au maillage (une texture représentant du bois). La figure 8.40 montre le
résultat obtenu après importation.
Sur la grille principale x_grid_root contenant la scène 3D x_viewport, nous positionnons
une grille x_grid_infos qui va afficher la lecture des données d’importation (figure 8.41)
avant de modéliser un objet 3D à partir de ce maillage d’importation. Tant qu’à la
scène, elle est composée d’un plateau TSol x_sol, texturé avec une image, et un repère
3D TRepere x_repere pour pouvoir visualiser plus facilement l’implantation du cube
après importation.
FIGURE 8.41

Copyright 2013 Patrice REY

<ContainerUIElement3D>
<!-- repere -->
<modele3d:TRepere x:Name=ˈx_repereˈ P_LongueurDemiAxe=ˈ4ˈ></modele3d:TRepere>
<!-- sol -->
<modele3d:TSol x:Name=ˈx_solˈ P_LargeurXNeg=ˈ3ˈ P_LargeurXPos=ˈ3ˈ
P_ProfondeurZPos=ˈ3ˈ P_ProfondeurZNeg=ˈ3ˈ>
...
</modele3d:TSol>
</ContainerUIElement3D>
FIGURE 8.40
392 Programmez des jeux vidéo 3D avec C#5 et WPF
La classe TFormatObj déclarée va servir à récupérer un fichier ASCII contenant des
données au format OBJ, et permettre de traiter ces données pour créer un maillage.
On ajoute un constructeur avec paramètre qui reçoit le chemin absolu pointant sur le
fichier ASCII à traiter. Pour cela, on utilise un objet StreamReader qui charge le fichier
avec un encodage ASCII par l’énumération Encoding.ASCII. La méthode ReadToEnd
permet de lire le contenu texte en entier, et à l’affecter à la variable v_contenu_texte
accessible par la propriété P_ContenuTexte. Un bloc try..catch permet de lever une
erreur en cas de problème d’ouverture de fichier.
public class TFormatObj {
//constructeur
public TFormatObj(string chemin_fichier) {
try {
using (StreamReader sr = new StreamReader(chemin_fichier, Encoding.ASCII)) {
v_contenu_texte = sr.ReadToEnd();
}
}
catch (Exception e) {
MessageBox.Show(«erreur: « + e.Message);
}
}
...
}
Avec la méthode TraiterLeFichier, on découpe le contenu texte ligne par ligne en
détectant le caractère de retour chariot (\u000A) de fin de ligne. On ajoute à une liste
List<string> v_liste_ligne toutes les lignes de texte contenues et non vides. La méthode
Trim permet de supprimer l’espace présent en début de ligne et en fin de ligne. Cette
liste est accessible par la propriété P_ListeLigne.
public class TFormatObj {
...
//traiter le fichier
public void TraiterLeFichier() {
string[] lignes = v_contenu_texte.Split(new char[] { ‘\u000A’ });
v_liste_ligne = new List<string>();
for (int xx = 0; xx < lignes.Length; xx++) {
if (lignes[xx].Length != 0) {
Copyright 2013 Patrice REY

v_liste_ligne.Add(lignes[xx].Trim());
}
}
}
...
}
La méthode ExtraireSommets permet de charger la liste des sommets, de type Point3D,
présents dans le fichier. Une boucle for permet de lire chaque ligne contenue dans la
CHAPITRE 8 Bonification du jeu par l’animation 393

liste. Une ligne est ensuite découpée en mots en utilisant le caractère séparateur espace
(‘ ‘). Si le premier mot contient la chaîne unique v, cela veut dire qu’on est en présence
d’un sommet. Donc les trois chaînes suivantes sont les représentations numériques
des coordonnées x, y et z du Point3D. A noter l’utilisation de la méthode Replace pour
remplacer le point décimal par une virgule décimale avant l’utilisation de la méthode
double.Parse, qui instancie une valeur double à partir d’une chaîne textuelle. Tous les
points Point3D générés sont ajoutés à une collection générique Point3DCollection v_
collect_p3d. Cette collection est accessible par la propriété P_CollectionPoint3D.
public class TFormatObj {
...
//recuperer les sommets Point3D
public void ExtraireSommets() {
v_collect_p3d = new Point3DCollection();
for (int xx = 0; xx < v_liste_ligne.Count; xx++) {
string[] decoupe = v_liste_ligne[xx].Split(new char[] { ‘ ‘ });
//sommets
if (decoupe[0] == «v») {
//on remplace le point decimal par la virgule decimale
string s_res_x = decoupe[1].Replace(‘.’, ‘,’);
string s_res_y = decoupe[2].Replace(‘.’, ‘,’);
string s_res_z = decoupe[3].Replace(‘.’, ‘,’);
Point3D p3d = new Point3D();
p3d.X = double.Parse(s_res_x);
p3d.Y = double.Parse(s_res_y);
p3d.Z = double.Parse(s_res_z);
v_collect_p3d.Add(p3d);
}
}
}
...
}
La méthode ExtraireCoordonneeTexture permet de charger la liste des coordonnées de
texture, de type Point. L’indicateur de coordonnées 2D de texture est vt. On procède
de même par un changement du point décimal en virgule décimale, puis en instanciant
des valeurs affectées aux propriétés X et Y d’un objet Point par la méthode double.
Parse appliquée à une chaîne textuelle.
Tous les points générés, de type Point, sont ajoutés à une collection générique
PointCollection v_collect_pd. Cette collection est accessible par la propriété P_
CollectionPointTexture.
public class TFormatObj {
...
//recuperer les coordonnées de texture Point
public void ExtraireCoordonneeTexture() {
394 Programmez des jeux vidéo 3D avec C#5 et WPF
v_collect_pd = new PointCollection();
for (int xx = 0; xx < v_liste_ligne.Count; xx++) {
string[] decoupe = v_liste_ligne[xx].Split(new char[] { ‘ ‘ });
//sommets
if (decoupe[0] == «vt») {
//on remplace le point decimal par la virgule decimale
string s_res_x = decoupe[1].Replace(‘.’, ‘,’);
string s_res_y = decoupe[2].Replace(‘.’, ‘,’);
Point pt = new Point();
pt.X = double.Parse(s_res_x);
pt.Y = double.Parse(s_res_y);
v_collect_pd.Add(pt);
}
}
}
...
}
La méthode ExtraireNormales permet de charger la liste des coordonnées des normales
aux sommets, de type Vector3D. L’indicateur des normales est vn. On procède de
même par un changement du point décimal en virgule décimale, puis en instanciant
des valeurs affectées aux propriétés X, Y et Z d’un objet Vector3D par la méthode
double.Parse appliquée à une chaîne textuelle.
Tous les vecteurs générés, de type Vector3D, sont ajoutés à une collection générique
Vector3DCollection v_collect_normale. Cette collection est accessible par la propriété
P_CollectionNormale.
public class TFormatObj {
...
//recuperer les normales Vector3D
public void ExtraireNormales() {
v_collect_normale = new Vector3DCollection();
for (int xx = 0; xx < v_liste_ligne.Count; xx++) {
string[] decoupe = v_liste_ligne[xx].Split(new char[] { ‘ ‘ });
//sommets
if (decoupe[0] == «vn») {
//on remplace le point decimal par la virgule decimale
string s_res_x = decoupe[1].Replace(‘.’, ‘,’);
string s_res_y = decoupe[2].Replace(‘.’, ‘,’);
Copyright 2013 Patrice REY

string s_res_z = decoupe[3].Replace(‘.’, ‘,’);


Vector3D v3d = new Vector3D();
v3d.X = double.Parse(s_res_x);
v3d.Y = double.Parse(s_res_y);
v3d.Z = double.Parse(s_res_z);
v_collect_normale.Add(v3d);
}
}
} ...}
CHAPITRE 8 Bonification du jeu par l’animation 395

La méthode ConstruireMaillage3D a pour but de modéliser le maillage v_maillage_3d de


type MeshGeometry. Le maillage obtenu est accessible par la propriété P_Maillage3D.
L’indicateur de face est représenté par la chaîne textuelle f. Si cet indicateur est suivi
de quatre ensembles de type v/vt/vn, cela veut dire qu’il faut modéliser une face
rectangulaire composée de deux triangles accolés. L’ordre des sommets est celui du sens
contraire des aiguilles d’une montre. Il suffit donc de récupérer le sommet correspondant
dans la liste v_collect_p3d et de l’ajouter à la propriété Positions de MeshGeometry.
Puis il faut récupérer les coordonnées de texture du sommet correspondant dans la
liste v_collect_pd et de l’ajouter à la propriété TextureCoordinates de MeshGeometry.
Enfin il faut récupérer le vecteur de la normale au sommet correspondant dans la liste
v_collect_normale et de l’ajouter à la propriété Normals de MeshGeometry.
public class TFormatObj {
...
//construire le maillage MeshGeometry3D
public void ConstruireMaillage3D() {
v_maillage_3d = new MeshGeometry3D();
for (int xx = 0; xx < v_liste_ligne.Count; xx++) {
string[] decoupe = v_liste_ligne[xx].Split(new char[] { ‘ ‘ });
if (decoupe[0] == «f» && decoupe.Length == 5) {
//face avec 4 sommets du type: f 1/21/1 2/22/2 22/43/3 21/42/4
string[] s_sommet_1 = decoupe[1].Split(new char[] { ‘/’ });
string[] s_sommet_2 = decoupe[2].Split(new char[] { ‘/’ });
string[] s_sommet_3 = decoupe[3].Split(new char[] { ‘/’ });
string[] s_sommet_4 = decoupe[4].Split(new char[] { ‘/’ });
Point3D pt3d_sommet_1 = v_collect_p3d[int.Parse(s_sommet_1[0]) - 1];
Point3D pt3d_sommet_2 = v_collect_p3d[int.Parse(s_sommet_2[0]) - 1];
Point3D pt3d_sommet_3 = v_collect_p3d[int.Parse(s_sommet_3[0]) - 1];
Point3D pt3d_sommet_4 = v_collect_p3d[int.Parse(s_sommet_4[0]) - 1];
v_maillage_3d.Positions.Add(pt3d_sommet_1);
v_maillage_3d.Positions.Add(pt3d_sommet_2);
v_maillage_3d.Positions.Add(pt3d_sommet_3);
v_maillage_3d.Positions.Add(pt3d_sommet_4);
Point pt_texture_1 = v_collect_pd[int.Parse(s_sommet_1[1]) - 1];
Point pt_texture_2 = v_collect_pd[int.Parse(s_sommet_2[1]) - 1];
Point pt_texture_3 = v_collect_pd[int.Parse(s_sommet_3[1]) - 1];
Point pt_texture_4 = v_collect_pd[int.Parse(s_sommet_4[1]) - 1];
v_maillage_3d.TextureCoordinates.Add(pt_texture_1);
v_maillage_3d.TextureCoordinates.Add(pt_texture_2);
v_maillage_3d.TextureCoordinates.Add(pt_texture_3);
v_maillage_3d.TextureCoordinates.Add(pt_texture_4);
Vector3D vect_1 = v_collect_normale[int.Parse(s_sommet_1[2]) - 1];
Vector3D vect_2 = v_collect_normale[int.Parse(s_sommet_2[2]) - 1];
Vector3D vect_3 = v_collect_normale[int.Parse(s_sommet_3[2]) - 1];
Vector3D vect_4 = v_collect_normale[int.Parse(s_sommet_4[2]) - 1];
v_maillage_3d.Normals.Add(vect_1);
396 Programmez des jeux vidéo 3D avec C#5 et WPF
v_maillage_3d.Normals.Add(vect_2);
v_maillage_3d.Normals.Add(vect_3);
v_maillage_3d.Normals.Add(vect_4);
}
...
}
Si l’indicateur est suivi de trois ensembles de type v/vt/vn, cela veut dire qu’il faut
modéliser une face triangulaire. On procède de la même façon.
public class TFormatObj {
...
if (decoupe[0] == «f» && decoupe.Length == 4) {
//face avec 4 sommets du type: f 1/21/1 2/22/2 22/43/3
string[] s_sommet_1 = decoupe[1].Split(new char[] { ‘/’ });
string[] s_sommet_2 = decoupe[2].Split(new char[] { ‘/’ });
string[] s_sommet_3 = decoupe[3].Split(new char[] { ‘/’ });
Point3D pt3d_sommet_1 = v_collect_p3d[int.Parse(s_sommet_1[0]) - 1];
Point3D pt3d_sommet_2 = v_collect_p3d[int.Parse(s_sommet_2[0]) - 1];
Point3D pt3d_sommet_3 = v_collect_p3d[int.Parse(s_sommet_3[0]) - 1];
v_maillage_3d.Positions.Add(pt3d_sommet_1);
v_maillage_3d.Positions.Add(pt3d_sommet_2);
v_maillage_3d.Positions.Add(pt3d_sommet_3);
Point pt_texture_1 = v_collect_pd[int.Parse(s_sommet_1[1]) - 1];
Point pt_texture_2 = v_collect_pd[int.Parse(s_sommet_2[1]) - 1];
Point pt_texture_3 = v_collect_pd[int.Parse(s_sommet_3[1]) - 1];
v_maillage_3d.TextureCoordinates.Add(pt_texture_1);
v_maillage_3d.TextureCoordinates.Add(pt_texture_2);
v_maillage_3d.TextureCoordinates.Add(pt_texture_3);
Vector3D vect_1 = v_collect_normale[int.Parse(s_sommet_1[2]) - 1];
Vector3D vect_2 = v_collect_normale[int.Parse(s_sommet_2[2]) - 1];
Vector3D vect_3 = v_collect_normale[int.Parse(s_sommet_3[2]) - 1];
v_maillage_3d.Normals.Add(vect_1);
v_maillage_3d.Normals.Add(vect_2);
v_maillage_3d.Normals.Add(vect_3);
}
GenererTriangleIndice(v_maillage_3d);
}
Avec la méthode GenererTriangleIndice on génère les indices pour le repérage des
triangles dans la liste MeshGeometry3D.TriangleIndices.
Copyright 2013 Patrice REY

public class TFormatObj {


...
//generer les indices pour la suite de triangles
private void GenererTriangleIndice(MeshGeometry3D maillage) {
int depart = 0;
for (int xx = 0; xx < maillage.Positions.Count; xx += 4) {
maillage.TriangleIndices.Add(depart);
maillage.TriangleIndices.Add(depart + 1);
CHAPITRE 8 Bonification du jeu par l’animation 397

maillage.TriangleIndices.Add(depart + 2);
maillage.TriangleIndices.Add(depart + 2);
maillage.TriangleIndices.Add(depart + 3);
maillage.TriangleIndices.Add(depart);
depart += 4;
}
}
...
}
Pour ajouter le modèle qui représente le maillage importé, il suffit de créer une classe
TFormatObjModele qui hérite de UIElement3D. On ajoute un constructeur avec
paramètre qui reçoit un maillage MeshGeometry.
public class TFormatObjModele : UIElement3D {
//
private MeshGeometry3D v_maillage_3d = null;
//constructeur
public TFormatObjModele(MeshGeometry3D maillage_3d) {
v_maillage_3d = maillage_3d;
}
...
}
La propriété P_CouleurUnie fixe une couleur unie à appliquer au modèle en l’absence
d’une texture (par défaut). La propriété P_Texture fixe une texture à appliquer au
modèle dès qu’elle est présente. Dans la méthode héritée redéfinie, le maillage est
affecté à la propriété Geometry de l’objet GeometryModel3D, qui lui-même est affecté
à la propriété TFormatObjModele.Visual3DModel.
public class TFormatObjModele : UIElement3D {
...
//redefinition de la methode de rendu
protected override void OnUpdateModel() {
GeometryModel3D geo = new GeometryModel3D();
geo.Geometry = v_maillage_3d;
if (this.P_Texture == null) {
geo.Material = new DiffuseMaterial(new SolidColorBrush(this.P_CouleurUnie));
}
else {
geo.Material = this.P_Texture;
}
geo.BackMaterial = new DiffuseMaterial(new SolidColorBrush(Colors.Yellow));
this.Visual3DModel = geo;
}
}
Pour ajouter l’objet importé à la scène 3D, on instancie un objet TFormatObjModele,
nommé modele_import, qui reçoit en paramètre le chemin absolu vers le fichier exporté
398 Programmez des jeux vidéo 3D avec C#5 et WPF
par MAYA. On lui ajoute une texture par la propriété P_Texture. Et cet objet est ajouté
à la scène par la propriété Children de x_conteneur_3d.
//clic sur x_btn_charge_ok
private void x_btn_charge_ok_Click(object sender, RoutedEventArgs e) {
x_grid_infos.Visibility = Visibility.Hidden;
TFormatObjModele modele_import = new TFormatObjModele(v_fichier_obj.P_Maillage3D);
ImageBrush pinceau_image = new ImageBrush();
pinceau_image.ImageSource = new BitmapImage(new Uri(«pack://application:,,/
contenu/image_texture/texture_cube_import.png», UriKind.Absolute));
DiffuseMaterial diffus = new DiffuseMaterial(pinceau_image);
modele_import.P_Texture = diffus;
x_conteneur_3d.Children.Add(modele_import);
}

Copyright 2013 Patrice REY


9
Sonorisation du jeu

Au sommaire de ce chapitre
• la prise en charge de l’audio avec le contrôle MediaElement
• les manipulations d’un fichier audio (lecture, pause, arrêt, positionnement
de la tête de lecture)
• l’utilisation de l’objet DispatcherTimer (minuterie)
• le contrôle du volume et de la balance d’une piste sonore
• la façon d’intégrer des pistes sonores dans un jeu pour apporter une
touche de réalisme et de convivialité supplémentaire
400 Programmez des jeux vidéo 3D avec C#5 et WPF
Tous les jeux vidéo possèdent une sonorisation. Cela permet de rendre encore plus
vivant le jeu. Dans ce chapitre vous verrez comment lire les fichiers audio avec leurs
différents formats au travers d’une application autonome pratique. Ecouter une
piste sonore et savoir comment obtenir ses caractérisques (durée, etc.) sont des
manipulations courantes à connaître. Et nous terminerons par une intégration de
pistes sonores dans le jeu.

1 - La prise en charge de l’audio

WPF prend en charge un certain nombre de format audio comme par exemple le
format WAV, le format WMA (Windows Media Audio) et le format MP3 (ISO MPEG-1
Layer III). Nous allons voir l’utilisation du contrôle MediaElement qui est le contrôle qui
prend en charge l’audio et la vidéo.

1.1 - Le contrôle MediaElement

La classe MediaElement représente un objet qui contient des données audio, vidéo
ou les deux. Un contrôle MediaElement peut lire plusieurs types de médias audio et
vidéo. Un MediaElement est fondamentalement une région rectangulaire qui peut
afficher de la vidéo sur sa surface ou lire un contenu audio (auquel cas aucune vidéo
n’est affichée, mais le MediaElement joue encore un rôle d’objet lecteur avec les API
appropriées). Dans la mesure où il s’agit d’un UIElement, un MediaElement prend
en charge des opérations d’entrée, telles que les événements de souris et du clavier,
et peut capturer le focus ou la souris. La figure 9.1 visualise l’arbre d’héritage d’un
MediaElement.
La classe MediaElement hérite de FrameworkElement, et elle est donc compatible
avec le système de disposition. Cependant, son comportement visuel est particulier
car il dépend du flux chargé. Un objet MediaElement permet de diffuser les deux types
de flux, audio et vidéo. Les propriétés booléennes HasAudio et HasVideo indiquent la
nature du flux chargé. Quand il diffuse un flux audio, l’objet MediaElement n’a pas de
Copyright 2013 Patrice REY

représentation visuelle. Quand il diffuse un flux vidéo, sa taille (propriétés ActualWidth


et ActualHeight) prend par défaut celle de la vidéo. Si les dimensions Width ou Height
sont définies, la vidéo subit un étirement ou un retrécissement. Les dimensions
originales de la vidéo sont disponibles dans les propriétés NaturalVideoWidth et
NaturalVideoHeight.
Le flux audio ou vidéo peut être spécifié au moyen d’un URI dans la propriété Source. Si
le flux est téléchargé, la progression du téléchargement peut être observée au moyen
CHAPITRE 9 Sonorisation du jeu 401

de la propriété DownloadProgress. La mise en mémoire tampon du flux peut être


observée au moyen des propriétés IsBuffering et BufferingProgress, et des événements
BufferingStarted et BufferingEnded. L’événement MediaOpened est émis quand le flux
est chargé.
Par défaut, la lecture est lancée automatiquement. Pour pouvoir la contrôler, il faut
basculer la propriété LoadedBehavior en mode Manual.
FIGURE 9.1
402 Programmez des jeux vidéo 3D avec C#5 et WPF
La classe MediaElement expose principalement les propriétés suivantes:
• la propriété Source fixe la source audio ou vidéo au travers d’un URI,
• la propriété Position représente la position courante du curseur de lecture de la
piste audio ou vidéo (de type TimeSpan),
• la propriété NaturalDuration représente la durée totale du média (de type
Duration),
• la propriété Volume représente le volume sonore du média (de type double avec
une valeur entre 0 et 1, la valeur par défaut est 0.5),
• la propriété Balance représente la balance du média (de type double avec une
valeur entre -1 et +1, la valeur par défaut est 0),
• la propriété LoadedBehavior représente le mode de prise en charge quand le média
est ouvert; sa valeur est du type de l’énumération MediaState, et sa valeur par
défaut est MediaState.Play (l’énumération MediaState est composée de MediaState.
Play, MediaState.Close, MediaState.Pause, MediaState.Stop et MediaState.Manual),
• la propriété UnloadedBehavior représente le mode de prise en charge quand le
média est fermé; sa valeur est du type de l’énumération MediaState,
• la propriété booléenne IsMuted permet de savoir si la piste sonore est coupée
(valeur false par défaut).
La méthode Play permet de lire le média depuis la position courante de la tête de
lecture. La méthode Pause permet de suspendre la lecture du média. La méthode Stop
permet d’arrêter la lecture du média et de repositionner la tête de lecture au début. La
méthode Close permet de fermer le média.
L’événement MediaOpened survient quand le média est ouvert et qu’il est correctement
chargé. En cas d’erreur (fichier non conforme, format non supporté, URI de la propriété
Source non conforme, etc.), l’événement MediaFailed est émis. L’événement MediaEnded
survient quand le média a été complètement lu.

1.2 - Manipulation d’un fichier audio

Nous allons réaliser une application simple et autonome pour bien s’imprégner des
Copyright 2013 Patrice REY

mécanismes concernant la manipulation des fichiers audio. Cette application se


trouve dans le dossier 05_VS2012_3d_sonorisation du code source de programmation.
Elle consiste à choisir un fichier audio dans un sélecteur puis à le lire. Des boutons
permettent de mettre en pause, d’arrêter et de redémarrer la piste sonore. Deux
glissières permettent de modifier le volume sonore ainsi que la balance sonore. Un
premier indicateur affiche la durée totale de la piste sonore, et un second indicateur
affiche la position de la tête de lecture en cours sous forme d’une durée. Un rectangle
CHAPITRE 9 Sonorisation du jeu 403

rouge visualise de façon proportionnelle la position de la tête de lecture sur la durée


totale de la piste sonore. La figure 9.2 visualise le résultat obtenu.

FIGURE 9.2

2
404 Programmez des jeux vidéo 3D avec C#5 et WPF
La figure 9.3 visualise la composition et la disposition des divers éléments en XAML. Le
sélecteur x_select permet de choisir une piste sonore parmi trois fichiers sonores. La
zone x_text_infos affiche des informations relatives au chargement de la piste sonore.
Les champs x_text_duree_totale et x_text_duree_encours affichent les durées relatives à la
piste sonore. Le volume sonore et la balance sonore sont modifiables par les glissières
x_slider_volume et x_slider_balance. Le rectangle x_rect_piste affiche une progression
visuelle de la piste sonore lue. Les boutons x_img_lecture, x_img_pause et x_img_arret
permettent de contrôler la piste sonore.
FIGURE 9.3

ComboBox x_select

TextBlock x_text_infos

TextBlock x_text_duree_totale

TextBlock x_text_duree_encours

Rectangle x_rect_piste
Slider x_slider_volume
Slider x_slider_balance

Image Image Image


x_img_lecture x_img_pause x_img_arret
<!-- titre -->
<TextBlock Canvas.Left=ˈ10ˈ TextWrapping=ˈWrapˈ Canvas.Top=ˈ3ˈ Width=ˈ487ˈ
TextAlignment=ˈCenterˈ FontFamily=ˈSegoe WP Semiboldˈ FontSize=ˈ20ˈ
Background=ˈ#C6FFF6D7ˈ>Lecteur de fichier audio</TextBlock>
<!-- selecteur de musique -->
Copyright 2013 Patrice REY

<ComboBox x:Name=ˈx_selectˈ Canvas.Left=ˈ10ˈ Canvas.Top=ˈ34ˈ Width=ˈ487ˈ


Height=ˈ30ˈ Cursor=ˈHandˈ FontFamily=ˈVerdanaˈ FontSize=ˈ16ˈ SelectedIndex=ˈ0ˈ
Foreground=ˈ#FFB40000ˈ SelectionChanged=ˈx_select_SelectionChangedˈ>
<ComboBoxItem Content=ˈChoisir une musique ...ˈ/>
<ComboBoxItem Foreground=ˈ#FF003D87ˈ Content=ˈmusique de fond (fichier: musique_
fond_03m00.mp3)ˈ/>
<ComboBoxItem Foreground=ˈ#FF003D87ˈ Content=ˈmusique de fond (fichier: musique_
inter_00m21.mp3)ˈ/>
CHAPITRE 9 Sonorisation du jeu 405

<ComboBoxItem Foreground=ˈ#FF003D87ˈ Content=ˈmusique de fond (fichier: musique_


fond_00m56.mp3)ˈ/>
</ComboBox>
<!-- zone des infos audio -->
<Rectangle Fill=ˈ#C4FEFEFFˈ Height=ˈ223ˈ Canvas.Left=ˈ10ˈ Stroke=ˈBlackˈ Canvas.
Top=ˈ67ˈ Width=ˈ487ˈ RadiusX=ˈ10ˈ RadiusY=ˈ10ˈ/>
<TextBlock Canvas.Left=ˈ21ˈ TextWrapping=ˈWrapˈ Text=ˈInformations :ˈ Canvas.
Top=ˈ76ˈ FontSize=ˈ16ˈ/>
<Line Stroke=ˈBlackˈ X2=ˈ468ˈ Canvas.Left=ˈ19ˈ Canvas.Top=ˈ100ˈ/>
<TextBlock x:Name=ˈx_text_infosˈ Canvas.Left=ˈ21ˈ TextWrapping=ˈWrapˈ Text=ˈinfosˈ
Canvas.Top=ˈ102ˈ FontSize=ˈ16ˈ Height=ˈ176ˈ Width=ˈ464ˈ/>
<!-- mediaelement pour lire flux audio -->
<Rectangle Fill=ˈ#C4FEFEFFˈ Height=ˈ133ˈ Canvas.Left=ˈ10ˈ Stroke=ˈBlackˈ Canvas.
Top=ˈ295ˈ Width=ˈ487ˈ RadiusX=ˈ10ˈ RadiusY=ˈ10ˈ/>
<MediaElement x:Name=ˈx_media_elem_xamlˈ LoadedBehavior=ˈManualˈ
UnloadedBehavior=ˈManualˈ/>
<TextBlock x:Name=ˈx_text_duree_totaleˈ Canvas.Left=ˈ239ˈ TextWrapping=ˈWrapˈ
Text=ˈsur 00:00ˈ Canvas.Top=ˈ299ˈ FontSize=ˈ14ˈ/>
<TextBlock x:Name=ˈx_text_duree_encoursˈ Canvas.Left=ˈ200ˈ TextWrapping=ˈWrapˈ
Text=ˈ00:00ˈ Canvas.Top=ˈ299ˈ FontSize=ˈ14ˈ Foreground=ˈ#FFFF0707ˈ/>
<Image x:Name=ˈx_img_lectureˈ Height=ˈ64ˈ Canvas.Left=ˈ281ˈ Canvas.
Top=ˈ354ˈ Width=ˈ64ˈ Source=ˈcontenu/btn_piste_sonore_lecture_non_actif.pngˈ
MouseLeftButtonDown=ˈx_img_lecture_MouseLeftButtonDownˈ />
<Image x:Name=ˈx_img_pauseˈ Height=ˈ64ˈ Canvas.Left=ˈ350ˈ Canvas.
Top=ˈ354ˈ Width=ˈ64ˈ Source=ˈcontenu/btn_piste_sonore_pause_non_actif.pngˈ
MouseLeftButtonDown=ˈx_img_pause_MouseLeftButtonDownˈ/>
<Image x:Name=ˈx_img_arretˈ Height=ˈ64ˈ Canvas.Left=ˈ419ˈ Canvas.
Top=ˈ354ˈ Width=ˈ64ˈ Source=ˈcontenu/btn_piste_sonore_arret_non_actif.pngˈ
MouseLeftButtonDown=ˈx_img_arret_MouseLeftButtonDownˈ/>
<Rectangle Fill=ˈ#C4B5FF9Aˈ Height=ˈ26ˈ Canvas.Left=ˈ21ˈ Stroke=ˈBlackˈ Canvas.
Top=ˈ322ˈ Width=ˈ464ˈ/>
<Rectangle x:Name=ˈx_rect_pisteˈ Fill=ˈ#C4FF6060ˈ Height=ˈ22ˈ Canvas.Left=ˈ23ˈ
Canvas.Top=ˈ324ˈ Width=ˈ460ˈ/>
<!-- slider volume -->
<TextBlock Canvas.Left=ˈ23ˈ TextWrapping=ˈWrapˈ Text=ˈVolumeˈ Canvas.Top=ˈ357ˈ/>
<Slider x:Name=ˈx_slider_volumeˈ Canvas.Left=ˈ77ˈ Canvas.Top=ˈ354ˈ Width=ˈ187ˈ
Background=ˈ#FFB6E8FFˈ Value=ˈ0.5ˈ Maximum=ˈ1ˈ Cursor=ˈHandˈ
ValueChanged=ˈx_slider_volume_ValueChangedˈ LargeChange=ˈ0.2ˈ/>
<!-- slider balance -->
<TextBlock Canvas.Left=ˈ23ˈ TextWrapping=ˈWrapˈ Text=ˈBalanceˈ Canvas.Top=ˈ390ˈ/>
<Slider x:Name=ˈx_slider_balanceˈ Canvas.Left=ˈ77ˈ Canvas.Top=ˈ389ˈ Width=ˈ187ˈ
Background=ˈ#FFB6E8FFˈ Value=ˈ0ˈ Maximum=ˈ1ˈ Cursor=ˈHandˈ
ValueChanged=ˈx_slider_balance_ValueChangedˈ LargeChange=ˈ0.2ˈ Minimum=ˈ-1ˈ
IsDirectionReversed=ˈTrueˈ/>
L’objet MediaElement x_media_elem_xaml est ajouté directement en XAML comme un
contrôle puisqu’il hérite de FrameworkElement. On fixe ses propriétés LoadedBehavior
et UnloadedBehavior à la valeur Manual (qui permet de contrôler la piste sonore par
programmation).
406 Programmez des jeux vidéo 3D avec C#5 et WPF
Le sélecteur ComboBox x_select permet de sélectionner une piste sonore. La méthode
OuvrirFichierMP3 lance la procédure de manipulation de la piste sonore (en passant
le nom du fichier MP3). La propriété Source de MediaElement fixe la source du média
au moyen d’un URI. Les flux audio et vidéo ne peuvent pas être embarqués comme
ressource dans l’application. Le plus simple consiste à créer un dossier media_audio
et à ajouter dedans les trois pistes sonores. Puis, en faisant un clic droit sur chaque
piste sonore en choisissant la rubrique Properties, dans la fenêtre des propriétés, on
fixe la valeur Content pour Build action, et la valeur Copy always pour Copy to output
directory. De cette façon, à la compilation, un dossier media_audio sera créé à côté
de l’exécutable, et il contiendra une copie des pistes sonores. Les figures 9.4 et 9.5
visualisent la démarche.

FIGURE 9.4

//ouverture d’un flux audio


private void OuvrirFichierMP3(string chemin_fichier_mp3) {
ViderZoneInfos();
Copyright 2013 Patrice REY

AfficherInfos(«choix: « + chemin_fichier_mp3);
v_fichier_audio = chemin_fichier_mp3;
x_media_elem_xaml.Source = new Uri(doss_exe + «/media_audio/» + v_fichier_audio,
UriKind.Absolute);
x_media_elem_xaml.Volume = 0.5;
x_media_elem_xaml.MediaOpened += x_media_elem_xaml_MediaOpened;
x_media_elem_xaml.MediaFailed += x_media_elem_xaml_MediaFailed;
x_media_elem_xaml.MediaEnded += x_media_elem_xaml_MediaEnded;
x_media_elem_xaml.Play(); }
CHAPITRE 9 Sonorisation du jeu 407
FIGURE 9.5

Les gestionnaires pour les événements MediaOpened, MediaFailed et MediaEnded sont


ajoutés. La piste sonore démarre manuellement par la méthode Play. Si la piste sonore
est bien ouverte, on affiche sa durée totale en récupérant l’objet TimeSpan par sa
propriété NaturalDuration.TimeSpan.
private void x_media_elem_xaml_MediaOpened(object sender, RoutedEventArgs e) {
if (x_media_elem_xaml.Source != null) {
AfficherInfos(«fichier ouvert -> « + v_fichier_audio);
TimeSpan duree = x_media_elem_xaml.NaturalDuration.TimeSpan;
string duree_texte = duree.Minutes.ToString(«00») + «:»
+ duree.Seconds.ToString(«00»);
AfficherInfos(«durée (MM:SS) -> « + duree_texte);
x_text_duree_totale.Text = «sur « + duree_texte;
v_duree_piste_ms = duree.TotalMilliseconds;
v_rect_piste_long_encours = 0;
x_rect_piste.Width = v_rect_piste_long_encours;
DesactiverBouton(x_img_lecture);
ActiverBouton(x_img_pause);
ActiverBouton(x_img_arret);
v_dispatcher.Start();
}
}
408 Programmez des jeux vidéo 3D avec C#5 et WPF
En cours de lecture de la piste sonore, il faut pouvoir récupérer le temps écoulé pour
visualiser la progression du temps écoulé par rapport au temps total. On va donc animer
une jauge, composée de deux rectangles superposés, et simuler un traitement au
moyen d’un timer spécifique à WPF qu’est le DispatcherTimer (dans l’espace de noms
System.Windows.Threading). Chaque thread visuel WPF intègre un objet Dispatcher
responsable de la boucle des messages. Contrairement aux minuteries traditionnelles
de .NET, le DispatcherTimer s’exécute automatiquement dans le thread visuel du
Dispatcher. La figure 9.6 visualise l’arbre d’héritage de la classe DispatcherTimer. La
propriété Interval de l’objet DispatcherTimer représente l’intervalle entre deux relevées
de la minuterie. L’événement Tick représente le relevé de la minuterie survenant au
début et à la fin d’un intervalle de temps. Les méthodes Start et Stop démarrent et
stoppent la minuterie.
FIGURE 9.6

On instancie une variable DispatcherTimer v_dispatcher. On lui fixe un relevé de


l’horloge à intervalles réguliers par la propriété Interval (tous les 100 millisecondes).
On lui ajoute un gestionnaire pour l’événement Tick (événement qui se produit à
chaque intervalle pour effectuer le relevé de la minuterie).
Tick Tick Tick Tick Tick Tick
Copyright 2013 Patrice REY

DispatcherTimer
v_dispatcher

intervalle de
0.1 seconde

Chaque fois qu’un relevé est effectué, on note la position de la tête de lecture (propriété
CHAPITRE 9 Sonorisation du jeu 409

MediaElement.Position). Cette position est lue à intervalles réguliers dans l’événement


Tick du DispatcherTimer. Il ne reste plus qu’a estimer la propriété Width du rectangle
x_rect_piste proportionnellement (régle de trois) par rapport au temps écoulé en
fonction du temps total (on se sert des variables additionnelles v_rect_piste_long_maxi
qui stocke la longueur maximale que peut prendre le rectangle, temps_encours_ms qui
représente le temps écoulé en cours exprimé en millisecondes, et v_duree_piste_ms
qui représente la durée de la piste sonore exprimée en millisecondes). On utilise les
durées en millisecondes pour faire une division entre des nombres de type double.

Rectangle
x_rect_piste

1 x_rect_piste.Width

v_rect_piste_long_maxi

Tick Tick Tick Tick Tick Tick

DispatcherTimer
v_dispatcher

2 intervalle de
0.1 seconde

private void Window_Loaded(object sender, RoutedEventArgs e) {


ViderZoneInfos();
v_rect_piste_long_maxi = x_rect_piste.Width;
x_rect_piste.Width = 0;
v_dispatcher = new DispatcherTimer();
v_dispatcher.Interval = TimeSpan.FromSeconds(0.1);
v_dispatcher.Tick += v_dispatcher_Tick;
}
410 Programmez des jeux vidéo 3D avec C#5 et WPF
private void v_dispatcher_Tick(object sender, EventArgs e) {
TimeSpan recup_temps = x_media_elem_xaml.Position;
string recup_texte = recup_temps.Minutes.ToString(«00») + «:»
+ recup_temps.Seconds.ToString(«00»);
x_text_duree_encours.Text = recup_texte;
double temps_encours_ms = recup_temps.TotalMilliseconds;
v_rect_piste_long_encours = (double)(v_rect_piste_long_maxi * temps_encours_ms) /
(double)v_duree_piste_ms;
x_rect_piste.Width = v_rect_piste_long_encours;
}
Le réglage du volume s’effectue par modification de la glissière x_slider_volume, qui
varie entre les valeurs 0 et 1 de type double.
0.5

0 1

Le réglage de la balance s’effectue par modification de la glissière x_slider_balance, qui


varie entre les valeurs -1 et +1 de type double. La valeur -1 correspond à la balance
côté droit et la valeur +1 correspond à la balance gauche. De façon à respecter le côté
gauche de la glissière pour la balance gauche, et inversement pour la balance droite,
on fixe la propriété IsDirectionReversed de la glissière à true.
0

-1 +1

enceinte enceinte enceinte enceinte


gauche gauche droite droite

//
private void x_slider_volume_ValueChanged(object sender,
Copyright 2013 Patrice REY

RoutedPropertyChangedEventArgs<double> e) {
x_media_elem_xaml.Volume = x_slider_volume.Value;
}
//
private void x_slider_balance_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e) {
x_media_elem_xaml.Balance = x_slider_balance.Value;
}
CHAPITRE 9 Sonorisation du jeu 411

Quand la piste sonore est en cours de lecture, elle peut être mise en pause (figure 9.7
repère 1) ou mise à l’arrêt (figure 9.7 repère 4). Quand la piste sonore est en pause,
elle peut être mise de nouveau en lecture (figure 9.7 repère 2) ou mise à l’arrêt (figure
9.7 repère 4).
FIGURE 9.7

//btn lecture
private void x_img_lecture_MouseLeftButtonDown(object sender,
MouseButtonEventArgs e) {
DesactiverBouton(x_img_lecture);
x_media_elem_xaml.Play();
ActiverBouton(x_img_pause);
ActiverBouton(x_img_arret);
}
412 Programmez des jeux vidéo 3D avec C#5 et WPF
La figure 9.8 visualise les différentes étapes de l’activité et de l’inactivité des trois
boutons en fonction de l’utilisation de la piste sonore (lecture, arrêt et pause).
FIGURE 9.8
piste sonore à l’arrêt

piste sonore en lecture

piste sonore en pause

piste sonore à l’arrêt

piste sonore en lecture

Copyright 2013 Patrice REY

piste sonore en pause


CHAPITRE 9 Sonorisation du jeu 413

//btn pause
private void x_img_pause_MouseLeftButtonDown(object sender,
MouseButtonEventArgs e) {
DesactiverBouton(x_img_pause);
x_media_elem_xaml.Pause();
ActiverBouton(x_img_lecture);
ActiverBouton(x_img_arret);
}
//btn arret
private void x_img_arret_MouseLeftButtonDown(object sender,
MouseButtonEventArgs e) {
DesactiverBouton(x_img_arret);
x_media_elem_xaml.Stop();
ActiverBouton(x_img_lecture);
DesactiverBouton(x_img_pause);
}

2 - Ajouter des séquences sonores au jeu

Maintenant, nous allons terminer en ajoutant des pistes sonores au jeu (dossier
05_VS2012_3d_c5_wpf45_jeu dans le code de programmation). Au lancement du jeu
(figure 9.9 repère 1), on a l’écran d’attente. Durant cette étape, on va jouer en boucle
la piste sonore intitulée musique_fond_00m56.mp3. A noter que toutes les pistes
sonores utilisées sont ajoutées dans un dossier nommé media_audio, et comme on l’a
déjà vu, à chaque piste audio, on lui affecte la valeur Content pour Build action et la
valeur Copy always dans Copy in output directory (fenêtre des propriétés). La variable
MediaElement v_audio_attente charge le fichier sonore, ses propriétés LoadedBehavior
et UnloadedBahavior sont fixées à Manual, et sa propriété Volume est fixée à 0.5. Avec
la méthode Play, on démarre la piste sonore. Et pour qu’elle joue en boucle, il faut
lui ajouter un gestionnaire pour l’événement MediaEnded dans lequel on applique la
méthode Stop à la piste sonore (ce qui entraîne le positionnement de la tête de lecture
au début) puis la méthode Play (qui joue la piste sonore à partir de l’emplacement
courant de la tête de lecture).
private void Window_Loaded(object sender, RoutedEventArgs e) {
...
v_audio_attente = new MediaElement();
v_audio_attente.Source = new Uri(doss_exe + «/media_audio/musique_fond_00m56.
mp3»,UriKind.Absolute);
v_audio_attente.LoadedBehavior = MediaState.Manual;
v_audio_attente.UnloadedBehavior = MediaState.Manual;
v_audio_attente.Volume = 0.5;
v_audio_attente.Play();
v_audio_attente.MediaEnded += v_piste_attente_MediaEnded; }
FIGURE
9.9

MediaElement
v_audio_attente
(volume = 0.5)

clic bouton
lancer

MediaElement
v_audio_attente
(volume = 0.30)

MediaElement
v_audio_attente
(volume = 0.30)
CHAPITRE 9 Sonorisation du jeu 415

//
private void v_piste_attente_MediaEnded(object sender, RoutedEventArgs e) {
v_audio_attente.Stop();
v_audio_attente.Play();
}
Quand le joueur clique sur le bouton de démarrage du jeu, on baisse le volume de v_
audio_attente à une valeur de 0.5 et on charge le fichier sonore musique_fond_03m00.
mp3 qui sera joué durant la phase propre du jeu. La variable MediaElement v_audio_jeu
stocke cette piste sonore. Le repère 2 de la figure 9.9 visualise les écrans de cette étape.
On ajoute aussi un gestionnaire pour l’événement MediaEnded pour faire boucler la
lecture de la piste sonore v_audio_jeu.
//bouton de lancement du jeu
private void x_btn_lancer_Click(object sender, RoutedEventArgs e) {
...
v_audio_attente.Volume = 0.30;
v_audio_jeu = new MediaElement();
v_audio_jeu.Source = new Uri(doss_exe + «/media_audio/musique_fond_03m00.mp3»,
UriKind.Absolute);
v_audio_jeu.LoadedBehavior = MediaState.Manual;
v_audio_jeu.UnloadedBehavior = MediaState.Manual;
v_audio_jeu.Volume = 0;
v_audio_jeu.MediaEnded += v_audio_jeu_MediaEnded;
}
//
private void v_audio_jeu_MediaEnded(object sender, RoutedEventArgs e) {
v_audio_jeu.Stop();
v_audio_jeu.Play();
}
Et quand le compte à rebours est terminé, on procède à la lecture de la piste sonore
v_audio_jeu par la méthode Play (et en fixant sa propriété Volume à 0.5).
//evenement storyboard arc termine
private void stb_arc_rebours_Completed(object sender, EventArgs e) {
...
v_audio_attente.Stop();
v_audio_jeu.Volume = 0.5;
v_audio_jeu.Play();
}
Quand la fin de jeu arrive, on a une piste sonore applaudissement_00m02.mp3 (durée 2
secondes) à jouer si la partie est gagnée et une piste sonore partie_perdue_00m04.mp3
(durée 4 secondes) à jouer si la partie est perdue.
En fonction de la fin de jeu, une de ces deux pistes est jouée (figure 9.10). La
variable MediaElement v_audio_partie_perdue joue une de ces pistes et la variable
MediaElement v_audio_partie_gagnee joue l’autre piste.
416 Programmez des jeux vidéo 3D avec C#5 et WPF
FIGURE 9.10

fin de jeu

MediaElement
v_audio_partie_perdue
(volume = 0.5)

MediaElement
v_audio_partie_gagnee
(volume = 0.5)

if (v_etat_jeu == EtatJeu.terminer) {
...
Copyright 2013 Patrice REY

if (v_liste_brique.Count != 0) {
message = «Partie perdue !!!»;
v_audio_partie_perdue = new MediaElement();
v_audio_partie_perdue.Source = new Uri(doss_exe +
«/media_audio/partie_perdue_00m04.mp3», UriKind.Absolute);
v_audio_partie_perdue.LoadedBehavior = MediaState.Manual;
v_audio_partie_perdue.UnloadedBehavior = MediaState.Manual;
v_audio_partie_perdue.Volume = 0.5;
v_audio_partie_perdue.Play(); }
CHAPITRE 9 Sonorisation du jeu 417

else {
message = «Partie gagnée avec « + v_score.ToString() + « pts en «;
message += v_stopwatch.Elapsed.Minutes.ToString(«00») + «:»
+ v_stopwatch.Elapsed.Seconds.ToString(«00»);
v_audio_partie_gagnee = new MediaElement();
v_audio_partie_gagnee.Source = new Uri(doss_exe +
«/media_audio/applaudissement_00m02.mp3», UriKind.Absolute);
v_audio_partie_gagnee.LoadedBehavior = MediaState.Manual;
v_audio_partie_gagnee.UnloadedBehavior = MediaState.Manual;
v_audio_partie_gagnee.Volume = 0.5;
v_audio_partie_gagnee.Play();
}
Et quand l’écran de fin de jeu est actif, un clic sur le bouton x_btn_rejouer relance une
nouvelle partie pour laquelle on redémarre une nouvelle séquence de la piste sonore
v_audio_attente. Ne pas oublier d’appliquer la méthode Stop puis la méthode Start pour
lire une piste sonore depuis le début quand elle a été jouée au moins une fois.
//clic sur bouton rejouer une partie
private void x_btn_rejouer_Click(object sender, RoutedEventArgs e) {
...
v_audio_attente.Volume = 0.5;
v_audio_attente.Play();
}
private void stb_arc_rebours_rejouer_Completed(object sender, EventArgs e) {
...
v_audio_attente.Stop();
v_audio_jeu.Volume = 0.5;
v_audio_jeu.Play();
}
INDEX
Index 421

Symboles Build action 406


ByteAnimation 318
ByteAnimationUsingKeyFrames 318
\u000A 392

A C
Camera 113
Add 24, 30, 32
caméra 77
AffineTransform3D 90
CenterX 91, 99
AmbientColor 124
CenterY 91, 99
AmbientLight 124, 127
CenterZ 91, 99
Angle 67
centre de projection 72
animation 316
CharAnimationUsingKeyFrames 318
animation réaliste 349
Children 113
Append 64
CircleEase 353
ASUS EP121 252
cisaillement 49
AutoReverse 321
ClipToBounds 134
axes de coordonnées 164
Close 402
Axis 67
collier nul 204
AxisAngleRotation3D 103
Collision 288
axonométriques 75
Color 124

B
ColorAnimation 318
ColorAnimationUsingKeyFrames 318
CompositionTarget 273
BackEase 352 CompositionTarget.Rendering 274
BackMaterial 117 cône 188
Balance 402 cône circulaire droit 189
Begin 320 ConstantAttenuation 128
BeginTime 321 ContainerUIElement3D 116
BitsPerPixel 256 Content 406
BooleanAnimationUsingKeyFrames 318 coordonnées euclidiennes 42
BounceEase 352 coordonnées homogènes 52
Bounds 256 coordonnée sphérique 172
Brush 123, 124 Copy always 406
BufferingEnded 401 Copy to output directory 406
BufferingProgress 401 Count 321
BufferingStarted 401 cube 150
422 Index
cylindre 172 ElasticEase 354
cylindre circulaire 173 EmissiveMaterial 124
Encoding.ASCII 392

D ExponentialEase 355

DecimalAnimation 318
DecimalAnimationUsingKeyFrames 318
F
DependencyProperty 214 facette cylindrique 175
DependencyProperty.Register 214 facette rectangulaire 144
Descartes 25 facette triangulaire 6, 134
Determinant 57 FielOfView 130
DeviceName 256 FillBehavior 322
DiffuseMaterial 122, 124 Forever 321
direction 26 format Wavefront OBJ 387
Direction 128 forme polygonale 185
direction du haut 78 FrameworkElement 400
direction du regard 78
DirectionLight 127
DispatcherTimer 408
DoubleAnimation 318
G
Geometry 117
DoubleAnimationUsingKeyFrames 318
GeometryModel3D 6, 116, 117
DoubleAnimationUsingPath 318

H
DownloadProgress 401
DrawingBrush 125
Duration 321
HasAudio 400

E HasInverse 57
HasVideo 400
HitTestResult 224
EaseIn 350
HoldEnd 322
EaseInOut 350

I
EaseOut 350
EasingFunctionBase 350
éclairage de la scène 6, 127
effet miroir 44 IEasingFunction 349
Elapsed 276 ImageBrush 145
ElapsedMilliseconds 276 InnerConeAngle 128
Index 423
Int16Animation 318 m21 53
Int32Animation 318 m22 53
Int64Animation 318 m23 53
IntAnimationUsingKeyFrames 318 m24 53
IntersectWith 285 m31 53
Interval 408 m32 53
Inverse 91, 99 m33 53
inversible 60 m34 53
Invert 63 Manual 401
IsAffine 57, 91 Material 117, 123
IsBuffering 401 MaterialGroup 124, 125
isEmpty 285 matrice de transformation 43
IsIdentity 57, 67 matrice inversée 60
IsMuted 402 Matrix 104
IsNormalized 67 Matrix3D 57
isométrique 75 Matrix3D.Identity 57
ISO MPEG-1 Layer III 400 Matrix3D.Multiply 63
MatrixAnimationUsingKeyFrames 318

L MatrixAnimationUsingPath 318
MatrixCamera 81, 129
MatrixTransform3D 6, 90, 104
Length 27
MAYA 386
LengthSquared 27, 35
MediaElement 400
Light 127
MediaEnded 402
LinearAttenuation 128
MediaFailed 402
LoadedBehavior 401, 402
MediaOpened 401, 402
Location 285
MediaState 402
longueur 26
MediaState.Close 402
Look Direction 78
MediaState.Manual 402
LookDirection 130
MediaState.Pause 402

M
MediaState.Play 402
MediaState.Stop 402
MeshGeometry3D 118
m11 53 Model3D 114
m12 53 Model3DGroup 116, 117, 214
m13 53 modélisation basique 6, 134
m14 53 ModelUIElement3D 112
424 Index
ModelVisual3D 6, 112, 114 Play 402
MouseWheel 235 Point3DAnimation 318
MP3 400 Point3DAnimationUsingKeyFrames 318
Multiply 34 Point3DCollection 23
Point4D 5, 56

N Point4D.Add 56
Point4D.Equals 56
Point4D.Multiply 56
NaturalDuration 402
Point4D.Parse 56
NaturalVideoHeight 400
Point4D.Substract 56
NaturalVideoWidth 400
PointAnimation 318
Normalize 36
PointAnimationUsingKeyFrames 318
Normals 119
PointAnimationUsingPath 319

O
point de fuite 73
point de fuite axial 73
PointLight 127
OBJ 385 PointLightBase 127
ObjectAnimationUsingKeyFrames 318 Position 402
obliques 75 Positions 118
Offset 56, 285 Prepend 64
OffsetX 57 Primary 256
OffsetY 57 projecteurs 72
OffsetZ 57 projection 5, 72
OnUpdateModel 214 ProjectionCamera 129
OrthographicCamera 130 ProjectionMatrix 81
orthonormale 75 projection parallèle 5, 75, 85
OuterConeAngle 128 projection perspective 5, 73
propagation 212

P PropertyMetadata 215

parallèle 73
Pause 320, 402
Q
perspective 73 QuadraticAttenuation 128
PerspectiveCamera 130 Quaternion 67, 233, 244
placage de texture 156 QuaternionAnimation 318
plan de projection 78 QuaternionAnimationUsingKeyFrames
plan de travail 164 318
Index 425
Quaternion.Identity 67 ScalePrepend 65
QuaternionRotation3D 104, 233, 244 ScaleTransform3D 90, 92
QuinticEase 356, 357, 358 ScaleX 91
ScaleY 91

R ScaleZ 91
Screen 255
Screen.AllScreens 256
Range 128
Screen.PrimaryScreen 256
RayHitTestResult 226
sens 26
RayMeshGeometry3DHitTestResult 224
SetTargetName 320
readonly 214
SetTargetProperty 320
Rect3D 285
SineEase 350
RectAnimation 318
SingleAnimation 318
RectAnimationUsingKeyFrames 318
SingleAnimationUsingKeyFrames 318
réflexion 44
SizeAnimation 318
réflexion spéculaire 6, 126
SizeAnimationUsingKeyFrames 318
Rendering 274
SizeX 285
rendu 72
SizeY 285
rendu d’animation 273
SizeZ 285
RepeatBehavior 321
Source 400, 402
Reset 277
SpecularMaterial 124
Resume 320
SpecularPower 126
Rotate 63
SpeedRatio 321
RotateAt 63
sphère 195
RotatePrepend 69
SpotLight 128
RotateTransform3D 90, 99, 233
Start 276, 408
Rotation 99
StartNew 276
Rotation3DAnimation 318
Stop 320, 402, 408
Rotation3DAnimationUsingKeyFrames
Stopwatch 276
318
Storyboard 320
routage 212
Storyboard.TargetName 320

S
Storyboard.TargetProperty 320
StreamReader 392
StringAnimationUsingKeyFrames 318
scalaire 34 Substract 31
Scale 63 surface conique 188
ScaleAt 63 surface cylindrique 173
426 Index
surface lumineuse 6, 125
surface mate 6, 124
System.Diagnostics 276
V
Value 91
System.Windows.Forms 255
Vector3DAnimation 318
System.Windows.Media.Media3D 285
Vector3DAnimationUsingKeyFrames 318

T
VectorAnimation 318
VectorAnimationUsingKeyFrames 318
ViewMatrix 81
test d’atteinte 221 Viewport3D 112, 227
TextureCoordinates 119 Visual 308
ThicknessAnimation 318 Visual3D 113, 214
ThicknessAnimationUsingKeyFrames 318 Visual3DModel 214, 397
Tick 408 VisualBrush 141, 145, 306
Timeline 321 VisualTreeHelper 224
TimeSpan 277, 321 Volume 402
tore 204 volume d’observation 77
Trackball 227
Transform 63, 81
Transform3D 90
Transform3DCollection 90
W
W 83
Transform3DGroup 90
WAV 400
transformations 3D 90
Wavefront OBJ 385
transformations géométriques 42
Wavefront Technologies 387
TransformGroup3D 109
Width 130
Translate 63
Windows Media Audio 400
TranslatePrepend 64
WMA 400
TranslateTransform3D 90, 96
WorkingArea 256
TriangleIndices 118

X
tunneling 212

U X 18

Y
UIElement3D 6, 116, 212
UnloadedBehavior 402
Up Direction 78
UpDirection 130 Y 18
Index 427

Z
Z 18
colonne colonne colonne colonne colonne colonne colonne colonne
1 2 3 4 5 6 7 8
ligne 1 V q 9 O i N v x
ligne 2 5 A U Z s 4 P h
ligne 3 T p 4 E g u M 8
ligne 4 b F 1 6 Y D y w
ligne 5 G r X B 0 t e Q
ligne 6 f S c 2 n L j z
ligne 7 7 H k I 3 9 C a
ligne 8 W 1 d J m K 5 R

Vous aimerez peut-être aussi