2d Chapitre 6 - Transformations 2D et sprites
CHAPITRE 6 Transformations 2D et sprites
Si vous n'avez jamais entendu ce mot auparavant, un sprite est tout simplement des données graphiques contenues dans la mémoire d'un ordinateur. Ces images sont parfois statiques, parfois en mouvement sur l'écran. La plupart du temps, ces images sont dessinés dans des programmes spécialisés, comme Paint Shop Pro, puis sauvegardé sous format graphiques tel GIF ou PCX. Le programme devra lire les informations sur le sprite sur le fichier afin d'initialiser le tableau qui contient le sprite. Que ce soit un Pac-Man ou un vaisseau intergalactique se déplaçant dans l'espace, ces graphiques sont contenues dans un tableau.
INITIALISATION DES SPRITES
Ce tableau, en plus de contenir les données graphiques proprement dites, devrait contenir sa hauteur, sa largeur, ses coordonnées cartésiennes et, si applicable, sa vitesse de déplacement. On pourrait déclarer un structure qui ressemblerait à ceci:
typedef struct tsprites
{
char *graphics; // Pointeur sur donnes graphiques
int x, y; // largeur et hauteur du sprite
int px,py; // position courante du sprite (optionnel)
int sx,sy; // vitesse courante su sprite (optionnel)
};
Les derniers paramètres sont optionnels, car si les sprites sont stationnaires, elles sont inutiles. Pour déclarer nos différents sprites, on utilise une variable (ou un tableau) de type tsprites:
tsprites sprite[3]; // Nos sprites
Dans cet exemple, le tableau de type tsprites contient de l'espace pour 3 sprites. Naturellement, ces sprites ne viennent pas de nulle part, elles ont été chargées dans un fichier graphique. Il serait également possible de faire un programme qui transforme les données brutes des sprites en tableau, et d’initialiser le sprite avec ce tableau, pour ainsi ne plus avoir besoin de charger sur disque les données. Donc, nous devons aller placer ces données graphiques, ou bitmap, dans le tableau pointé par « graphics ». Voici un exemple de code qui fait justement cela:
void liresprite(int x1,int y1,int largeur,int hauteur,tsprite *sprite)
{
sprite->x = width;
sprite->y = height;
sprite->graphics = new char[largeur*hauteur];
for (int y=0; y<hauteur; y++)
for (int x=0; x<largeur; x++)
sprite->graphics[(y*largeur)+ x] = bitmap[(y1+y)*BITMAP_X+x+x1];
}
La fonction liresprite reçoit comme paramètres les cordonnées x et y du bitmap (sur l'écran virtuel où il se situe). Ensuite, sa largeur et sa hauteur, et finalement 2 pointeurs : un sur l'écran d'où le bitmap provient, l'autre vers la structure qui contient le sprite (nous passons cette structure par adresse, pour modifier son contenu!). Une fois le tableau alloué, en multipliant sa hauteur par sa largeur, il est alors temps de transférer les données graphiques. 2 boucles feront l'affaire, en parcourant chaques octets avec la formule: y*largeur+x. On peut une fois le transfert terminer libérer la mémoire allouer pour le bitmap.
AFFICHER LES SPRITES
La fonction putsprite fonctionne de façon inverse, c'est à dire qu'elle va prendre les données graphiques et elle va ensuite les transférer sur un écran. En passant la structure du sprite, nous n'avons besoin que de la position (x,y) finale. Cette fonction copiera le tableau contenant les données graphiques. Cette première version ne gère pas la transparence. En effet, si on affichait un sprite sur une image avec cette fonction, le sprite s’afficherait avec un rectangle noir autour de lui (à moins qu’il soit parfaitement rectangulaire).
// Version ordinaire
void putsprite(int x1,int y1,tsprite *sprite)
{
for (int y=0; y<sprite->y; y++)
for (int x=0; x<sprite->x; x++)
virtuel[(y1+y)*320+x+x1] = sprite->graphics[y*sprite->x+x];
}
La solution pour ajouter de la transparence est toute simple. On détermine une couleur dans la palette qui agira comme couleur de fond, par convention on utilise 0 (qui est généralement le noir). Avant d’afficher un octet, on vérifie sa couleur. S’il est noir, on ne l’affiche pas, sinon on le copie comme d’habitude :
// Version Transparente
void putspriteT(int x1,int y1,tsprite *sprite)
{
unsigned char octet;
for (int y=0; y<sprite->y; y++)
for (int x=0; x<sprite->x; x++)
octet = sprite->graphics[y*sprite->x+x];
if (octet) virtuel[(y1+y)*320+x+x1] = octet;
}
CHANGEMENT D’ÉCHELLE
Souvent, on désire donner une apparence de profondeur à nos bitmaps, comme par exemple dans les jeux d’aventures. On pourrait en théorie avoir un set d’image représentant le personnage à différentes échelles, mais il existe une solution qui ne requiert pas d’autre images : il suffit de changer l’échelle du bitmap, en l’agrandissant quand il se « rapproche » et en le rapetissant quand il « s’éloigne ».
La solution est encore très facile. Il suffit d’ajouter ou d’enlever des pixels au sprite original. Par exemple, si nous voulons doubler la taille d’un sprite, pour chaque pixel original, en rajouter un autre. Donc, un sprite de 32x32 verrait sa taille doubler à 64x64. De la même façon, si on désire réduire sa taille de 50%, pour chaque pixel affiché, on en omet un. Notre sprite aurait donc maintenant une taille de 16x16. Pour venir modifier la taille d’un sprite, il faudra multiplier chaque x et chaque y par le facteur de changement d’échelle :
// Version avec changement d’échelle
void putspriteE(int x1, int y1, float echelle, tsprite *sprite)
{
float tx,ty;
float sy = sprite->y / echelle;
float sx = sprite->x / echelle;
float centreX = x1 - sx/2;
float centreY = y1 - sy/2;
for (int y=0;y<sy;y++)
{
ty = (y * echelle);
for (int x=0;x<sx;x++)
{
tx = (x * echelle);
virtuel[(centreY+y)*320+x+centreX]=sprite->graphics[tx+((int)(ty)*sprite->x) ];
}
}
}
La seule difficulté dans ce cas est de ne pas oublier que le nombre de pixels à parcourir est déterminer par la taille originale du sprite diviser par le facteur d’échelle. Il faut également effectuer une translation pour effectuer le changement d’échelle sur le centre du bitmap, et non à l’origine (0,0).
ROTATIONS
Nous sommes dans l’obligation de faire un petit survol de trigonométrie de base, donc ceux qui sont à l'aise avec ces concepts peuvent sauter ces explications. Les rotations 2D utilisent abondamment des fonctions trigonométrique donc pour le bénéfice de tous, voici un petit cours rapide sur la trigonométrie.
Trigonométrie
Le terme trigonométrie contient le mot grec "trigon", qui signifie triangle. La trigonométrie est basé sur un ensemble de fonctions qui agissent sur les triangles. Il existe plusieurs fonctions, mais les plus utilisé son Sinus et Cosinus. Ces fonctions travaillent exclusivement sur des angles, exprimés en degrés ou en radians. Dans notre cas, nous travaillerons en degrés. Maintenant, qu'est-ce que retourne exactement ces fonctions? Regardez le schéma suivant:
Nous sommes ici en présence d'un cercle ayant un rayon de 1 (également appelé "cercle unitaire"). Dans ce cercle, nous avons un triangle rectangle rouge, avec un angle de 60 degré. Si on vous demandait d'estimer la hauteur du segment directement opposé à cet angle (le côté vertical du triangle, celui parallèle à l'axe Y), que répondriez-vous? Sûrement quelque chose entre 0.7 et 0.9. Maintenant, faites la même chose mais pour le côté adjacent. Il semble se terminer au milieu, donc 0.5. Vérifions nos dires de façon mathématiques pour voir si nos estimations étaient réalistes:
sin(60) = 0,866025403784438646763723170752936
cos(60) = 0,5
La fonction sinus permet de trouver la longueur des côtés du triangle! Étant donné un cercle de rayon 1, et une ligne de n'importe quel angle originant du centre du cercle, sinus de cet angle nous ramène sa hauteur (Y), et cosinus de l'angle nous ramène la largeur (X) de cette ligne. On utilise aussi parfois l'expression "opposé sur hypoténuse" et "adjacent sur hypoténuse", en parlant du rapport entre ces deux côtés. Par exemple, quelle serait le sinus et le cosinus de 90 degré? La hauteur de la ligne correspond à la hauteur du cercle, donc 1, et celle ligne de va ni à gauche ni à droite, donc elle mesure 0 de largeur. Cela ce vérifie mathématiquement avec sin et cos de 90. Cette infime base de trigo est tout ce qu'il nous faut pour comprendre ce qui suit.
Rotations
Encore une fois, regardez ce petit schéma. Dans cet exemple, nous voulons effectuer une rotation de 40 deg, en partant d'un point situé sur le cercle à 30 deg, et on désire qu'il termine sa course avec un angle de 70 deg:
Expliquons maintenant la notation: la ligne qui part de l'origine de chaque cercle est notée "R", pour rayon (ces lignes formes l’hypoténuse des triangles rectangles). Le base du triangle rouge est représenté par X, et de la même façon, la hauteur de ce triangle est noté "Y". Pour le triangle bleu, le X et Y est noté "U" et "V", respectivement. Finalement, nos deux angles. Le premier, celui qui part de l'axe X et qui se termine sur la première ligne R est appelé ï (Phi). Le second, entre le premier et le second R, est appelé ë (Thêta). Regardez attentivement le schéma et assurez-vous de bien le comprendre.
A priori, nous savons que notre point initial est à un angle inconnu (on sait que c'est 30 deg, mais en théorie on ne le saurait pas pour des points arbitraire), mais nous savons qu'il possède des coordonnée cartésienne (X,Y). Nous voulons prendre ces coordonnées, et les transformés afin de faire décrire au point une rotation de 40 degré autour du point d'origine, afin de trouvé ses nouvelles coordonnées (U,V). Mais quelle équation utilisé?
Il existe plusieurs propriété trigonométrique qui nous viennent alors en aide. Une d'elle se nomme la "Loi des Sinus", qui dit que:
/ |
/á |
C / |
/ |A
/ |
/ |
/à â|
----------
B
Sin(à) Sin(á) Sin(â)
------ = ------ = ------
A B C
Dans ce cas, A,B et C sont les longueurs des côté d'un triangle, et à, á et â sont les angles directement opposé à ces côtés. Cette propriété fonctionne sur tous les types de triangles, mais comme dans notre cas nous utilisons les triangles rectangles, cela nous simplifiera la vie. En prenant notre triangle rouge, et en substituant le R pour notre C, en sachant qu'il s'agit d'un triangle rectangle, nous savons automatiquement que â equivaut à 90 deg. Puisque le sinus de 90 est 1, nous pouvons simplifier. Dans notre cas, nous avons seulement besoin d'un côté de notre Loi des sinus, celui A-à, où le "A" equivaut à Y, et à est le même que ë. Maintenant nous obtenons cette formule:
Sin(90) Sin(ë) 1 Sin(ë)
------- = ------ équivaut à --- = ------
R Y R Y
Maintenant, isolons ë en multipliant de chaque côté par Y, nous obtenons:
Y
--- = Sin(ë)
R
De la même façon, cette formule est valide pour l'angle complet, c'est à dire ï + ë:
V
--- = Sin(í+é)
R
Bon, maintenant, voici une autre propriété trigonométrique, qui affirme que pour n'importe quel angle à et á,
Sin(à+á) = Sin(à)*Cos(á) + Cos(à)*Sin(á)
En substituant nos équations précédentes, nous obtenons:
V
--- = Sin(ï)*Cos(ë) + Cos(ï)*Sin(ë)
R
Isolons notre V, pour obtenir le Y du point d'arrivé, en multipliant chaque côté de l'équation par R, nous obtenons:
V = R*Sin(ï)*Cos(ë) + R*Cos(ï)*Sin(ë)
Notre dernière propriété, sert à convertir les coordonnées polaire en coordonnée cartésienne. Je ne vais pas m'étendre longtemps là-dessus, mais ne mélangez pas les R,X et Y de cette équation avec ceux nos formules précédentes! Il ne s'agit ici que d'équation de conversion polaire-cartésienne, rien de plus.
X = R*Cos(Theta)
Y = R*Sin(Theta)
En observant notre équation pour trouver le V ci-dessus, nous remarquons que Phi est un angle dans le triangle qui s'occupe seulement de X et Y, que nous connaissons car il s'agit de nos points d'origines. Alors on peut laisser tomber R*Sin(ï) et R*Cos(ï) et simplement substituer nos X et Y...
V = Y*Cos(ë) + X*Sin(ë)
Voilà, nous avons notre formule finale pour effectuer une rotation 2D. L'équation du X se prouve de la même manière. Dernière chose: n'oubliez pas que ces équations assume que nous sommes à l'origine (0,0). Afin de centrer sur un autre point, n'oubliez pas d'effectuer une translation avant la rotation.
X’ = (X*Cos(Thêta)) - (Y*Sin(Thêta))
Y’ = (Y*Cos(Thêta)) + (X*Sin(Thêta))
ROTATION 2D D’UN SPRITE
En utilisant donc l’équation de rotation 2D ci-dessus, nous pouvons transformer nos sprites pour leurs faire effectuer des rotations. Il suffit tout simplement de transformer chaque pixels du sprite, et de vérifier si le pixel d’origine est dans les limites du sprite.
void putspriteR(int x1, int y1, float angle, tsprite *sprite)
{
float tx,ty;
float centreX,centreY;
for (int y=0;y<sprite->y;y++)
{
centreY=(float)(y-8);
for (int x=0;x<sprite->x;x++)
{
centreX=(float)(x-8);
tx = centreX * cos(angle) - centreY * sin(angle) + 8;
ty = centreY * sin(angle) + centreY * cos(angle) + 8;
if ( (tx>0) && (ty>0) && (tx<sprite->x) && (ty<sprite->y))
virtuel[(y1+y)*320+x+x1] = sprite->graphics[(tx+(int)(ty)*sprite->x)];
}
}
}
Cette fonction effectue une translation avant d’effectuer une rotation pour que le point central de la rotation soit le centre du sprite et non l’origine (0,0).
CONCLUSION
Ces fonctions sont un bon départ pour construire une libraire de sprites. Il faut cependant juger quand utiliser ces fonctions, et quand utiliser différents dessin de sprite. Par exemple, si notre donne la possibilité au sprite de s’orienter dans seulement 4 directions, il serait plus sage d’avoir 4 dessin du même sprite dans l’orientation voulu, plutôt que d’effectuer les calculs nécessaires à la rotation. Mais si on veux donner au sprite une totale liberté de mouvement, alors les équations sont très utiles. Au cours de ce chapitre, nous avons ajouter à notre bibliothèque graphique :
- La possibilité de charger en mémoire des sprites
- L’affichage de ces sprites (transparent et ordinaire)
- Transformations 2D (changements d’échelle et rotation)
2dchap6.cpp
//----------------------------------------------------------------------//
// FICHIER : 2DCHAP6.CPP //
// AUTEUR : Shaun Dore //
// DESCRIPTION : Les sprites //
// DATE DE MODIFICATION : 21-04-98 //
// COMPILATEUR : Borland Turbo C++ Real Mode 16-bit compiler //
// NOTES : Compiler avec modele memoire Compact //
//----------------------------------------------------------------------//
//----------------------------------------------------------------------//
// Fichiers include //
//----------------------------------------------------------------------//
#include <mem.h>
#include <math.h>
#include <conio.h>
#include <stdio.h>
//----------------------------------------------------------------------//
// Constantes //
//----------------------------------------------------------------------//
const unsigned int largeur = 16; // largeur du bitmap contenant le sprite
const unsigned int hauteur = 16; // hauteur du bitmap contenant le sprite
const unsigned long taille = hauteur*largeur; // taille du bitmap
//----------------------------------------------------------------------//
// Structure de donnees //
//----------------------------------------------------------------------//
typedef struct tsprite
{
char *graphics; // pointeur sur donnes graphiques
int x, y; // largeur et hauteur du sprite
int px,py; // position courante du sprite (optionel)
int sx,sy; // vitesse courante su sprite (optionel)
};
//----------------------------------------------------------------------//
// Variables globales //
//----------------------------------------------------------------------//
char *ecran = (char *) (0xA0000000); // ptr sur memoire video
char *virtuel = new char[64000L]; // ptr sur ecran virtuel
char *bitmap = new char[taille];
//----------------------------------------------------------------------//
// setmode - Appelle le mode passer en parametre //
//----------------------------------------------------------------------//
void setmode(unsigned int mode)
{
asm {
MOV AX, [mode]
INT 0x10
}
}
//----------------------------------------------------------------------//
// setpal - Modifie la palette //
//----------------------------------------------------------------------//
void setpal(unsigned char coul,unsigned char r,unsigned char g,unsigned char b)
{
outp (0x03C8,coul);
outp (0x03C9,r);
outp (0x03C9,g);
outp (0x03C9,b);
}
//----------------------------------------------------------------------//
// loadpcx - Charge en memoire un fichier .PCX //
//----------------------------------------------------------------------//
int loadpcx(char *nomfich,unsigned long taille,char *image)
{
unsigned char data, nb_octets, palette[768];
unsigned long index = 0;
unsigned int indexrle;
FILE *fichpcx;
if (!(fichpcx = fopen(nomfich, "rb"))) return 0;
fseek(fichpcx, -768, SEEK_END);
fread(&palette, 768, 1, fichpcx);
for (int coul=0;coul<=255;coul++)
setpal(coul,palette[coul*3]>>2,palette[coul*3+1]>>2,palette[coul*3+2]>>2);
fseek(fichpcx, 128, SEEK_SET);
do
{
fread(&data, 1, 1, fichpcx);
if ((data & 0xC0) == 0xC0)
{
nb_octets = (data & 0x3F);
fread(&data,1,1,fichpcx);
for (indexrle=0;indexrle<nb_octets;indexrle++) image[index++]=data;
}
else image[index++] = data;
} while(index < taille);
fclose(fichpcx);
return 1;
}
//----------------------------------------------------------------------//
// getsprite - Mets en memoire un sprite //
//----------------------------------------------------------------------//
void liresprite(int x1,int y1,int larg,int haut, tsprite *sprite)
{
sprite->x = larg;
sprite->y = haut;
sprite->graphics = new char[larg*haut];
for (int y=0; y<haut; y++)
for (int x=0; x<larg; x++)
sprite->graphics[(y*larg)+ x] = bitmap[(y1+y)*largeur+x+x1];
}
//----------------------------------------------------------------------//
// putsprite - Affiche un sprite version ordinaire //
//----------------------------------------------------------------------//
void putsprite(int x1,int y1,tsprite *sprite)
{
for (int y=0; y<sprite->y; y++)
for (int x=0; x<sprite->x; x++)
virtuel[(y1+y)*320+x+x1] = sprite->graphics[y*sprite->x+x];
}
//----------------------------------------------------------------------//
// putspriteT - Affiche un sprite version transparente //
//----------------------------------------------------------------------//
void putspriteT(int x1,int y1,tsprite *sprite)
{
unsigned char octet;
for (int y=0; y<sprite->y; y++)
for (int x=0; x<sprite->x; x++)
{
octet = sprite->graphics[y*sprite->x+x];
if (octet) virtuel[(y1+y)*320+x+x1] = octet;
}
}
//----------------------------------------------------------------------//
// putspriteE - Affiche un bitmap version changement d'echelle //
//----------------------------------------------------------------------//
void putspriteE(int x1, int y1, float echelle, tsprite *sprite)
{
float tx,ty;
float sy = sprite->y / echelle;
float sx = sprite->x / echelle;
float centreX = x1 - sx/2;
float centreY = y1 - sy/2;
for (int y=0;y<sy;y++)
{
ty = (y * echelle);
for (int x=0;x<sx;x++)
{
tx = (x * echelle);
virtuel[(centreY+y)*320+x+centreX]=sprite->graphics[(tx)+((int)(ty)*sprite->x)];
}
}
}
//----------------------------------------------------------------------//
// putspriteR - Affiche un bitmap version rotation 2D //
//----------------------------------------------------------------------//
void putspriteR(int x1, int y1, float angle, tsprite *sprite)
{
float tx,ty;
float centreX,centreY;
for (int y=0;y<sprite->y;y++)
{
centreY = y-8;
for (int x=0;x<sprite->x;x++)
{
centreX = x-8;
tx = centreX * cos(angle) - centreY * sin(angle) + 8;
ty = centreX * sin(angle) + centreY * cos(angle) + 8;
if ( (tx>0) && (ty>0) && (tx<sprite->x) && (ty<sprite->y))
virtuel[(y1+y)*320+x+x1] = sprite->graphics[(tx+(int)(ty)*sprite->x)];
}
}
}
//----------------------------------------------------------------------//
// Fonction MAIN() //
//----------------------------------------------------------------------//
void main()
{
tsprite vaisseau;
setmode(0x13);
memset(virtuel,2,64000L);
if(!(loadpcx("vaisseau.pcx",taille,bitmap))) // Charger le bitmap
{
setmode(0x03);
printf("ERREUR: Incapable de charger vaisseau.pcx!\n");
return;
}
liresprite(0,0,16,16,&vaisseau); // Initialiser le sprite
// Affichage ordinaire
putsprite(160-(vaisseau.x/2),100-(vaisseau.x/2),&vaisseau);
memcpy(ecran,virtuel,64000L); // Afficher le sprite
gotoxy(1,1);printf("Sprite version ordinaire");
getch();
// Affichage transparent
memset(virtuel,2,64000L);
putspriteT(160-(vaisseau.x/2),100-(vaisseau.x/2),&vaisseau);
memcpy(ecran,virtuel,64000L); // Afficher le sprite
gotoxy(1,1);printf("Sprite version transparent");
getch();
// Affichage avec changement d'echelle
memset(virtuel,0,64000L);
putspriteE(160,100,.1,&vaisseau); // Changement d'echelle
memcpy(ecran,virtuel,64000L);
gotoxy(1,1);printf("Sprite version changment d'echelle");
getch();
// Table precalculer pour les rotations 2D
memset(ecran,0,64000L);
float CosTable[256];
unsigned char angle=0;
for(int i=0; i<256; i++) CosTable[i]=cos(i*M_PI/128);
// Rotation du sprite
while(!(kbhit()))
{
memset(virtuel,0,64000L);
putspriteR(160,100,CosTable[angle]*2*M_PI,&vaisseau);
while(!(inp(0x3da)&8));
memcpy(ecran+(8*320),virtuel,64000L-(8*320));
gotoxy(1,1);printf("Sprite version rotation");
angle++;
}
setmode(0x03);
printf("Dump de sprite:\n\n");
for (int y=0;y<16;y++)
{
printf("\n");
for (int x=0;x<16;x++)
printf("%c ",bitmap[(y*16)+x]);
}
printf("\n\nShaun Dore\ndores@videotron.ca\n http://pages.infinit.net/shaun/ ");
delete[] bitmap;
delete[] virtuel;
delete[] vaisseau.graphics;
}