Publicité pour l'hébergement

Projet complet

Introduction
Le "Ray casting" est une technique qui permet d'afficher des rendus en 3 dimensions même avec une puissance de calcul limité.
Par contre cette technique ne permet pas d'afficher n'importe quel objet en 3D. En fait, elle ne permet que d'afficher des murs orthogonaux.
Cependant, à l'aide de certaines techniques supplémentaires, il est possible de réaliser des jeux 3D complets, par exemple du type FPS (first person shooter).
Dans notre projet, nous utiliserons la librairie SDL ainsi que cette technique du ray casting pour développer un moteur 3D simple.



I - Tutoriel Librairie SDL

"SDL" pour "Simple Directmedia Layer". Il s'agit d'une librairie graphique accessible par tous. Elle permet de créer des applications graphiques comme des jeux 2D ou des émulateurs. Cette bibliothèque est très flexible, simple d'utilisation et portative. Tous ces atouts ont largement contribué à son succès.


1) Installation

Afin de commencer à utiliser la librairie SDL, nous allons procéder à son installation sous le logiciel Code::Blocks v1.0 RC2.

Tout d'abord, il vous faut récupérer les fichiers nécéssaires à l'installation de la librairie.
Pour cela, il vous suffit de cliquer ici :)

Une fois ceci fait, voici la procédure à suivre.
1) Entrez dans le dossier SDL-1.2.12.
2) Dézippez le contenu du dossier lib dans le dossier lib de Code::Blocks :
C:/Program Files/CodeBlocks/lib (par défaut)
3) Dézippez le contenu du dossier bin dans le dossier bin de Code::Blocks :
C:/Program Files/CodeBlocks/bin (par défaut)
3) Ouvrez le dossier include.
4) Dézippez le dossier SDL dans le dossier include de Code::Blocks :
C:/Program Files/CodeBlocks/include (par défaut)
5) Retournez dans le dossier principal et ouvrez le dossier bin.
6) Dézippez le fichier SDL.dll dans le dossier C:/WINDOWS/system32 afin que la librairie SDL soit accessible pour n'importe quel programme à compiler.
7) Lancez Code::Blocks puis créez un projet SDL :
http://zupi.free.fr/PTuto/images/SDL/install1.jpg
8) Ensuite, rendez-vous dans les propriétés du projet : Project -> Properties
9) Cliquez sur l'onglet Targets et veillez à bien décocher la case Pause when execution ends puis validez.
http://zupi.free.fr/PTuto/images/SDL/install2.JPG

La librairie SDL en elle même est maintenant prête.
Cependant, nous aurons aussi besoin d'utiliser SDL_image et SDL_ttf.

Commençons par SDL_image. Cette librairie utilise SDL mais permet de charger n'importe quel type d'image à l'écran et non seulement des BMP comme le permet SDL.

Tout d'abord, il vous faut récupérer les fichiers nécéssaires à l'installation de la librairie.
Pour cela, il vous suffit de cliquer ici (mais non on ne se répète pas ;))

Une fois ceci fait, voici la procédure à suivre :
1) Dézippez tous les fichiers .dll du dossier /lib/ de l'archive dans le dossier /bin/ de votre CodeBlocks : C:/Program Files/CodeBlocks/bin (par défaut).
Profitez en pour faire de même dans le dossier C:/WINDOWS/System32/.
2) Dézippez le fichier .lib du dossier /lib/ de l'archive dans le dossier /lib/ de votre CodeBlocks : C:/Program Files/CodeBlocks/bin (par défaut).
3) Dézippez le fichier .h du dossier /include/ de l'archive dans le dossier /include/SDL/ de votre CodeBlocks : C:/Program Files/CodeBlocks/include/SDL (par défaut).
4) Maintenant, lancez Code::Blocks et rendez-vous dans le menu "Build" puis "Compiler Options".
http://zupi.free.fr/PTuto/images/SDL/install3.jpg
5) Sélectionnez l'onglet "Linker" puis cliquez sur le bouton "Add". Entrez alors "SDL_image" (cf screenshot). Validez le tout.
http://zupi.free.fr/PTuto/images/SDL/install4.jpg

Désormais, vous pourrez utiliser SDL_image en incluant :
#include "SDL/SDL_image.h"

Enfin, mettons un terme à toutes ces installations barbantes vitales :)

SDL_ttf sert à afficher des textes à partir de polices courantes ce qui évite de faire des images pour chaque affichage de texte.

Pour installer SDL_ttf, il vous faut d'abord récupérer les fichiers nécessaires à l'installation en cliquant ici (petit variante :))

Ensuite, reproduisez exactement les mêmes étapes que pour SDL_image, jusqu'à l'étape 5).
En effet, pour SDL_ttf vous ne devrez pas écrire "SDL_image" mais bien ... (suspense) ... "SDL_ttf" !

Enfin, pour utilisez SDL_ttf, il vous suffira d'inclure :
#include "SDL/SDL_ttf.h"


Vous êtes maintenant paré à utiliser la librairie SDL et à débuter ce tutorial sur SDL ! (les choses sérieuses quoi :))


