Pour animer notre cube nous allons simplement le faire tourner en utilisant ce que vous connaissez déjà par coeur :
la rotation !
Au niveau du dessin nous n'avons vraiment pas grand chose à changer, juste à faire tourner le repère
avant de dessiner le cube. Nous allons le faire tourner à la fois sur Z (la verticale) et X donc nous avons besoin de 2 variables globales* pour chacun des angles à contrôler.
* En général en programmation on essaye d'éviter les variables globales mais ici nous faisons en quelque sorte du prototypage pour apprendre et tester les concepts OpenGL, ce n'est donc vraiment pas bien grave.
Le code, simplifié, du programme devient alors :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 | #include <SDL/SDL.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <cstdlib>
void Dessiner();
double angleZ = 0;
double angleX = 0;
int main(int argc, char *argv[])
{
//le code du main
}
void Dessiner()
{
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glMatrixMode( GL_MODELVIEW );
glLoadIdentity( );
gluLookAt(3,4,2,0,0,0,0,0,1);
glRotated(angleZ,0,0,1);
glRotated(angleX,1,0,0);
//dessin du cube
glFlush();
SDL_GL_SwapBuffers();
}
|
En terme de code SDL nous voulons que ces angles soient modifiés automatiquement avec le temps. On ne peut donc plus se permettre d'
attendre les événements avec
SDL_WaitEvent et nous allons en conséquence utiliser
SDL_PollEvent pour récupérer les événements s'il y en a puis animer notre cube.
Nous modifions donc le code de notre boucle d'affichage pour incrémenter nos angles à chaque image :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | for (;;)
{
while (SDL_PollEvent(&event))
{
switch(event.type)
{
case SDL_QUIT:
exit(0);
break;
}
}
angleZ += 1;
angleX += 1;
Dessiner();
}
|
Gérer la vitesse d'animation
En testant le code actuel vous voyez que le cube tourne beaucoup trop vite, nous n'avons franchement rien le temps de voir.
Il faut donc introduire des vitesses de rotation. Ces vitesses ne doivent pas dépendre de l'ordinateur sur lequel le programme est lancé et donc doivent prendre en compte le temps réel.
Pour ce faire, à chaque image (chaque passage dans la boucle donne lieu à une image), il faut déterminer combien de temps il s'est passé depuis la dernière image et faire bouger le cube en conséquence.
Nous avons donc besoin de 3 variables :
- une pour garder en mémoire le temps qu'il était lors de la dernière image : last_time ;
- une pour avoir le temps de l'image actuelle : current_time ;
- une (par commodité) pour le temps écoulé : ellapsed_time.
Pour connaître le temps écoulé, en millisecondes, depuis le lancement de l'application nous utiliserons l'instruction SDL_GetTicks();
Le principe est alors le suivant.
- On initialise une première fois, avant de rentrer dans notre boucle d'affichage last_time avec le temps actuel.
- À chaque image on récupère le temps actuel dans current_time.
- On utilise la différence entre le temps actuel et le temps qu'il était lors de l'ancien passage pour savoir combien de temps s'est écoulé. On stocke le résultat dans ellapsed_time.
- On réalise nos mouvements en fonction du temps écoulé.
- On finit par affecter à last_time la valeur de current_time car nous passons à une nouvelle image et donc le présent devient du passé (
c'est beau !) .
En ce qui concerne l'unité de mesure des
vitesses, comme le temps écoulé est donné en
millisecondes et que les angles utilisés dans
glRotate sont en
degrés, il s'agit tout simplement de
degrés par milliseconde.
Dans notre cas j'utiliserai
0.05 °/ms.
La traduction en code du principe tout juste évoqué est la suivante :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 | Uint32 last_time = SDL_GetTicks();
Uint32 current_time,ellapsed_time;
for (;;)
{
while (SDL_PollEvent(&event))
{
switch(event.type)
{
case SDL_QUIT:
exit(0);
break;
}
}
current_time = SDL_GetTicks();
ellapsed_time = current_time - last_time;
last_time = current_time;
angleZ += 0.05 * ellapsed_time;
angleX += 0.05 * ellapsed_time;
Dessiner();
}
|
Ne pas monopoliser le CPU
Ça rame ! Le programme prend 100% du CPU rien que pour faire tourner un simple cube, je ne vais jamais pouvoir faire un jeu !
En effet si on regarde la charge du processeur imposée par notre application on voit qu'il est totalement occupé à gérer notre programme :
Cela ne veut pas dire que votre programme est lent c'est juste que nous bouclons en permanence et que nous ne prenons jamais de pause. En réalité nous n'avons pas vraiment besoin de boucler tout le temps. Nous allons utiliser
une technique possible (celle que je préfère et donc souhaite vous expliquer) :
limiter les FPS (frames per second - images par seconde).
En effet pour avoir une animation très fluide il nous suffit de 50 images par seconde.
En fixant le nombre d'images par secondes désirées par votre application, il est alors possible de la faire s'endormir un certain temps si elle va plus vite que nécessaire, ce qui soulagera (même s'il ne s'en plaint pas) le processeur.
Pour ce faire nous allons calculer à chaque image combien de temps nous avons mis pour la dessiner, si nous avons été plus rapides que le temps moyen nécessaire, nous stopperons l'exécution pour un certain temps.
Par exemple, autoriser 50 images par seconde donne à chaque image 20 millisecondes pour s'afficher. Imaginons qu'une image mette 5 ms à s'afficher réellement, il reste alors 15 ms à tuer. Plutôt que de passer directement à l'image suivante, nous allons
endormir l'application pendant ces 15 ms.
En terme de code nous allons utiliser
SDL_GetTicks comme auparavant pour déterminer le temps écoulé entre le début et la fin de la création de l'image. Nous utiliserons
SDL_Delay pour suspendre temporairement l'application.
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 | Uint32 start_time; //nouvelle variable
for (;;)
{
start_time = SDL_GetTicks();
while (SDL_PollEvent(&event))
{
switch(event.type)
{
case SDL_QUIT:
exit(0);
break;
case SDL_KEYDOWN:
animation = !animation;
break;
}
}
current_time = SDL_GetTicks();
ellapsed_time = current_time - last_time;
last_time = current_time;
angleZ += 0.05 * ellapsed_time;
angleX += 0.05 * ellapsed_time;
Dessiner();
ellapsed_time = SDL_GetTicks() - start_time;
if (ellapsed_time < 10)
{
SDL_Delay(10 - ellapsed_time);
}
}
return 0;
}
|
Notez qu'ici nous ne voulons pas savoir combien de temps il s'est passé depuis la dernière fois mais combien de temps notre image a pris à se dessiner (j'y ai inclus la gestion des événements). Il y a donc un appel à
SDL_GetTicks au début de notre boucle et un appel à la toute fin. Je réutilise
ellapsed_time par commodité mais pas les autres variables pour ne pas mélanger les 2 concepts : limitation des FPS et gestion du temps dans les animations.
Code final :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120 | #include <SDL/SDL.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <cstdlib>
void Dessiner();
double angleZ = 0;
double angleX = 0;
int main(int argc, char *argv[])
{
SDL_Event event;
SDL_Init(SDL_INIT_VIDEO);
atexit(SDL_Quit);
SDL_WM_SetCaption("SDL GL Application", NULL);
SDL_SetVideoMode(640, 480, 32, SDL_OPENGL);
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
gluPerspective(70,(double)640/480,1,1000);
glEnable(GL_DEPTH_TEST);
Dessiner();
Uint32 last_time = SDL_GetTicks();
Uint32 current_time,ellapsed_time;
Uint32 start_time;
for (;;)
{
start_time = SDL_GetTicks();
while (SDL_PollEvent(&event))
{
switch(event.type)
{
case SDL_QUIT:
exit(0);
break;
}
}
current_time = SDL_GetTicks();
ellapsed_time = current_time - last_time;
last_time = current_time;
angleZ += 0.05 * ellapsed_time;
angleX += 0.05 * ellapsed_time;
Dessiner();
ellapsed_time = SDL_GetTicks() - start_time;
if (ellapsed_time < 10)
{
SDL_Delay(10 - ellapsed_time);
}
}
return 0;
}
void Dessiner()
{
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glMatrixMode( GL_MODELVIEW );
glLoadIdentity( );
gluLookAt(3,4,2,0,0,0,0,0,1);
glRotated(angleZ,0,0,1);
glRotated(angleX,1,0,0);
glBegin(GL_QUADS);
glColor3ub(255,0,0); //face rouge
glVertex3d(1,1,1);
glVertex3d(1,1,-1);
glVertex3d(-1,1,-1);
glVertex3d(-1,1,1);
glColor3ub(0,255,0); //face verte
glVertex3d(1,-1,1);
glVertex3d(1,-1,-1);
glVertex3d(1,1,-1);
glVertex3d(1,1,1);
glColor3ub(0,0,255); //face bleue
glVertex3d(-1,-1,1);
glVertex3d(-1,-1,-1);
glVertex3d(1,-1,-1);
glVertex3d(1,-1,1);
glColor3ub(255,255,0); //face jaune
glVertex3d(-1,1,1);
glVertex3d(-1,1,-1);
glVertex3d(-1,-1,-1);
glVertex3d(-1,-1,1);
glColor3ub(0,255,255); //face cyan
glVertex3d(1,1,-1);
glVertex3d(1,-1,-1);
glVertex3d(-1,-1,-1);
glVertex3d(-1,1,-1);
glColor3ub(255,0,255); //face magenta
glVertex3d(1,-1,1);
glVertex3d(1,1,1);
glVertex3d(-1,1,1);
glVertex3d(-1,-1,1);
glEnd();
glFlush();
SDL_GL_SwapBuffers();
}
|
En comparaison, pour une animation de la même fluidité, la charge moyenne du processeur est
négligeable :
Notes diverses :
- il est aussi possible d'utiliser des timers pour limiter les FPS (voir la doc de SDL_AddTimer ainsi que son exemple). Ce n'est pas la solution retenue ici ;
- les adeptes de la doc ne manqueront pas de signaler le problème de la granularité du temps de pause (cf. SDL_Delay). Dans les tests effectués pour rédiger ce tutoriel, le temps de pause réel n'excédait jamais le temps demandé de plus de 1 milliseconde. Le nombre réel d'images par seconde est donc égal (ou très proche) au nombre fixé ;
- en terme d'ergonomie cela peut faire peur à certains de faire s'endormir le programme un certain temps. Qu'ils soient rassurés, même dans une application 3D bien plus complexe, la gestion des événements n'est en rien altérée.