1. Comment voir ce que l'on dessine ?▲
Dans l'article précédent (1), l'affichage de notre monde 3D est défini grâce à l'utilisation de l'instruction gluLookAt pour positionner la caméra (position, cible, orientation). Les paramètres sont fixes, écrits en dur dans le code. Il n'y a donc pas de possibilité de se déplacer dans le monde 3D afin d'atteindre un point de vue plus adapté à notre besoin.
Une première méthode afin de pouvoir déplacer le point de vue consiste à modifier les trois paramètres de la caméra :
- sa position (où l'on se trouve dans le monde 3D) ;
- la position de la cible (vers où se dirige le regard) ;
- son orientation (où est le haut?) ;
et à les passer à la méthode gluLookAt afin de redéfinir la caméra.
L'expérience montre que cette méthode, quoique simple en apparence (les translations sont facilement définies, mais les rotations amènent à de savants calculs à coups de sinus et cosinus), est assez laborieuse à gérer et amène souvent à rencontrer le fameux problème de "gimbal lock" ou blocage de cardan (2).
La seconde méthode se passe de la gestion de ces trois points, ainsi que de l'utilisation de gluLookAt. L'affichage OpenGL utilise des opérations matricielles pour définir toutes les manipulations de l'espace 3D, il est donc intéressant de travailler directement sur des matrices, que l'on passe ensuite à OpenGL pour définir la vision que l'on désire avoir.
Le calcul matriciel peut paraitre assez lourd, surtout si on n'a pas suffisamment de bagage technique ou d'expérience de manipulation, mais nous n'utiliserons que des opérations simples ici.
Afin d'éviter les calculs trigonométriques et le "gimbal lock", nous nous servirons également d'un outil mathématique "magique" : le quaternion. Le nom fait souvent peur mais nous verrons que son utilisation est relativement simple et qu'il serait dommage de s'en priver.
2. Les opérations matricielles▲
2-A. Opérations de base▲
Nous n'allons pas aborder en détail toute la théorie du calcul matriciel ici, le net regorge de suffisamment d'information à ce sujet. Je vous renvoie néanmoins vers la FAQ Mathématiques pour les Jeux du site qui renferme tout ce qu'il faut savoir sur les matrices.
Dans PGE, nous utiliserons principalement des matrices 3x3 (rotations, changement de repère...) et 4x4 (quaternions et matrices OpenGL). Les principales opérations implémentées sont :
- initialisation (tableau, matrice identité, matrice de transformation...) ;
- multiplication (matrice x matrice, matrice x vecteur...) ;
- affectation de valeurs des éléments ;
- conversion en buffer OpenGL.
Le faible nombre de méthodes à définir ne m'a pas poussé à créer une classe générique de matrice NxN à étendre pour obtenir ces deux classes, ni à utiliser une classe d'une bibliothèque mathématique déjà existante.
En amont de ces deux classes de matrices, nous créons également les classes pour les objets qui seront manipulés par ces matrices, à savoir le point (classe Point3d) et le vecteur (classe Vector3d)
2-B. La classe Point3d▲
Cette classe définit un point de l'espace 3D. Un nom et les coordonnées du point sont définis, ainsi que les opérations de translation.
public
class
Point3d {
public
String id;
private
double
x;
private
double
y;
private
double
z;
public
Point3d
(
) {
init
(
);
}
public
Point3d
(
double
a, double
b, double
c) {
init
(
);
this
.x =
a;
this
.y =
b;
this
.z =
c;
}
public
final
void
init
(
) {
this
.id =
"noname"
;
this
.setX
(
0
);
this
.setY
(
0
);
this
.setZ
(
0
);
}
public
void
add
(
double
a, double
b, double
c) {
this
.setX
(
this
.getX
(
) +
a);
this
.setY
(
this
.getY
(
) +
b);
this
.setZ
(
this
.getZ
(
) +
c);
}
public
void
add
(
Vector3d vector) {
this
.setX
(
this
.getX
(
) +
vector.getX
(
));
this
.setY
(
this
.getY
(
) +
vector.getY
(
));
this
.setZ
(
this
.getZ
(
) +
vector.getZ
(
));
}
public
void
ratio
(
double
r) {
this
.setX
(
this
.getX
(
) /
r);
this
.setY
(
this
.getY
(
) /
r);
this
.setZ
(
this
.getZ
(
) /
r);
}
@Override
public
String toString
(
) {
String _chaine =
"x="
+
this
.getX
(
) +
" y="
+
this
.getY
(
) +
" z="
+
this
.getZ
(
);
return
_chaine;
}
public
double
getX
(
) {
return
x;
}
public
void
setX
(
double
x) {
this
.x =
x;
}
public
double
getY
(
) {
return
y;
}
public
void
setY
(
double
y) {
this
.y =
y;
}
public
double
getZ
(
) {
return
z;
}
public
void
setZ
(
double
z) {
this
.z =
z;
}
public
Point3d translate
(
Vector3d vector) {
Point3d temp=
new
Point3d
(
);
temp.setX
(
this
.getX
(
) +
vector.getX
(
));
temp.setY
(
this
.getY
(
) +
vector.getY
(
));
temp.setZ
(
this
.getZ
(
) +
vector.getZ
(
));
return
temp;
}
public
Point3d translate
(
double
tx,double
ty,double
tz) {
Point3d temp=
new
Point3d
(
);
temp.setX
(
this
.getX
(
) +
tx);
temp.setY
(
this
.getY
(
) +
ty);
temp.setZ
(
this
.getZ
(
) +
tz);
return
temp;
}
}
2-C. La classe Vector3d▲
Cette classe définit un vecteur de l'espace 3D, associé à un angle (nous verrons son utilisation lors de la définition de la classe Quaternion).
Les opérations implémentées dans cette classe sont les suivantes :
- définition, initialisation ;
- normalisation du vecteur ;
- affectation et récupération des composantes ;
- échelle ;
- produits scalaire et vectoriel ;
- combinaison linéaire ;
- conversion vers String ;
- clonage.
public
class
Vector3d {
private
double
x;
private
double
y;
private
double
z;
private
double
angle;
public
Vector3d
(
) {
this
.init
(
);
}
public
Vector3d
(
double
x,double
y,double
z) {
this
.x=
x;
this
.y=
y;
this
.z=
z;
this
.angle=
0
;
}
public
Vector3d
(
Vector3d vec) {
this
.x=
vec.x;
this
.y=
vec.y;
this
.z=
vec.z;
this
.angle=
vec.angle;
}
public
Vector3d
(
Point3d a,Point3d b) {
this
.x=
b.getX
(
)-
a.getX
(
);
this
.y=
b.getY
(
)-
a.getY
(
);
this
.z=
b.getZ
(
)-
a.getZ
(
);
this
.angle=
0
;
}
public
Vector3d
(
Point3d a) {
this
.x=
a.getX
(
);
this
.y=
a.getY
(
);
this
.z=
a.getZ
(
);
this
.angle=
0
;
}
public
final
void
init
(
) {
this
.setX
(
0
);
this
.setY
(
0
);
this
.setZ
(
0
);
this
.setAngle
(
0
);
}
}
public
void
normalize
(
) {
double
norm=
Math.sqrt
(
getX
(
)*
getX
(
)+
getY
(
)*
getY
(
)+
getZ
(
)*
getZ
(
));
if
(
norm>
0
) {
this
.setX
(
this
.getX
(
) /
norm);
this
.setY
(
this
.getY
(
) /
norm);
this
.setZ
(
this
.getZ
(
) /
norm);
}
}
public
double
getX
(
) {
return
x;
}
public
void
setX
(
double
x) {
this
.x =
x;
}
public
double
getY
(
) {
return
y;
}
public
void
setY
(
double
y) {
this
.y =
y;
}
public
double
getZ
(
) {
return
z;
}
public
void
setZ
(
double
z) {
this
.z =
z;
}
public
double
getAngle
(
) {
return
angle;
}
public
void
setAngle
(
double
angle) {
this
.angle =
angle;
}
public
double
getComponent
(
int
i) {
if
((
i>=
0
)&&(
i<
3
)) {
if
(
i==
0
) {
return
this
.getX
(
);
}
else
if
(
i==
1
) {
return
this
.getY
(
);
}
else
{
return
this
.getZ
(
);
}
}
else
{
return
0
;
}
}
public
void
setComponent
(
int
i,double
val) {
if
(
i==
0
) {
this
.setX
(
val);
}
else
if
(
i==
1
) {
this
.setY
(
val);
}
else
if
(
i==
2
) {
this
.setZ
(
val);
}
}
public
void
add
(
Vector3d vect) {
this
.setX
(
this
.getX
(
) +
vect.getX
(
));
this
.setY
(
this
.getY
(
) +
vect.getY
(
));
this
.setZ
(
this
.getZ
(
) +
vect.getZ
(
));
this
.setAngle
(
this
.getAngle
(
) +
vect.getAngle
(
));
}
public
void
add
(
double
x,double
y,double
z) {
this
.setX
(
this
.getX
(
) +
x);
this
.setY
(
this
.getY
(
) +
y);
this
.setZ
(
this
.getZ
(
) +
z);
}
public
void
mult
(
double
value) {
this
.x=
this
.x*
value;
this
.y=
this
.y*
value;
this
.z=
this
.z*
value;
}
public
static
double
scalarProduct
(
Vector3d a, Vector3d b) {
return
a.x*
b.x+
a.y*
b.y+
a.z*
b.z;
}
public
static
Vector3d crossProduct
(
Vector3d a, Vector3d b) {
Vector3d temp=
new
Vector3d
(
);
temp.setX
(
a.getY
(
) *
b.getZ
(
) -
a.getZ
(
) *
b.getY
(
));
temp.setY
(
a.getZ
(
) *
b.getX
(
) -
a.getX
(
) *
b.getZ
(
));
temp.setZ
(
a.getX
(
) *
b.getY
(
) -
a.getY
(
) *
b.getX
(
));
temp.setAngle
(
0
);
return
temp;
}
public
static
Vector3d getCombination
(
Vector3d vectA,double
valA,Vector3d vectB,double
valB) {
double
compX=
vectA.x*
valA+
vectB.x*
valB;
double
compY=
vectA.y*
valA+
vectB.y*
valB;
double
compZ=
vectA.z*
valA+
vectB.z*
valB;
return
new
Vector3d
(
compX, compY, compZ);
}
@Override
public
String toString
(
) {
return
this
.getX
(
)+
", "
+
this
.getY
(
)+
", "
+
this
.getZ
(
)+
", "
+
this
.getAngle
(
);
}
public
Vector3d copy
(
) {
return
new
Vector3d
(
this
.x, this
.y, this
.z);
}
2-D. La classe Matrix3▲
Un objet Matrix3 est représenté par un tableau à deux dimensions, sur lequel les méthodes de la classe vont travailler.
Les différents constructeurs permettent de définir la matrice de la manière la plus appropriée :
- constructeur par défaut (matrice nulle) ;
- constructeur par passage de tous les éléments individuellement ;
- constructeur par tableau de valeurs ;
- constructeur par passage de trois vecteurs (matrice de changement de repère) ;
- constructeur par passage d'un buffer OpenGL.
Ces différents constructeurs sont présentés dans le code ci-après :
public
class
Matrix3 {
private
static
int
SIZE=
3
;
private
double
[][] coef =
new
double
[3
][3
];
public
Matrix3
(
) {
this
.init
(
);
}
public
Matrix3
(
double
m11, double
m12, double
m13,
double
m21, double
m22, double
m23,
double
m31, double
m32, double
m33) {
this
.coef[0
][0
] =
m11;
this
.coef[1
][0
] =
m12;
this
.coef[2
][0
] =
m13;
this
.coef[0
][1
] =
m21;
this
.coef[1
][1
] =
m22;
this
.coef[2
][1
] =
m23;
this
.coef[0
][2
] =
m31;
this
.coef[1
][2
] =
m32;
this
.coef[2
][2
] =
m33;
}
public
Matrix3
(
double
[][] values) {
this
.coef =
values;
}
public
Matrix3
(
Vector3d v1,Vector3d v2,Vector3d v3) {
this
.coef[0
][0
] =
v1.getX
(
);
this
.coef[1
][0
] =
v1.getY
(
);
this
.coef[2
][0
] =
v1.getZ
(
);
this
.coef[0
][1
] =
v2.getX
(
);
this
.coef[1
][1
] =
v2.getY
(
);
this
.coef[2
][1
] =
v2.getZ
(
);
this
.coef[0
][2
] =
v3.getX
(
);
this
.coef[1
][2
] =
v3.getY
(
);
this
.coef[2
][2
] =
v3.getZ
(
);
}
public
Matrix3
(
FloatBuffer buff) {
if
(
buff.array
(
).length !=
(
SIZE*
SIZE)) {
this
.init
(
);
}
else
{
for
(
int
i =
0
; i <
SIZE; i++
) {
for
(
int
j =
0
; j <
SIZE; j++
) {
this
.coef[j][i] =
buff.get
(
i *
SIZE +
j);
}
}
}
}
public
final
void
init
(
) {
for
(
int
i =
0
; i <
SIZE; i++
) {
for
(
int
j =
0
; j <
SIZE; j++
) {
this
.coef[i][j] =
0
;
}
}
}
}
Les opérations de base sont définies dans les méthodes suivantes :
- récupérer toutes les valeurs ou une seule ;
- calculer la trace de la matrice ;
- initialiser la matrice à la matrice identité ;
- multiplication Matrix3 x Matrix3 ;
- multiplication Matrix3 x Vector3d.
public
double
[][] getValues
(
) {
return
this
.coef;
}
public
double
getValue
(
int
row,int
column) {
return
this
.coef[row][column];
}
public
double
getTrace
(
) {
return
this
.coef[0
][0
]+
this
.coef[1
][1
]+
this
.coef[2
][2
];
}
public
void
setIdentity
(
) {
this
.init
(
);
for
(
int
i =
0
; i <
SIZE; i++
) {
this
.coef[i][i] =
1
;
}
}
public
Matrix3 mult
(
Matrix3 mult) {
Matrix3 temp =
new
Matrix3
(
);
temp.init
(
);
for
(
int
i =
0
; i <
SIZE; i++
) {
for
(
int
j =
0
; j <
SIZE; j++
) {
double
som =
0
;
for
(
int
k =
0
; k <
SIZE; k++
) {
som =
som +
this
.coef[i][k] *
mult.coef[k][j];
}
temp.coef[i][j] =
som;
}
}
return
temp;
}
public
Vector3d mult
(
Vector3d vect) {
Vector3d temp =
new
Vector3d
(
);
temp.init
(
);
for
(
int
i =
0
; i <
SIZE; i++
) {
double
som =
0
;
for
(
int
k =
0
; k <
SIZE; k++
) {
som =
som +
this
.coef[i][k] *
vect.getComponent
(
k);
}
temp.setComponent
(
i, som);
}
return
temp;
}
Enfin, les méthodes de conversion permettent d'obtenir l'équivalent en String et FloatBuffer de notre matrice :
@Override
public
String toString
(
) {
StringBuilder str=
new
StringBuilder
(
);
for
(
int
i =
0
; i <
SIZE; i++
) {
for
(
int
j =
0
; j <
SIZE; j++
) {
str.append
(
this
.coef[i][j]).append
(
" "
);
}
str.append
(
" | "
);
}
return
str.toString
(
);
}
public
FloatBuffer toFloatBuffer
(
) {
FloatBuffer temp =
FloatBuffer.allocate
(
SIZE*
SIZE);
temp.put
(
0
, (
float
) this
.coef[0
][0
]);
temp.put
(
1
, (
float
) this
.coef[1
][0
]);
temp.put
(
2
, (
float
) this
.coef[2
][0
]);
temp.put
(
3
, (
float
) this
.coef[0
][1
]);
temp.put
(
4
, (
float
) this
.coef[1
][1
]);
temp.put
(
5
, (
float
) this
.coef[2
][1
]);
temp.put
(
6
, (
float
) this
.coef[0
][2
]);
temp.put
(
7
, (
float
) this
.coef[1
][2
]);
temp.put
(
8
, (
float
) this
.coef[2
][2
]);
return
temp;
}
2-E. La classe Matrix4▲
La classe Matrix4 est en tous points similaire à la classe Matrix3 (à l'adaptation près à une dimension supplémentaire).
Trois méthodes d'initialisation de matrice sont cependant disponibles dans cette classe. Elles permettent d'obtenir les matrices de translation et d'échelle directement d'après les paramètres fournis.
Attention, ces trois méthodes sont statiques.
public
static
Matrix4 getTranslationMatrix
(
Vector3d vect) {
Matrix4 mat =
new
Matrix4
(
);
mat.setIdentity
(
);
for
(
int
i =
0
; i <
3
; i++
) {
mat.coef[i][3
] =
vect.getComponent
(
i);
}
return
mat;
}
public
static
Matrix4 getTranslationMatrix
(
Vector3d vect, double
factor) {
Matrix4 mat =
new
Matrix4
(
);
mat.setIdentity
(
);
for
(
int
i =
0
; i <
3
; i++
) {
mat.coef[i][3
] =
vect.getComponent
(
i) *
factor;
}
return
mat;
}
public
static
Matrix4 getScaleMatrix
(
double
s) {
Matrix4 mat =
new
Matrix4
(
);
for
(
int
i =
0
; i <
3
; i++
) {
mat.coef[i][i] =
s;
}
mat.coef[3
][3
] =
1
;
return
mat;
}
2-F. Références▲
Vous trouverez d'excellentes informations dans la FAQ Mathématiques pour les Jeux du site Développez.com, au chapitre Matrices : FAQ Développez.com - Mathématiques pour les Jeux, MatricesFAQ Développez.com - Mathématiques pour les Jeux, Matrices
3. Le quaternion▲
3-A. Utilité et utilisation▲
Le quaternion, pour le néophyte, est un outil magique très compliqué qu'il ne pourra jamais ni comprendre ni utiliser. C'est pourquoi, en général, il se retrouve avec des équations complexes utilisant les angles d'Euler, des cosinus et des sinus, donnant un résultat plus ou moins correct au final.
Il est vraiment dommage de se priver de l'utilisation du quaternion sous prétexte que c'est "trop compliqué". Non, c'est très simple, et couplé avec l'utilisation des matrices, cela résout la rotation de caméra mobile de manière très rapide.
Je parle bien de rotation, et pas de translation, de la caméra. Si on regarde la définition du quaternion dans une encyclopédie, une de ses utilisations consiste en la représentation d'une rotation dans l'espace. Plutôt que d'utiliser trois rotations, autour de trois axes, dans un ordre bien défini, comme utilisé avec les angles d'Euler, le quaternion résume la transformation en une seule et unique rotation autour d'un axe défini par un vecteur (d'où la présence d'une propriété angle dans notre classe Vector3d). Simple non ?
Un quaternion est défini par quatre valeurs. Trois de ces valeurs peuvent être interprétées comme les composantes du vecteur définissant l'axe de la rotation, la quatrième définissant l'angle de la rotation (en fait, le cosinus de la moitié de l'angle).
Le quaternion représentant une rotation, il peut être converti en une matrice de rotation 3x3. L'équivalence quaternion/matrice nous amène aux mêmes opérations sur le quaternion que sur les matrices :
- conjugué ;
- inverse ;
- normalisation ;
- multiplication,
auxquelles s'ajoutent les opérations de conversion quaternion/matrice, et également l'opération d'interpolation entre deux quaternions.
3-B. La classe Quaternion▲
La classe Quaternion définit notre objet quaternion ainsi que les principales opérations et transformations qui seront utilisées dans PGE.
public
class
Quaternion {
private
double
x;
private
double
y;
private
double
z;
private
double
w;
public
Quaternion
(
) {
this
.init
(
);
}
public
Quaternion
(
double
x, double
y, double
z, double
w) {
this
.x =
x;
this
.y =
y;
this
.z =
z;
this
.w =
w;
}
public
Quaternion
(
Matrix3 mat) {
double
t=
mat.getTrace
(
)+
1
;
double
s;
if
(
t>
1e-5
) {
s=
1
/(
2
*
Math.sqrt
(
t));
this
.x=(
mat.getValue
(
2
, 1
)-
mat.getValue
(
1
, 2
))*
s;
this
.y=(
mat.getValue
(
0
, 2
)-
mat.getValue
(
2
, 0
))*
s;
this
.z=(
mat.getValue
(
1
, 0
)-
mat.getValue
(
0
, 1
))*
s;
this
.w=
1
/(
4
*
s);
this
.normalize
(
);
}
else
if
(
Math.abs
(
t)<=
1e-5
) {
double
m00=
mat.getValue
(
0
, 0
);
double
m11=
mat.getValue
(
1
, 1
);
double
m22=
mat.getValue
(
2
, 2
);
double
m01=
mat.getValue
(
0
, 1
);
double
m10=
mat.getValue
(
1
, 0
);
double
m02=
mat.getValue
(
0
, 2
);
double
m20=
mat.getValue
(
2
, 0
);
double
m12=
mat.getValue
(
1
, 2
);
double
m21=
mat.getValue
(
2
, 1
);
double
max=
Math.max
(
m00, Math.max
(
m11, m22));
if
(
Math.abs
(
max-
m00)<
1e-5
) {
s=
2
*
Math.sqrt
(
1
+
m00-
m11-
m22);
this
.x=
s/
4
;
this
.y=(
m01+
m10)/
s;
this
.z=(
m02+
m20)/
s;
this
.w=
0
;
this
.normalize
(
);
}
else
if
(
Math.abs
(
max-
m11)<
1e-5
) {
s=
2
*
Math.sqrt
(
1
-
m00+
m11-
m22);
this
.x=(
m01+
m10)/
s;
this
.y=
s/
4
;
this
.z=(
m12+
m21)/
s;
this
.w=
0
;
this
.normalize
(
);
}
else
if
(
Math.abs
(
max-
m22)<
1e-5
) {
s=
2
*
Math.sqrt
(
1
-
m00-
m11+
m22);
this
.x=(
m02+
m20)/
s;
this
.y=(
m12+
m21)/
s;
this
.z=
s/
4
;
this
.w=
0
;
this
.normalize
(
);
}
}
else
{
this
.init
(
);
System.out.println
(
"erreur d'initialisation du quaternion !"
);
}
}
public
final
void
init
(
) {
this
.x =
1
;
this
.y =
1
;
this
.z =
1
;
this
.w =
1
;
this
.normalize
(
);
}
@Override
public
String toString
(
) {
return
this
.x+
" / "
+
this
.y+
" / "
+
this
.z+
" / "
+
this
.w;
}
}
public
final
void
normalize
(
) {
double
norm =
Math.sqrt
(
x *
x +
y *
y +
z *
z+
w*
w);
if
(
norm >
0
) {
this
.x =
this
.x /
norm;
this
.y =
this
.y /
norm;
this
.z =
this
.z /
norm;
this
.w =
this
.w /
norm;
}
}
public
Quaternion getConjugate
(
) {
return
new
Quaternion
(-
x, -
y, -
z, w);
}
public
Quaternion mult
(
Quaternion quat) {
Quaternion result=
new
Quaternion
(
w *
quat.x +
x *
quat.w +
y *
quat.z -
z *
quat.y,
w *
quat.y +
y *
quat.w +
z *
quat.x -
x *
quat.z,
w *
quat.z +
z *
quat.w +
x *
quat.y -
y *
quat.x,
w *
quat.w -
x *
quat.x -
y *
quat.y -
z *
quat.z);
result.normalize
(
);
return
result;
}
public
Vector3d mult
(
Vector3d vec) {
Vector3d vn =
new
Vector3d
(
vec);
vn.normalize
(
);
Quaternion vecQuat =
new
Quaternion
(
);
Quaternion resQuat;
vecQuat.x =
vn.getX
(
);
vecQuat.y =
vn.getY
(
);
vecQuat.z =
vn.getZ
(
);
vecQuat.w =
0.0
f;
resQuat =
vecQuat.getConjugate
(
);
resQuat =
this
.mult
(
resQuat);
return
new
Vector3d
(
resQuat.x, resQuat.y, resQuat.z);
}
public
void
FromAxis
(
Vector3d v, double
angle) {
double
sinAngle;
angle *=
0.5
f;
Vector3d vn =
new
Vector3d
(
v);
vn.normalize
(
);
sinAngle =
Math.sin
(
angle);
this
.x =
(
vn.getX
(
) *
sinAngle);
this
.y =
(
vn.getY
(
) *
sinAngle);
this
.z =
(
vn.getZ
(
) *
sinAngle);
this
.w =
Math.cos
(
angle);
this
.normalize
(
);
}
public
Vector3d getAxisAngle
(
) {
Vector3d axis =
new
Vector3d
(
);
double
scale =
Math.sqrt
(
x *
x +
y *
y +
z *
z);
axis.setX
(
x /
scale);
axis.setY
(
y /
scale);
axis.setZ
(
z /
scale);
axis.setAngle
(
Math.acos
(
w) *
2.0
);
return
axis;
}
public
void
FromEuler
(
double
pitch, double
yaw, double
roll) {
double
PIOVER180 =
Math.PI /
180.0
;
double
pdeg =
pitch *
PIOVER180 /
2.0
;
double
ydeg =
yaw *
PIOVER180 /
2.0
;
double
rdeg =
roll *
PIOVER180 /
2.0
;
double
sinp =
Math.sin
(
pdeg);
double
siny =
Math.sin
(
ydeg);
double
sinr =
Math.sin
(
rdeg);
double
cosp =
Math.cos
(
pdeg);
double
cosy =
Math.cos
(
ydeg);
double
cosr =
Math.cos
(
rdeg);
this
.x =
sinr *
cosp *
cosy -
cosr *
sinp *
siny;
this
.y =
cosr *
sinp *
cosy +
sinr *
cosp *
siny;
this
.z =
cosr *
cosp *
siny -
sinr *
sinp *
cosy;
this
.w =
cosr *
cosp *
cosy +
sinr *
sinp *
siny;
this
.normalize
(
);
}
public
Matrix4 getMatrix
(
) {
double
x2 =
x *
x;
double
y2 =
y *
y;
double
z2 =
z *
z;
double
xy =
x *
y;
double
xz =
x *
z;
double
yz =
y *
z;
double
wx =
w *
x;
double
wy =
w *
y;
double
wz =
w *
z;
return
new
Matrix4
(
1.0
f -
2.0
f *
(
y2 +
z2), 2.0
f *
(
xy -
wz), 2.0
f *
(
xz +
wy), 0.0
f,
2.0
f *
(
xy +
wz), 1.0
f -
2.0
f *
(
x2 +
z2), 2.0
f *
(
yz -
wx), 0.0
f,
2.0
f *
(
xz -
wy), 2.0
f *
(
yz +
wx), 1.0
f -
2.0
f *
(
x2 +
y2), 0.0
f,
0.0
f, 0.0
f, 0.0
f, 1.0
f);
}
public
Matrix4 getMatrixInv
(
) {
Quaternion temp=
new
Quaternion
(
x, y, z, -
w);
return
temp.getMatrix
(
);
}
public
Matrix3 getRotMatrix
(
) {
double
x2 =
x *
x;
double
y2 =
y *
y;
double
z2 =
z *
z;
double
xy =
x *
y;
double
xz =
x *
z;
double
yz =
y *
z;
double
wx =
w *
x;
double
wy =
w *
y;
double
wz =
w *
z;
return
new
Matrix3
(
1.0
f -
2.0
f *
(
y2 +
z2), 2.0
f *
(
xy -
wz), 2.0
f *
(
xz +
wy),
2.0
f *
(
xy +
wz), 1.0
f -
2.0
f *
(
x2 +
z2), 2.0
f *
(
yz -
wx),
2.0
f *
(
xz -
wy), 2.0
f *
(
yz +
wx), 1.0
f -
2.0
f *
(
x2 +
y2));
}
public
Matrix3 getRotMatrixInv
(
) {
Quaternion quatInv=
new
Quaternion
(
x, y, z, -
w);
return
quatInv.getRotMatrix
(
);
}
3-C. Références▲
4. La caméra▲
4-A. Description de la caméra▲
Maintenant nous avons tous les outils pour définir notre objet Camera.
Le but est de supprimer l'appel à gluLookAt, et de le remplacer par la redéfinition de la matrice MODELVIEW. Mais comment calculer cette matrice ?
Il nous faut tout d'abord définir le comportement de notre caméra. Pour le logiciel, nous avons besoin des fonctions suivantes :
- translation (x et y écran) ;
- zoom ;
- rotation.
en notant que le zoom peut être vu comme une translation suivant l'axe z de l'écran. La rotation se fera par rapport à un point de l'espace prédéfini auparavant par l'utilisateur, et ne sera pas obligatoirement sur l'axe de visée de la caméra.
Nous allons donc avoir besoin de données suivantes pour définir notre caméra :
- un point de positionnement de la caméra ;
- un point de visée ;
- un vecteur pour orienter la caméra (nous indiquerons où se trouve le haut de la caméra) ;
- un centre de rotation de notre scène 3D.
Pour définir la matrice que nous allons passer à OpenGL, il nous faut déterminer les transformations à appliquer pour aboutir à la vision que l'on veut avoir avec la caméra. Dans notre cas ici, elles sont définies par l'enchainement suivant :
- initialisation : par défaut, avec la matrice identité, la caméra est positionnée sur l'origine du monde 3D, plan de la caméra parallèle au plan XY, vecteur haut orienté suivant Y, l'axe Z sortant de l'écran ;
- translation jusqu'au centre de rotation de la scène ;
- rotation afin d'aligner la vue de la caméra sur le point de visée, et orienter le haut de la caméra tel que souhaité ;
- translation jusqu'au point de positionnement de la caméra.
Il est très important de bien respecter l'ordre des transformations à appliquer, sans quoi la transformation finale ne sera pas correcte. En effet, les transformations sont représentées par des matrices, l'enchainement des transformations se fait en les multipliant entre elles, et le produit matriciel n'est (de manière générale) pas commutatif (c'est-à-dire qu'une translation suivie d'une rotation ne donne pas le même résultat que la même rotation suivie de la même translation).
Les transformations seront représentées par des vecteurs 3D pour les translations, et par un quaternion pour la rotation. Les opérations de conversion des classes Matrix4 et Quaternion permettront d'obtenir les matrices 4x4 correspondantes, que nous multiplierons suivant l'ordre défini ci-dessus afin d'obtenir la matrice complète MODELVIEW.
4-B. La classe Camera▲
Le constructeur de la classe Camera prend en paramètres les trois points et le vecteur définis ci-dessus. Ils sont utilisés afin de calculer la matrice de changement de repère permettant de déterminer le quaternion représentant la rotation de la caméra, ainsi que les deux vecteurs de translation (origine/centre de rotation, et centre de rotation/position caméra)
Attention ! Le vecteur de translation entre le centre de rotation et la position de la caméra est utilisé APRÈS l'application de la rotation de la scène 3D. Ses coordonnées ne sont donc pas celles obtenues directement à partir des coordonnées des points fournis en paramètres, mais doivent être transformées par application de la matrice de rotation afin de correspondre au nouveau repère.
public
class
Camera {
private
Quaternion quatRot;
private
Vector3d transVect;
private
Vector3d transCDR;
private
Point3d location;
private
Point3d target;
private
Point3d rotCenter;
private
Vector3d upVector;
public
Camera
(
Point3d loc, Point3d targ, Vector3d up, Point3d rotCenter) {
this
.location =
loc;
this
.target =
targ;
this
.upVector =
up;
this
.rotCenter =
rotCenter;
this
.updateQuaternion
(
);
this
.updateTranslations
(
);
}
public
final
void
updateQuaternion
(
) {
Vector3d y1 =
this
.upVector;
Vector3d z1 =
new
Vector3d
(
this
.target, this
.location);
z1.normalize
(
);
Vector3d x1 =
Vector3d.crossProduct
(
y1, z1);
x1.normalize
(
);
Vector3d y2 =
Vector3d.crossProduct
(
z1, x1);
this
.quatRot =
new
Quaternion
(
new
Matrix3
(
x1, y2, z1));
}
public
final
void
updateTranslations
(
) {
Point3d origin =
new
Point3d
(
0
, 0
, 0
);
Vector3d transVectInit =
new
Vector3d
(
this
.location, this
.rotCenter);
transVectInit =
this
.quatRot.getRotMatrix
(
).mult
(
transVectInit);
this
.setTranslation
(
transVectInit);
this
.setTranslationToRotCenter
(
new
Vector3d
(
this
.rotCenter, origin));
}
public
void
setTranslation
(
Vector3d vect) {
this
.transVect =
vect;
}
public
void
setTranslation
(
double
x, double
y, double
z) {
this
.transVect =
new
Vector3d
(
x, y, z);
}
public
void
setTranslationToRotCenter
(
Vector3d vect) {
this
.transCDR =
vect;
}
public
void
setTranslationToRotCenter
(
double
x, double
y, double
z) {
this
.transCDR =
new
Vector3d
(
x, y, z);
}
}
La matrice de la caméra est obtenue en multipliant les matrices de translation et de rotation suivant l'ordre défini au paragraphe précédent (on multiplie à gauche pour le produit matriciel) :
public
FloatBuffer getCameraMatrix
(
) {
Matrix4 matCDR =
Matrix4.getTranslationMatrix
(
transCDR);
Matrix4 matRot =
this
.quatRot.getMatrixInv
(
);
Matrix4 matTrans =
Matrix4.getTranslationMatrix
(
this
.transVect);
return
matTrans.mult
(
matRot).mult
(
matCDR).toFloatBuffer
(
);
}
Quelques opérations élémentaires sur la caméra sont définies, telles que les rotations suivant les axes X et Y caméra, et les trois translations suivant les trois axes. Les rotations sont définies en multipliant le quaternion courant de la caméra par un quaternion correspondant à la rotation souhaitée. Pour les translations, on ajoute au vecteur translation de la caméra le vecteur correspondant à la translation souhaitée.
On remarquera à ce stade que toutes les opérations sur le déplacement de la caméra se font en référence aux axes caméra : X orienté suivant la droite de la caméra, Y vers le haut, et Z sortant de l'écran.
public
void
rotateX
(
double
angle) {
Quaternion rot =
new
Quaternion
(
);
rot.FromAxis
(
new
Vector3d
(
1
, 0
, 0
), angle /
180.0
*
Math.PI);
this
.quatRot =
this
.quatRot.mult
(
rot);
this
.quatRot.normalize
(
);
}
public
void
rotateY
(
double
angle) {
Quaternion rot =
new
Quaternion
(
);
rot.FromAxis
(
new
Vector3d
(
0
, 1
, 0
), angle *
Math.PI /
180.0
);
this
.quatRot =
this
.quatRot.mult
(
rot);
this
.quatRot.normalize
(
);
}
public
void
translateX
(
double
tx) {
this
.transVect.add
(
tx, 0
, 0
);
}
public
void
translateX
(
) {
this
.transVect.add
(
0
, 1
, 0
);
}
public
void
translateY
(
double
ty) {
this
.transVect.add
(
0
, ty, 0
);
}
public
void
translateY
(
) {
this
.transVect.add
(
0
, 0
, 1
);
}
public
void
translateZ
(
double
tz) {
this
.transVect.add
(
0
, 0
, tz);
}
5. Manipulation de la caméra à la souris▲
5-A. Définition de la classe MouseCameraManager▲
La classe MouseCameraManager est chargée de gérer tous les évènements liés à la souris. Elle implémente donc les interfaces MouseListener et MouseMotionListener. Elle gère ainsi les translations, rotations et zoom de la caméra dans le monde 3D.
Parmi les propriétés principales de la classe, on trouve :
- panel : le panneau PGE qui déclenchera les évènements ;
- camera : la caméra associée au panneau PGE ;
- mode : le mode d'interaction avec le monde 3D (translation, rotation ou zoom, défini par propriétés de classe déclarées en static).
Le constructeur prend uniquement le panneau PGE en paramètre, l'initialisation des autres propriétés de l'objet étant effectuée directement en utilisant ce panneau PGE dans la méthode updateData via l'appel de méthodes publiques de la classe PGEPanel.
public
class
MouseCameraManager implements
MouseListener, MouseMotionListener {
private
PgePanel panel;
private
Camera camera;
private
double
width;
private
double
height;
private
double
xOld, yOld;
private
double
xNew, yNew;
private
int
mode;
static
int
DRAG=
1
;
static
int
ROTATE=
2
;
static
int
ZOOM=
3
;
public
MouseCameraManager
(
PgePanel pan) {
this
.panel=
pan;
this
.updateData
(
);
}
public
final
void
updateData
(
) {
this
.camera=
panel.getCamera
(
);
this
.width=
panel.getWidth
(
);
this
.height=
panel.getHeight
(
);
}
}
5-B. Détermination du mode d'interaction▲
L'appui sur un des boutons de la souris sélectionnera automatiquement le mode d'interaction (mémorisé par la propriété mode de l'objet) sur le monde 3D de la manière suivante :
- bouton de gauche : rotation ;
- bouton du milieu : zoom ;
- bouton de droite : translation.
Lors de la détection de l'appui, on mémorise la position actuelle du curseur dans la fenêtre. Cela définit le point de départ de la souris qui sera utilisé dans le calcul des paramètres de transformation.
@Override
public
void
mousePressed
(
MouseEvent e) {
this
.panel.requestFocus
(
);
xOld=
e.getX
(
);
yOld=
e.getY
(
);
int
button=
e.getButton
(
);
if
(
button==
MouseEvent.BUTTON1) {
this
.mode=
MouseCameraManager.ROTATE;
}
else
if
(
button==
MouseEvent.BUTTON2) {
this
.mode=
MouseCameraManager.ZOOM;
}
else
if
(
button==
MouseEvent.BUTTON3) {
this
.mode=
MouseCameraManager.DRAG;
}
}
5-C. Gestion du déplacement actif de la souris▲
L'évènement mouseDragged, appelé lors du déplacement de la souris dans la fenêtre avec appui sur un des boutons, permet de gérer la transformation appliquée sur le monde 3D.
Pour cela, la position actuelle du curseur est récupérée et est ensuite utilisée pour définir les paramètres de la transformation. Ceux-ci sont ici calculés comme étant des facteurs de la distance entre la position actuelle du curseur (xNew/yNew) et sa position de départ (xOld/yOld), initialisée précédemment lors de l'appui sur l'un des boutons.
En fonction du mode d'interaction, on appelle les méthodes de translation ou de rotation de la caméra :
- déplacement en x du curseur, mode TRANSLATION : translation de la caméra suivant x ;
- déplacement en y du curseur, mode TRANSLATION : translation de la caméra suivant y ;
- déplacement en x du curseur, mode ROTATION : rotation de la caméra autour de l'axe Oy ;
- déplacement en y du curseur, mode ROTATION : rotation de la caméra autour de l'axe Ox ;
- déplacement positif en x ou y du curseur, mode ZOOM : translation positive de la caméra suivant Oz ;
- déplacement négatif en x ou y du curseur, mode ZOOM : translation négative de la caméra suivant Oz.
@Override
public
void
mouseDragged
(
MouseEvent e) {
xNew=
e.getX
(
);
yNew=
e.getY
(
);
if
(
this
.mode==
MouseCameraManager.DRAG) {
this
.camera.translateX
((
xNew-
xOld)/
this
.width*
10
);
this
.camera.translateY
(-(
yNew-
yOld)/
this
.height*
10
);
}
else
if
(
this
.mode==
MouseCameraManager.ROTATE) {
this
.camera.rotateY
(-(
xNew-
xOld)/
this
.width*
360
);
this
.camera.rotateX
(-(
yNew-
yOld)/
this
.height*
360
);
}
else
if
(
this
.mode==
MouseCameraManager.ZOOM) {
double
deltaX=(
xNew-
xOld)/
this
.width*
10
;
double
deltaY=(
yNew-
yOld)/
this
.height*
10
;
if
(
Math.abs
(
deltaX)>
Math.abs
(
deltaY)) {
this
.camera.translateZ
(
deltaX);
}
else
{
this
.camera.translateZ
(
deltaY);
}
}
this
.panel.display
(
);
xOld=
xNew;
yOld=
yNew;
}
6. Intégration de la caméra dans le panneau 3D de PGE▲
Comme expliqué dans le chapitre 4, la classe Camera est chargée de fournir la matrice MODELVIEW à appliquer afin d'avoir la vision souhaitée sur la scène 3D.
Cela se fait de manière très simple, en remplaçant l'appel à gluLookAt par une création d'un objet Camera suivant les mêmes paramètres, et un appel à glLoadMatrixf pour charger la matrice de la caméra dans la matrice MODELVIEW.
À partir de la classe PGEPanel de l'article précédent, on définit tout d'abord deux propriétés supplémentaires : un MouseCameraManager, afin de gérer les évènements souris, et une Camera pour définir la caméra associée au PGEPanel.
private
MouseCameraManager mcm; // Listener souris
private
Camera camera =
null
; // camera associee au PGEPanel
La méthode initComponents est modifiée afin de connecter le MouseCameraManager au PGEPanel :
private
void
initComponents
(
) {
this
.addGLEventListener
(
this
);
// instanciation et connexion du MouseCameraManager
this
.mcm =
new
MouseCameraManager
(
this
);
this
.addMouseListener
(
this
.mcm);
this
.addMouseMotionListener
(
this
.mcm);
this
.setFocusable
(
true
);
this
.cube=
new
PGEUnitCube
(
0
, 0
, 0
);
this
.coord=
new
PGEGlobalCoord
(
);
}
La définition de la caméra se fait dans la méthode init. Cette méthode n'était pas utilisée dans le précédent article, mais ici elle va permettre d'initialiser la caméra. On garde les mêmes paramètres que dans le précédent article lors de l'appel à la méthode gluLookAt :
- position de la camera : point (3,5,2) ;
- point de visée : point (0,0,0) ;
- vecteur "up" : point (0,0,1).
Le centre de rotation de la scène est fixé arbitrairement à l'origine.
Les propriétés du MouseCameraManager sont mises à jour afin de lier les listeners à la caméra.
@Override
public
void
init
(
GLAutoDrawable arg0) {
// position de la camera : point (3,5,2)
// point de visée : point (0,0,0)
// vecteur "up" : point (0,0,1)
// centre de rotation de la scène : point (0,0,0)
Point3d loc =
new
Point3d
(
3
, 5
,2
);
Point3d targ =
new
Point3d
(
0
,0
, 0
);
Vector3d up =
new
Vector3d
(
0
, 0
, 1
);
Point3d rotCenter=
new
Point3d
(
0
, 0
, 0
);
if
(
this
.camera ==
null
) {
this
.camera =
new
Camera
(
loc, targ, up, rotCenter);
}
this
.mcm.updateData
(
);
}
Il ne reste plus qu'à modifier la méthode display, en remplaçant l'appel à gluLookAt par un appel à gLoadMatrixf, en passant en paramètre la matrice de la caméra.
Attention de bien vérifier que vous utilisez glLoadMatrixf en mode GL_MODELVIEW avec la matrice caméra.
@Override
public
void
display
(
GLAutoDrawable arg0) {
GL2 gl =
arg0.getGL
(
).getGL2
(
);
GLU glu =
new
GLU
(
);
gl.glViewport
(
0
, 0
, this
.winAW, this
.winAH);
// initialisation de la matrice de projection
gl.glMatrixMode
(
GL2.GL_PROJECTION);
gl.glLoadIdentity
(
);
// définition de la matrice de projection
// vue en perspective, angle 45 °, ratio largeur/hauteur, plan de clipping "near" et "far"
glu.gluPerspective
(
45
, (
double
) this
.winAW /
(
double
) this
.winAH, 0.1
, 100.0
);
// définition de la matrice MODELVIEW à partir de la matrice camera
gl.glMatrixMode
(
GL2.GL_MODELVIEW);
gl.glLoadMatrixf
(
this
.camera.getCameraMatrix
(
));
// initialisation de la matrice modèle
// couleur de fond : noir
gl.glClearColor
(
0.0
f, 0.0
f, 0.0
f, 0.0
f);
gl.glClear
(
GL2.GL_COLOR_BUFFER_BIT);
// dessin des axes du repère global
this
.cube.draw
(
gl);
// dessine un cube unitaire à l'origine du repère global
this
.coord.draw
(
gl);
}
Pour finir, on rajoute également une méthode getCamera qui est utilisée par la classe MouseCameraManager :
public
Camera getCamera
(
) {
return
this
.camera;
}
7. Conclusion▲
Nous pouvons maintenant définir une caméra adaptée à nos besoins de visualisation et naviguer interactivement dans notre monde 3D par l'intermédiaire de la souris.
Nous verrons dans le prochain article comment construire des objets plus ou moins complexes.
Les sources de cet article sont disponibles en suivant ce lien : src_PGE_article_2.zipTBD.
Vous pouvez également regarder la vidéo de démonstration sur Vimeovidéo de démonstration sur Vimeo pour voir le résultat de cet article.
8. Remerciements▲
Merci à ClaudeLELOUP pour sa relecture orthographique, ainsi qu'à djibril pour son aide sur le KitOOoDVP.