2) Hello World v1.0

Comme le veut la tradition en programmation, tout débutant dans un domaine se doit de passer par le fameux "Hello world !".
Ainsi, nous allons procéder à l'établissement du Hello world v1.0 qui utilisera SDL_image précédemment installé. Autrement dit, nous écrirons "Hello World !" sur une fenêtre avec une imagee (vous verrez à la v2.0 qu'il est possible de faire autrement).

Commençons donc l'écriture du programme.

Tout d'abord, lancez Code::Blocks et créer un nouveau projet "SDL Application", tout en cochant "Do not create any files".

Une fois la page vierge affichée, entrons l'en tête du fichier :

Code C:
//Les fichiers d'entête
#include "SDL/SDL.h"
#include "SDL/SDL_image.h"
#include "SDL/SDL_ttf.h"
#include <string> //Afin de récupérer les noms des fichiers à charger
 
using namespace std;

Ici, on inclut donc une partie ce qu'on a installé avant à savoir SDL_image et SDL tout court :)

Code C:
//Les attributs de l'ecran (640 * 480)
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const int SCREEN_BPP = 32;

On fixe la hauteur de l'écran à 480, la largeur à 640 et le nombre de bits par pixels à 32.

Code C:
//Les surfaces
SDL_Surface *background = NULL;
SDL_Surface *message = NULL;
SDL_Surface *screen = NULL;

Ensuite, on définit les différentes parties de l'écran final. On a donc screen pour l'écran, background pour le fond et message pour notre fameux "Hello world !". Pour l'instant, ces surfaces sont des pointeurs qui ne redirigent vers rien.

Code C:
SDL_Surface *load_image( std::string filename )
{
//L'image qui est chargée
SDL_Surface *loadedImage = NULL;
 
//L'image optimisée que nous utiliserons par la suite
SDL_Surface *optimizedImage = NULL;
 
//Chargement de l'image grâce à SDL_image
loadedImage = IMG_Load( filename.c_str() );
 
//Si l'image est chargée correctement
if( loadedImage != NULL )
{
//creation de l'image optimisée
optimizedImage = SDL_DisplayFormat( loadedImage );
 
//liberation de l'ancienne image
SDL_FreeSurface( loadedImage );
 
}
 
//on retourne l'image optimisé
return optimizedImage;
}

Voici la création de la première fonction. La fonction "load_image" va nous servir à charger dans le tampon une image "optimisée". L'optimisation est fortement conseillée afin d'éviter d'éventuelles pertes de performance dans le cas ou l'image ne serait pas encodée en 32 bits.
A noter que cette fonction retourne un pointeur vers la version optimisée de l'image chargée.

Code C:
void apply_surface( int x, int y, SDL_Surface* source, SDL_Surface* destination, SDL_Rect* clip = NULL )
{
SDL_Rect offset;
 
offset.x = x;
offset.y = y;
 
//on blit la surface
SDL_BlitSurface( source, NULL, destination, &offset );
}

La fonction apply_surface va nous permettre d'appliquer (afficher) une image sur l'écran. Pour cela, on lui donne la position (x;y) à laquelle le coin en haut gauche de l'image doit être placé. Ensuite, on crée un rectangle SDL auquel on donne la même position que celle voulue pour l'image. Enfin, on "blit" la surface sur l'écran. (Le terme blitter signifie en quelque sorte "coller")
Pour cela, on fournit à SDL_BlitSurface la source, la destination et le rectangle précédemment crée.

Code C:
bool init()
{
//initialisation de tout les sous-systemes de sdl
if( SDL_Init( SDL_INIT_EVERYTHING ) == -1 )
{
return false;
}
 
//on met en place l'ecran
screen = SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP, SDL_SWSURFACE );
 
//Si il y a une erreur lors de la mise en place de l'ecran
if( screen == NULL )
{
return false;
}
 
//on met en place la barre caption de la fenetre
SDL_WM_SetCaption( "Hello World ! v1.0", NULL );
 
//si tout s'est bien passé
return true;
}

La fonction booléenne init va nous permettre d'initialiser l'écran qui va recevoir et afficher ce que l'on veut. C'est l'une des fonctions clés de ce programme. On initialise donc tous les sous système de SDL à savoir les systèmes audio, vidéo, etc ... même si l'on ne se sert pas de tous. Ensuite, on met en place l'écran avec ces différentes caractéristiques.

Code C:
bool load_files()
{
//chargement du fond
background = load_image( "background.png" );
 
//si il y a un probleme au chargement du fond
if( background == NULL )
{
return false;
}
 
message = load_image( "message.jpg" );
 
//si il y a un probleme au chargement du message
if( message == NULL )
{
return false;
}
 
//Si tout s'est bien passé
return true;
}

Le fonction booléenne load_files est simplement une fonction de vérification qui permet de contrôler si le chargement des images c'est bien passé.

Code C:
void clean_up()
{
//Liberation des surfaces
SDL_FreeSurface( background );
SDL_FreeSurface( message );
 
//On quitte SDL
SDL_Quit();
}

