PGE (Plegat Graphic Engine) - Définition des caméras et de leur manipulation

Jean-Michel BORLOT

Second article de la série de tutoriels sur la création de mon moteur graphique PGE (Plegat Graphic Engine).
Nous aborderons ici la définition d'une caméra permettant de définir la vue sur notre monde 3D, ainsi que la manière de la piloter par code et par interaction avec la souris.

Commentez

- Titre : PGE (Plegat Graphic Engine) - Définition des caméras et de leur manipulation

- Auteur : Jean-Michel BORLOT

- Parution : 3 décembre 2011

- Mise à jour : 15 septembre 2013

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

Sommaire

- 1. Comment voir ce que l'on dessine ?

- 2. Les opérations matricielles

- 2-A. Opérations de base

- 2-B. La classe Point3d

- 2-C. La classe Vector3d

- 2-D. La classe Matrix3

- 2-E. La classe Matrix4

- 2-F. Références

- 3. Le quaternion

- 3-A. Utilité et utilisation

- 3-B. La classe Quaternion

- 3-C. Références

- 4. La caméra

- 4-A. Description de la caméra

- 4-B. La classe Camera

- 5. Manipulation de la caméra à la souris

- 5-A. Définition de la classe MouseCameraManager

- 5-B. Détermination du mode d'interaction

- 5-C. Gestion du déplacement actif de la souris

- 6. Intégration de la caméra dans le panneau 3D de PGE

- 7. Conclusion

- 8. Remerciements

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.

Définition de la classe Point3d

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.

Définition de la classe Vector3d

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);
}

}

Normalisation d'un Vector3d

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);
}

}

Affectation et récupération des composantes

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);
}
}

Addition de Vector3d

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);
}

Mise à l'échelle d'un Vector3d

public void mult(double value) {
this.x=this.x*value;
this.y=this.y*value;
this.z=this.z*value;
}

Produits scalaire et vectoriel de Vector3d

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;
}

Combinaison linéaire de deux Vector3d

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);
}

Conversion en String

@Override
public String toString() {
return this.getX()+", "+this.getY()+", "+this.getZ()+", "+this.getAngle();
}

Clonage d'un Vector3d

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 :

Définition de la classe Matrix3

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.

Opérations de base sur objet Matrix3

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 :

Opérations de conversion de la classe Matrix3

@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.

Méthodes supplémentaires de la classe Matrix4

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, 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.

Définition de la classe Quaternion

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;
}

}

Normalisation et conjugué du quaternion

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);
}

Multiplication

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.0f;

resQuat = vecQuat.getConjugate();
resQuat = this.mult(resQuat);

return new Vector3d(resQuat.x, resQuat.y, resQuat.z);
}

Conversions quaternion/vecteur

public void FromAxis(Vector3d v, double angle) {

double sinAngle;
angle *= 0.5f;
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();
}

Conversions quaternion/matrice

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.0f - 2.0f * (y2 + z2), 2.0f * (xy - wz), 2.0f * (xz + wy), 0.0f,
2.0f * (xy + wz), 1.0f - 2.0f * (x2 + z2), 2.0f * (yz - wx), 0.0f,
2.0f * (xz - wy), 2.0f * (yz + wx), 1.0f - 2.0f * (x2 + y2), 0.0f,
0.0f, 0.0f, 0.0f, 1.0f);
}

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.0f - 2.0f * (y2 + z2), 2.0f * (xy - wz), 2.0f * (xz + wy),
2.0f * (xy + wz), 1.0f - 2.0f * (x2 + z2), 2.0f * (yz - wx),
2.0f * (xz - wy), 2.0f * (yz + wx), 1.0f - 2.0f * (x2 + y2));
}

public Matrix3 getRotMatrixInv() {

Quaternion quatInv=new Quaternion(x, y, z, -w);

return quatInv.getRotMatrix();
}

3-C - Références

FAQ Développez.com - Mathématiques pour les Jeux, Quaternions

The Game Programming Wiki

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.

Définition de la classe Camera

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) :

Méthode d'obtention de la matrice de la caméra

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.

Opérations élémentaires sur la caméra

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.

Définition de la classe MouseCameraManager

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.

Évènement mousePressed

@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.

Évènement mouseDragged

@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.

Déclaration du MouseCameraManager et de la Camera

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 :

Modification de la méthode initComponents

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.

Modification de la méthode init

@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.

Modification de la méthode display

@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.0f, 0.0f, 0.0f, 0.0f);
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.zip.

Vous pouvez également regarder la vidé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.

(1)PGE - Introduction

(2)Lien Wikipédia - Blocage de cardan ("Gimbal lock")