La fonction clean_up permet de vider le tampon avant de quitter SDL et donc le programme.

Code C:
int main( int argc, char* args[] )
{
//Initialisation
if( init() == false )
{
return 1;
}
 
//Chargement des fichiers
if( load_files() == false )
{
return 1;
}
 
 
//Application des surfaces(images) sur l'ecran
apply_surface( 0, 0, background, screen );
apply_surface( 220, 150, message, screen );
 
//mise à jour de l'ecran
if( SDL_Flip( screen ) == -1 )
{
return 1;
}
 
//On "met en pause" le programme pendant 3000 millisecondes (3 secondes)
SDL_Delay(3000);
 
//liberation des surface et on quitte sdl
clean_up();
 
return 0;
}

Enfin, la fonction principale du programme.
On initialise l'écran ainsi que les fichiers. Si tout c'est bien passé, on applique les images à l'écran et enfin, on laisse le programme ouvert 3 secondes avant de tout fermer.

Compilez et lancez et admirez votre premier programme fait avec SDL :)

Vous pouvez télécharger la source en cliquant ici


3) Hello world v2.0

Comme vous vous en doutez, nous aurons probablement besoin d'écrire sur notre écran de jeu. Ainsi, faire une image à chaque message à écrire, ça risque de très vite devenir lourd. On va donc reprendre le code précédent et remplacer le message en image pas un message écrit avec une police. Nous prendrons GeekT.ttf pour cette exemple :)

Nous allons donc rajouter à l'en tête du premier programme ceci :

Code C:
//Le Font qu'on va utiliser
TTF_Font *font;
 
//La couleur du Font
SDL_Color textColor = { 255, 255, 255 };

Comme vous le devinez facilement, nous allons donc utiliser SDL_ttf :)

Code C:
	//Initialisation de SDL_TTF 
if( TTF_Init() == -1 ) {
return false;
}
 

A rajouter dans la fonction booléenne init entre le test de mise en place de l'écran et la définition de la barre de caption de la fenêtre, à la place de ce qui concerne message

Code C:
 
//Ouverture du Font
font = TTF_OpenFont( "geekt.ttf", 28 );
 
//S'il y a une erreur dans le chargement du Font
if( font == NULL ) {
return false;
}
 

A rajouter dans la fonction booléenne load_files après le test du chargement correct du background et avant le retour de la fonction

Ici, on assigne une police ainsi que sa taille à la variable font. Nous sommes maintenant prêts à écrire avec :)

Code C:
 
void clean_up() { //Libération des surfaces
SDL_FreeSurface( background );
SDL_FreeSurface( message );
 
//Fermeture des Fonts qu'on a utilisé
TTF_CloseFont( font );
 
//On quitte SDL_ttf
TTF_Quit();
 
//On quitte SDL
SDL_Quit();
}

La fonction clean_up ne change que très peu. On ajoute juste la fermeture des fonts et on quitte SDL_ttf

Code C:
//Mise en place du texte sur la surface message 
message = TTF_RenderText_Solid( font, "Hello World !", textColor );
 
 
//S'il y a une erreur dans la mise en place du texte
if( message == NULL ) {
return 1;
}
 
//Application des images sur l'écran
apply_surface( 0, 0, background, screen );
apply_surface( 200, 175, message, screen );
 

A rajouter dans la fonction main après le chargement des fichiers

Bienvenue dans le monde SDL !

Vous pouvez télécharger la source en cliquant ici


4) Gestion d'évènements

Dans cette partie, nous allons voir comment gérer les évènements. Nous créerons donc un programme qui effectuera une action en fonction des touches directionnelles qui sont pressées, etc ...

Nous allons donc reprendre l'ensemble du programme Hello World v2.0 et le modifier petit à petit.

Commençons donc par gérer la croix qui permet de quitter le programme. (c'est pas le plus joyeux mais bon c'est nécéssaire :))
Ainsi, le programme ne se fermera plus automatiquement au bout de 3 secondes mais lorsque l'utilisateur le souhaitera.

Rajoutons donc dans l'en tête :
Code C:
 
//La structure d'événements qu'on va utiliser
SDL_Event event;
 

Cette nouvelle variable va permettre de gérer les évènements reçus.

Ensuite, remplaçons la fonction main par celle-ci :
Code C:
int main( int argc, char* args[] )
{
//Ce qui va nous permettre de quitter
bool quit = false;
 
//Initialisation
if( init() == false )
{
return 1;
}
 
//Chargement des fichiers
if( load_files() == false )
{
return 1;
}
 
//Mise en place du texte sur la surface message
message = TTF_RenderText_Solid( font, "Hello World !", textColor );
 
 
//S'il y a une erreur dans la mise en place du texte
if( message == NULL ) {
return 1;
}
 
//Application des images sur l'écran
apply_surface( 0, 0, background, screen );
apply_surface( 200, 175, message, screen );
 
//Mise à jour de l'écran
if( SDL_Flip( screen ) == -1 )
{
return 1;
}
 
//Tant que l'utilisateur n'a pas quitté
while( quit == false )
{
//Tant qu'il y a un événement
while( SDL_PollEvent( &event ) )
{
//Si l'utilisateur à cliqué sur le X de la fenêtre
if( event.type == SDL_QUIT )
{
//On quitte le programme
quit = true;
}
}
}

 
//Libèration des surfaces et on quitte sdl
clean_up();
 
return 0;
}
 

La partie qui nous intéresse est celle en gras. On peut donc remarquer une double boucle while qui permet grâce à SDL_PollEvent d'abord de capter l'évènement puis s'il s'agit de l'évènement SDL_QUIT (clic sur la croix de fermeture) d'agir en conséquence.

La source de cette partie est téléchargeable ici

Continuons donc nos découvertes ...
Code C:
SDL_Surface *down = NULL;
SDL_Surface *up = NULL;

A rajouter dans les variables en tête afin de pouvoir afficher un message spécifique si la flèche haut ou bas sont pressées.

Code C:
void clean_up()
{
//Libération des surfaces
SDL_FreeSurface( background );
SDL_FreeSurface( message );
SDL_FreeSurface( up );
SDL_FreeSurface( down );
 
//Fermeture des Fonts qu'on a utilisé
TTF_CloseFont( font );
 
//On quitte SDL_ttf
TTF_Quit();
 
//On quitte SDL
SDL_Quit();
}

Petits rajouts concernant ces deux nouvelles variables, pour plus de propreté.

Code C:
 
int main( int argc, char* args[] )
{
//Ce qui va nous permettre de quitter
bool quit = false;
 
//Initialisation
if( init() == false )
{
return 1;
}
 
//Chargement des fichiers
if( load_files() == false )
{
return 1;
}
 
 
//Génération des surfaces de message
up = TTF_RenderText_Solid( font, "Haut a ete presse.", textColor );
down = TTF_RenderText_Solid( font, "Bas a ete presse.", textColor );
 
if(up==NULL || down==NULL)
{
return 1;
}

 
//Tant que l'utilisateur n'a pas quitté
while( quit == false)
{
//Tant qu'il y a un événement
if ( SDL_PollEvent( &event ) )
{
//Si l'utilisateur à cliqué sur le X de la fenêtre
if( event.type == SDL_QUIT )
{
//On quitte le programme
quit = true;
}
//Si une touche est pressée
else if( event.type == SDL_KEYDOWN )
{
switch(event.key.keysym.sym)
{
case SDLK_UP: message = up;
break;
case SDLK_DOWN: message = down;
break;
}
}
//Si un message a besoin d'être affiché
if( message != NULL )
{
//Application de l'image sur la l'écran
apply_surface( 0, 0, background, screen );
apply_surface( ( SCREEN_WIDTH - message->w ) / 2, ( SCREEN_HEIGHT - message->h ) / 2, message, screen );
 
//On met à NULL le pointeur vers la surface message
message = NULL;
}
//Mise à jour de l'écran
if( SDL_Flip( screen ) == -1 )
{
return 1;
}
}

}
 
//Libèration des surfaces et on quitte sdl
clean_up();
 
return 0;
 
}

Voici notre nouvelle fonction main.
La première partie en gras concerne la création des messages qui seront affichés en fonction de la touche pressée.
La seconde partie correspond au traitement de la touche pressée.
Ainsi, vous remarquerez que SDL_KEYDOWN permet de détecter l'appui sur une touche mais qu'après il existe un moyen particulier de récupérer quelle touche a été pressée. Il y a donc une hiérarchie afin de remonter à la touche pressée. La voici :
http://zupi.free.fr/PTuto/images/SDL/keysym.jpg
Le event.key.keysym.sym est donc justifié ainsi et retourne la touche qui a été pressée. Il suffit juste de le comparé avec les standards SDL, à savoir SDLK_UP et SDLK_DOWN en ce qui nous concerne. (Il en aurait été de même pour SDLK_LEFT et SDLK_RIGHT)

Zoom sur la ligne suivante :
Code C:
apply_surface( ( SCREEN_WIDTH - message->w ) / 2, ( SCREEN_HEIGHT - message->h ) / 2, message, screen );

Cette ligne permet de placer le nouveau message au centre parfait de l'écran. En effet, message->w récupère la largeur du message et message->h sa hauteur. Si l'on ôte ces deux valeurs de celles correspondantes à l'écran et que l'on divise le tout par 2, on obtient le centre de l'écran.

La source de cette partie est téléchargeable ici




II - Ray Casting

1) Qu'est ce que le Ray-casting?

Le Ray-casting est une technique qui transforme une forme limitée de données (une carte très simplifiée ou plan d'étage) dans une projection 3D en traçant les rayons du point de vue dans la visualisation de volume.

2) Création d'un monde

La première étape du Ray-casting est de créer un monde sur lequel on pourra se déplacer. Pour cela, il faut déjà créer un tableau 2D, qui représentera le monde sur lequel on se déplacera.
Pour notre objectif, chaque cube aura la taille de 64x64x64 unités, car cela facilitera les nombreux calculs à venir. Le monde est constitué de la manière suivante.

http://zupi.free.fr/PTuto/images/upload/Raycasting1.jpg

3) Définition des attributs de projection

Maintenant le monde est établit, nous avons besoin de définir certains attributs avant de pouvoir projeter et rendre visible le monde. Plus précisément, nous avons besoin de connaître les attributs suivants:

1. La taille du joueur, le champ de vision du joueur (FOV), et la position du joueur.
2. Dimension du plan de projection.
3. Relation entre le joueur et le plan de projection.

Le joueur devrait être en mesure de voir ce qui est en face de lui. Pour ce faire, nous aurons besoin de définir un champ de vision. Le champ de vision détermine comment le joueur voit le monde en face de lui. Il est conseillé de définir un champ de vision de 60 degrés, car ce dernier donne un très bon aperçu à l'écran.
Le joueur est défini à hauteur de 32 unités, car il s'agit d'une hypothèse raisonnable étant donné que les murs (les cubes) sont de 64 unités de haut.

http://zupi.free.fr/PTuto/images/upload/Raycasting4.jpg
http://zupi.free.fr/PTuto/images/upload/Raycasting4bis.jpg

Nous avons besoin de définir un plan de projection afin que nous puissions visualiser ce que le joueur voit dans le plan de projection. Un plan de projection de 320 unités de large et 200 unités de haut est un bon choix, car c'est la résolution de la plupart des cartes vidéo VGA.

En connaissant le champ de vision (FOV) et la dimension de la projection du plan, nous pouvons calculer l'angle entre les rayons ultérieurs, et la distance entre le joueur et le plan de projection.

http://zupi.free.fr/PTuto/images/upload/Raycasting5.jpg
http://zupi.free.fr/PTuto/images/upload/Raycasting5bis.jpg
http://zupi.free.fr/PTuto/images/upload/Raycasting5bis1.jpg
http://zupi.free.fr/PTuto/images/upload/Raycasting5bis2.jpg

Alors maintenant, nous connaissons:

* Dimension de la projection du plan = 320 x 200 unités
* Centre de la projection du plan = (160.100)
* La distance du plan de projection = 277 unités
* Angle entre les rayons ultérieures = 60/320 degrés


4) Trouver l'emplacement des murs

Maintenant, il va falloir vérifier l'emplacement des murs dans notre monde. Pour cela, nous allons vérifier l'intersection verticale et horizontale entre notre rayon et le mur et cela de façon séparer.
L'étude de l'intersection horizontale entre notre rayon et le mur sert à connaitre l'emplacement des murs horizontaux dans le tableau 2D. Tandis que l'étude de l'intersection verticale entre notre rayon et le mur sert à connaitre l'emplacement des murs verticaux dans le tableau 2D.
Tout d'abord, nous allons vous présenter le monde et les intersections avec les murs horizontaux afin de mieux visualiser la façon de connaitre l';emplacement des murs.

http://zupi.free.fr/PTuto/images/upload/Raycasting6.jpg

Donc comme vous pouvez le constater, le point P représente notre emplacement. Chaque case est un carré de dimension 64*64. Les cases blanches représentent des emplacements vides, tandis que les cases de couleurs représentent des murs.

4.1) Les intersections horizontales

http://zupi.free.fr/PTuto/images/upload/Raycasting7.jpg
http://zupi.free.fr/PTuto/images/upload/Raycasting7bis.jpg
http://zupi.free.fr/PTuto/images/upload/Raycasting7bis1.jpg

Maintenant, nous allons réaliser les étapes, qui vont permettre de trouver des intersections avec des lignes de la grille horizontale :

1. Trouver des coordonnées de la première intersection (point A dans cet exemple de coordonnée (1,2)).
Si le rayon est positionné vers le haut alors
A.y = int(PlayerPtY/64) * (64) - 1;
Si le rayon est positionné vers le bas alors
A.y = int(PlayerPtY/64) * (64) + 64;
Et on a :
A.x = PlayerPtX + (PlayerPtY-A.y)/tan(ALPHA);

2. Trouvez Ya. (Note: Ya est juste la hauteur de la grille, mais si le rayon est orienté vers le haut, Ya sera négative, si le rayon est en bas, Ya sera positive.)

3. Trouvez Xa en utilisant l'équation Xa = 64/tan(a). Xa est la distance qui sépare deux points d'intersections successifs sur l'axe des abscisses (ex : A et C).

4. Vérifiez la grille à l'intersection. S'il ya un mur sur la grille, on cesse de calculer cette distance.

5. S'il n'y a pas de mur, étendre le rayon à la prochaine intersection. A Noter que les coordonnées du prochain point d'intersection de appeler (Xnew, Ynew) est Xnew = Xold + Xa, et Ynew = YOld + Ya.

4.2) Les intersections verticales


http://zupi.free.fr/PTuto/images/upload/Raycasting8.jpg

Ensuite, nous allons réaliser les étapes, qui vont permettre de trouver des intersections avec des lignes de la grille verticale :

1. Trouver les coordonnées de la première intersection (point B dans cet exemple).
Le rayon est confrontée à droite de la photo, de sorte
Bx = int (PlayerPtX/64) * (64) + 64.
Si les rayons ont été confrontés à gauche
Bx = int (PlayerPtX/64) * (64) - 1.
Et on a :
By = PlayerPtXY + (PlayerPtX-x) * tan (AngleDeVision);

2. Trouvez Xa. (Note: Xa n'est que la largeur de la grille, mais si le rayon est confronté à droite, Xa sera positif, si le rayon est confronté à gauche, Xa sera négatif.)

3. Trouvez Ya en utilisant l'équation
Ya = 64*tan(AngleDeVision).

4. Vérifiez la grille à l'intersection. S'il ya un mur sur la grille, on cesse de calculer cette distance.

5. S'il n'y a pas de mur, étendre le rayon à la prochaine intersection. A Noter que les coordonnées du prochain point d'intersection de appeler (Xnew, Ynew) est Xnew = Xold + Xa, et Ynew = YOld + Ya.

4.3) Calcul de la distance entre un mur et le joueur, et calcul de la hauteur d'un mur

Maintenant que l'emplacement des murs est connu, nous allons calculer la distance entre le joueur et les mur, afin de déterminer le mur le plus proche.
On pose :
-mur trouver de façon horizontale : A(x,y)
-mur trouver de façon verticale : B(x1,By1)

Donc il faut effectuer les calculs suivants :
PA = abs (PlayerPtX-x) / cos(AngleDeVision)
PB = abs (PlayerPtX-x1) / cos(AngleDeVision)

Ensuite on compare les 2 résultats, et on sélectionne le plus court, qu'on appellera distanceIncorrecte. En effet, si l'on utilise cette distance, nous aurons un problème à l'affichage, le "fishbowl effect", c'est pour cette raison que nous devons recalculer cette distance grâce a la formule suivante :

correcteDistance = distanceIncorrecte * cos(Beta)
Beta = +30 pour les rayons à gauche du rayon du milieu
ou
Beta = -30 pour les rayons à droite du rayon du milieu

Ce dernier va nous servir à calculer la hauteur du mur, grâce a la formule suivante :

hauteurMur = ((hauteurMur = 64) / correcteDistance) * (distance du plan de projection = 277 )

Ensuite, il ne reste plus qu'à définir une couleur au mur en lui assignant un numéro dans le tableau 2D qui représentera un mur avec une couleur.
Et enfin, il faudra afficher tous les pixels de l'écran en indiquant indiquant le plus bas et le plus élevé pixel à remplir de la bande actuelle.









III - Tutoriel création S3DE

Il s'agit maintenant de réaliser notre moteur, le code est divisé en plusieurs parties précédées d'explications sur l'objectif de ces parties.


1) Entêtes et variables globales

On commence par inclure les librairies dont on va se servir.

Code C:
 
#include <cmath>
#include <string>
#include <vector>
#include <iostream>
#include <SDL.h>
 


Déclaration des variables globales et des macros qui vont nous servir tout au long du programme

Code C:
 
int w; ///Largueur de l'ecran
int h; ///Hauteur de l'ecran
Uint8* inkeys; ///Permet la gestion des frappes sur les touches du clavier
SDL_Event event; ///Permet de gerer les différents evenements de SDL
SDL_Surface* scr; ///La surface simple de SDL utilisée
#define MAPWIDTH 24 /*! def Nombre de colonnes de la carte de jeu */
#define MAPHEIGHT 24 /*! def Nombres de lignes de la carte de jeu */



Le programme que nous avons réalisé utilise une dizaine de fonctions qui sont plus amplement détaillées ICI (doc technique). Dans cette partie nous nous limiterons à la fonction main() et aux grands principes du moteur 3D.



2) Main: initilisation

Initialisation des variables propres au main(), elles sont de type double pour plus de précision:

Code C:
 
double posX = 2, posY = 2; ///x et y position de depart
double dirX = -1, dirY = 0; ///initialisation du verteur directeur
double planeX = 0, planeY = 0.66; ///Raycaster la deuxieme version du plan de camera
 
double time = 0; ///Temps de la sequence courante
double oldTime = 0; ///Temps de l'image precedente
 
screen(512, 384, 0, "Raycaster");/// definition des parametres de l'ecran (512x384 avec le Titre "Raycaster")
 
 



On utilise alors une boucle qui enregistre les frappes sur le clavier et qui vérifie que l'utilisateur ne souhaite pas quitter l'application. Tant que celui ci ne ferme pas la fenêtre, on effectue les traitements nécessaires à l'affichage et aux éventuels déplacements du joueur.

Code C:
 
while(!done()) /// tant que la fonction done() retourne faux, on ne quitte pas le programme
{
....
 




3) Boucle d'affichage

Pour l'affichage, les calculs s'effectueront d'abord sur la ligne de pixel vertical la plus à gauche de l'écran puis sur celle à sa droite et ainsi de suite jusqu'à atteindre la ligne la plus à droite de l'écran. On effectue ainsi un balayage de l'écran.
Pour chaque ligne on collecte les différentes données nécessaires à son affichage sur l'écran.

Initialisation des variables liées à la position actuelle du joueur

Code C:
 
for(int x = 0; x < w; x++)
{
///calcul de la position et de l'orientation du rayon
double cameraX = 2 * x / double(w) - 1; ///Coordonner x dans l'espace de la camera
double rayPosX = posX; /// Coordonner x de rayon dans l'espace
double rayPosY = posY; /// Coordonner y de rayon dans l'espace
double rayDirX = dirX + planeX * cameraX;
double rayDirY = dirY + planeY * cameraX;
///Dans quelle case de la carte, nous nous situons au debut
int mapX = int(rayPosX);
int mapY = int(rayPosY);
 
///Longueur des rayons de la position actuelle au prochain x ou y
double distMurX; ///Distance entre le joueur et 1e prochain mur vertical
double distMurY; ///Distance entre le joueur et 1e prochain mur horizontal
 
///Longueur des rayons de la position du x ou y actuel au prochain x ou y
double dist2MurX = sqrt(1 + (rayDirY * rayDirY) / (rayDirX * rayDirX)); ///Distance entre deux murs verticaux
double dist2MurY = sqrt(1 + (rayDirX * rayDirX) / (rayDirY * rayDirY)); ///Distance entre deux murs horizontaux
double longueurMur;
 



Il s'agit maintenant de calculer la distance entre le joueur et le prochain mur.
Cette distance permettra de connaître quelle place en hauteur occupera le mur sur la ligne à afficher. Pour cela on se déplace de case en case et on effectue un test sur chaque case jusqu'à ce que l'on rencontre un mur.

Code C:
 
///Etape du prochain mouvement
int etapeX; ///si gauche x=-1, si droite x=+1
int etapeY; ///si avance y=+1 et si recule y=-1
 
int touche = 0; ///Y a-t-il un mur de detecter?
int murVertiOuHori; ///est-ce un mur horizontal ou vertical?
 
///Calcul le sens de la prochaine etape et la distance entre le joueur et le mur vertical le plus proche en fonction de la prochaine etape x
if (rayDirX < 0) /// Si le rayon est oriente vers la gauche
{
etapeX = -1;
distMurX = (rayPosX - mapX) * dist2MurX; /// On calcule la distance entre joueur et 1e prochain mur vertical
}
else ///Si le rayon est oriente vers la droite
{
etapeX = 1;
distMurX = (mapX + 1.0 - rayPosX) * dist2MurX; /// On calcule la distance entre joueur et 1e prochain mur vertical
}
///Calcul le sens de la prochaine etape et la distance entre le joueur et le mur horizontal le plus proche en fonction de la prochaine etape y
if (rayDirY < 0)
{
etapeY = -1;/// On recule
distMurY = (rayPosY - mapY) * dist2MurY;/// On calcule la distance entre le joueur et 1e prochain mur horizontal
}
else
{
etapeY = 1;/// On avance
distMurY = (mapY + 1.0 - rayPosY) * dist2MurY;/// On calcule la distance entre joueur et 1e prochain mur horizontal
}
///lance de DDA = Digital Differential Analysis (algorithme de detection des murs)
while (touche == 0)
{
///Saute au prochain carre de la map soit vers la direction x, soit vers la direction y en fonction du mur le plus proche
if (distMurX < distMurY)
{
distMurX += dist2MurX;
mapX += etapeX;
murVertiOuHori = 0; ///Mur vertical
}
else
{
distMurY += dist2MurY;
mapY += etapeY;
murVertiOuHori = 1; ///Mur vertical
}
///Verifier si le rayon a detecte un mur
if (worldMap[mapX][mapY] > 0) touche = 1;
}
 



On effectue le calcul de la hauteur du mur à afficher et on enregistre le plus bas et le plus haut des pixels qu'il faudra colorier dans la ligne actuelle puis on appelle une fonction qui va dessiner la ligne actuelle (et colorier les pixels présents entre les pixels sauvegardés précédemment).

Code C:
 
///Calcul de la distance projetee sur la direction de la camera (La distance oblique donnera un effet fisheye !)
if (murVertiOuHori == 0)
longueurMur = fabs((mapX - rayPosX + (1 - etapeX) / 2) / rayDirX);
else
longueurMur = fabs((mapY - rayPosY + (1 - etapeY) / 2) / rayDirY);
 
///Calculer la hauteur de la ligne appelee a l'ecran
int hauteurMur = abs(int(h / longueurMur));
 
///Calculer le plus bas et le plus eleve des pixels a remplir dans la bande actuelle
int drawStart = -hauteurMur / 2 + h / 2;
if(drawStart < 0)drawStart = 0;
int drawEnd = hauteurMur / 2 + h / 2;
if(drawEnd >= h)drawEnd = h - 1;
 
/// Definition de la couleur des murs
Uint8 colorR;
Uint8 colorG;
Uint8 colorB;
switch(worldMap[mapX][mapY])
{
case 1: colorR = 0xFF; colorG = 0x00; colorB = 0x00; break; ///rouge
case 2: colorR = 0x00; colorG = 0xFF; colorB = 0x00; break; ///vert
case 3: colorR = 0x00; colorG = 0x00; colorB = 0xFF; break; ///bleu
case 4: colorR = 0xFF; colorG = 0xFF; colorB = 0xFF; break; ///blanc
default: colorR = 0xFF; colorG = 0xFF; colorB = 0x00; break; ///jaune
}
 
///On applique une couleur differente pour les murs verticaux (sinon on ne "ressent pas" la 3D)
if (murVertiOuHori == 1)
{
colorR /= 2;
colorB /= 2;
colorG /= 2;
}
///Dessine les pixels de l'ecran par ligne verticale
verLine(x, drawStart, drawEnd, colorR, colorG, colorB);
} ///Fin de la boucle for
 


Une fois que toutes les lignes verticales sont dessinées, on efface l'écran pour pouvoir balayer à nouveau les lignes et les dessiner une par une. Néanmoins ce rafraîchissement de l'écran est très rapide donc l'image est fluide et on ne voit pas d'écran noir.

Code C:
 
oldTime = time;
time = getTicks();
double frameTime = (time - oldTime) / 1000.0; /// temps de rafraichissement de l'ecran en seconde
 
redraw();///Actualisation de l'ecran
cls(); ///On efface l'ecran
 




4) Déplacements du joueur

On va maintenant s'intéresser aux déplacements du joueur.
On commence par définir la vitesse du déplacement (lorsque le joueur fait un pas) puis la vitesse de rotation(lorsque le joueur tourne vers la gauche ou la droite).
Ensuite on va détecter si une touche est pressée (on a vu que SDL permettait aussi de faire ça). Seules les touches directionnelles (HAUT/BAS/GAUCHE/DROITE)nous intéressent, les autres seront donc ignorées.
Lorsque les touches HAUT/BAS seront pressée, on modifiera la position du joueur en prenant en compte la vitesse de déplacement. Dans le cas des touches GAUCHE/DROITE on prendra en compte la vitesse de rotation afin de mettre à jour le vecteur directeur.

Code C:
 
///Variables des vitesses
double moveSpeed = frameTime * 5.0; ///la constante est en squares/second
double rotSpeed = frameTime * 3.0; ///la constante est en in radians/second
inkeys = SDL_GetKeyState(NULL);
 
///Avancer s'il n'y a pas de mur en face
if ((inkeys[SDLK_UP] != 0))
{
if(worldMap[int(posX + dirX * moveSpeed)][int(posY)] == false) posX += dirX * moveSpeed;
if(worldMap[int(posX)][int(posY + dirY * moveSpeed)] == false) posY += dirY * moveSpeed;
}
///Reculer s'il n'y a aucun mur derriere soi
if ((inkeys[SDLK_DOWN] != 0))
{
if(worldMap[int(posX - dirX * moveSpeed)][int(posY)] == false) posX -= dirX * moveSpeed;
if(worldMap[int(posX)][int(posY - dirY * moveSpeed)] == false) posY -= dirY * moveSpeed;
}
///Effectuer une rotation vers la droite
if ((inkeys[SDLK_RIGHT] != 0))
{
///la direction ainsi que le plan de la caméra doivent également effectuer une rotation
double oldDirX = dirX;
dirX = dirX * cos(-rotSpeed) - dirY * sin(-rotSpeed);
dirY = oldDirX * sin(-rotSpeed) + dirY * cos(-rotSpeed);
double oldPlaneX = planeX;
planeX = planeX * cos(-rotSpeed) - planeY * sin(-rotSpeed);
planeY = oldPlaneX * sin(-rotSpeed) + planeY * cos(-rotSpeed);
}
///effectuer une rotaion vers la gauche
if ((inkeys[SDLK_LEFT] != 0))
{
///la direction ainsi que le plan de la caméra doivent également effectuer une rotation
double oldDirX = dirX;
dirX = dirX * cos(rotSpeed) - dirY * sin(rotSpeed);
dirY = oldDirX * sin(rotSpeed) + dirY * cos(rotSpeed);
double oldPlaneX = planeX;
planeX = planeX * cos(rotSpeed) - planeY * sin(rotSpeed);
planeY = oldPlaneX * sin(rotSpeed) + planeY * cos(rotSpeed);
}
 


Nous sommes à la fin de la boucle while, les phases d'affichage et de déplacement se succéderont tant que l'utilisateur n'aura pas décidé de quitter. Nous sommes donc également à la fin de notre programme =)

La source du moteur est téléchargeable ici



Conclusion
Nous voici arrivé à la fin de ce petit tutorial.
Bien entendu, vous pouvez aller beaucoup plus loin dans le développement du moteur en ajoutant des textures sur les murs ou encore en blittant quelques objets de décor comme un pistolet au milieu en bas de l'écran afin de vous prendre pour un "céréales" killer :)
D'ailleurs, il n'est pas exclu que nous continuions à développer un peu le moteur afin de le rendre encore plus amusant. Stay tuned ;) !



Stats :