Sujet du projet

Transcription

Sujet du projet
UJF
RICM3
POLYTECH
Année 2013-14
Algorithmique et Programmation Fonctionnelle
Projet - Binary Space Partitioning
Objectifs
Le but de ce projet est de programmer un jeu simple Doom : un personnage contrôlé au clavier
évolue dans un labyrinthe en 3D. Le squelette de l’application est fourni et contient une grande partie
du code, notamment les modules liés à la géométrie et à l’affichage de polygones en 3D. Initialement,
l’application permet l’affichage d’un polygone décrit par ses coordonnées en 3D tel qu’il est vu par
un observateur. Considérons par exemple la pièce dont la vue de dessus est donnée dans la figure 1.
Imaginons un observateur qui se situerait à l’endroit marqué par la croix faisant dos au mur 1. Le but
principal du projet est d’être capable d’afficher la partie droite de la figure à partir de la représentation
de gauche.
5
4
7
3
6
8
2
1
Figure 1 – Exemple d’un monde
Pour réaliser le jeu, les principales étapes sont :
1. Afficher une liste de polygones.
2. Modéliser le monde (labyrinthe, pièce, etc...).
3. Trier une liste de polygones à afficher de telle sorte que les polygones les plus proches de
l’observateur apparaissent en premier.
4. Modifier la position de la caméra de manière interactive (i.e. déplacer le personnage).
5. Lire un fichier de configuration contenant les données du jeu.
6. Gérer les collisions entre le personnage et son environnement.
Une partie importante du projet concerne la technique de Binary Space Partitioning (BSP). Il
s’agit d’une méthode de division de l’espace qui a été très largement utilisée dans les jeux vidéo à la
Doom. Pour le reste, le projet fait appel à des techniques déjà vues, telles que le système de modules,
les flots...
Rappel : Certaines constructions syntaxiques qui vous seront nécessaires ne sont pas données ou
décrites en détail. Pour plus de précisions référez-vous à la documentation en ligne de Caml : http:
//caml.inria.fr/pub/docs/manual-ocaml/. A titre indicatif, vous pouvez également consulter les
conventions et conseils pour la présentation des programmes Caml, préconisés par les concepteurs du
langage : http://caml.inria.fr/resources/doc/guides/guidelines.fr.html.
1
Prise en main de l’application (2 points)
Récupérer les fichiers du projet disponibles sur le site web du cours. Vous pouvez compiler
l’application à l’aide du fichier Makefile fourni. La commande make génère un fichier exécutable
./main.native. Initialement, le répertoire contient un exemple de pièce du jeu piece.ml et l’application composée d’un fichier main.ml et des trois modules suivants :
Geometrie contient deux réalisations de la signature Geom, respectivement Geom2D et Geom3D, qui
permettent de manipuler des objets (points, polygones etc . . .) soit en 2D soit en 3D.
Camera définit le type des caméras. Une caméra est essentiellement définie par la position de l’observateur dans un espace en 3D et un écran de projection. Ceci est illustré par la figure 2.
Figure 2 – Un observateur, un écran et un objet du monde
Affichage fournit des fonctions pour ouvrir une fenêtre graphique et afficher dans cette fenêtre un
polygone 3D tel qu’il est vu par l’observateur à travers l’écran de la caméra.
Vous trouverez plus d’informations dans les signatures des différents modules.
Remarque : Le fichier Makefile utilise la commande ocamlbuild pour la compilation. Pour des
raisons d’efficacité du code obtenu, il compile votre programme non pas avec ocamlc mais avec
ocamlopt (qui produit du code natif plutôt que du bytecode — cf. précis de compilation).
Exercice 1 (Familiarisation avec l’application) (1 point)
– Compiler et éxécuter le programme.
– Lire, comprendre et expliquer :
– le fichier main.ml
– les interfaces des différents modules, et notamment les fonctions utilisées dans main.ml.
– Donner l’ordre de dépendance entre les différents modules. Vous pouvez alors compiler vos fichiers de la manière habituelle et ainsi créér votre propre Makefile n’utilisant pas ocamlbuild.
– Modifier ensuite main.ml afin de :
1. Changer l’ordre d’affichage des polygones.
2. Modifier la position de la caméra.
3. Modifier l’angle de la caméra.
Pour les deux dernières questions, utiliser les fonctions de déplacement de la caméra du module
Camera.
Exercice 2 (Affichage de plusieurs polygones) (1 point)
1. Dans main.ml, écrire une fonction display_polygones qui affiche une liste de polygones, en
respectant l’ordre de la liste.
2. Écrire une fonction qui convertit une liste d’éléments de type (float*float*float) array en
une liste de polygones.
3. Tester votre fonction display_polygones sur la liste définie dans le fichier piece.ml.
4. Déplacer cette fonction dans le module Affichage et modifier main.ml pour pouvoir l’utiliser.
2
Binary Space Partitioning (3 points)
Le Binary Space Partitioning, abrégé en BSP, est une méthode utilisée par les moteurs de jeu
pour dessiner les éléments fixes dans l’environnement du personnage (les murs essentiellement) mais
également pour gérer les jeux de lumière ou détecter des collisions entre le personnage et son environnement. Le premier jeu à avoir employé cette technique est Doom. Elle a ensuite été la norme
pendant longtemps.
Un BSP est un arbre qui contient une représentation du monde — c’est à dire de l’ensemble des
murs — dans lequel le joueur évolue. Cet arbre est indépendant de la position du joueur dans le
monde et il est calculé une seule fois. Tous les calculs nécessaires à l’affichage des murs du monde, à
la gestion des collisions etc. sont alors réalisés en parcourant ce BSP.
Reprenons l’exemple de la figure 1. Cette pièce est constituée de 8 murs, représentés par 8 segments.
Nous classons les murs du monde selon le principe suivant : nous choisissons un des murs comme
“pivot”, et nous trions récursivement le reste des murs selon qu’ils sont à gauche ou à droite de la
droite (le plan si nous sommes en 3D) portée par le pivot. Les murs qui intersectent cette droite (comme
le segment 5 de l’exemple 3) sont coupés en deux. Dans la figure 3 nous donnons une illustration de
notre méthode de construction d’un BSP à partir de l une liste de segments (nous sommes donc dans
un univers en 2D) représentant l’ensemble des murs du monde de la figure 1. Nous supposons que l
est égale à [2;8;4;6;1;3;5;7] (pour plus de lisibilité les segments sont représentés par un numéro).
Nous donnons une méthode qui construit un arbre BSP représentant ce monde.
1. si l est la liste vide ou contient un seul segment, alors on rend l’arbre BSP constitué d’une feuille
étiquetée par l ;
2. sinon, nous choisissons une droite (en 3D un plan) d pour partitionner notre plan (en 3D notre
espace) ; nous prenons toujours celle qui correspond au premier segment de la liste ;
3. nous répartissons le reste de la liste en distinguant les segments situés à gauche de d, ceux à droite
de d et ceux qui intersectent d (si certains segments sont portés par d ils sont arbitrairement
considérés comme étant situés à droite de d) ;
4. nous divisons les segments qui intersectent d en deux, un de chaque côté de d ;
5. nous appelons récursivement notre méthode sur la liste des segments à gauche de d et sur celle
des segments à droite de d, puis nous renvoyons le noeud étiqueté par le premier segment de la
liste (celui qui correspond à d), et dont les fils sont les sous-arbres BSP obtenus lors des appels
récursifs.
Remarque : Si la liste était triée au départ, l’arbre obtenu serait beaucoup moins bien équilibré.
Cette méthode est applicable à un espace en n’importe quelle dimension finie du moment que l’on
définit ce qu’est un point et un plan en dimension n, ainsi que les différentes opérations sur ces objets
dont nous avons besoin (intersection, etc...). Nous utilisons donc un foncteur Bsp paramétré par la
signature Geometrie.Geom pour coder l’ensemble de nos fonctions de manipulation et construction
d’arbres BSP. Cela nous permet, en instanciant différemment notre foncteur, de travailler dans des
univers 2D ou 3D.
Exercice 3 Le but de cet exercice est de créer le module Bsp dont la signature est donnée dans le
fichier bsp.mli.
1. Créer un fichier bsp.ml.
2. Implanter et tester la méthode build_bsp à l’aide des types fournis dans la signature et en
suivant la méthode BSP.
5a
4
3
d
5a
5ba
5b
4
7
3
d
6
5bb
5ba
8
2
7
d
6
1
8
1
d
1
5bb
7
6
d
Figure 3 – Exemple de BSP
3
Algorithme du peintre (4 points)
Nous savons comment afficher une liste de polygones et comment représenter un monde. Il ne nous
reste plus qu’à afficher correctement le monde en fonction de la position de la caméra. Reprenons
l’exemple de la figure 1. Si nous dessinons les murs sans nous préoccuper de savoir lesquels sont
visibles et lesquels sont cachés de l’observateur, nous pouvons obtenir (entre autres) les deux vues de
la figure 4, parmi lesquelles une seule est satisfaisante.
Figure 4 – Deux affichages possibles d’un même monde.
L’ordre dans lequel les polygones sont affichés est important pour un rendu correct. Il existe
plusieurs méthodes pour dessiner un monde vu d’un certain point de vue. Nous utilisons celle dite du
peintre pour déterminer dans quel ordre les polygones doivent être affichés, en fonction de la position
de l’observateur. Cette technique consiste à dessiner les murs en commençant par les plus éloignés et
en terminant par les plus proches, de sorte que les murs cachés à l’observateur soient recouverts par
les murs qui sont placés devant. Pour cela nous utilisons le BSP afin de déterminer quels murs sont
les plus proches de l’observateur et lesquels sont les plus éloignés.
Exercice 4 Écrire la fonction painter qui prend en entrée un arbre BSP et un point p, et qui rend
la liste des polygones de l’espace représenté par l’arbre BSP triée dans l’ordre d’affichage du point de
vue de l’observateur placé en p, de manière à afficher d’abord les polygones les plus éloignés, puis les
plus proches. L’algorithme fonctionne de la façon suivante :
– si l’arbre est une feuille, nous renvoyons son contenu
– si l’arbre est un noeud, il est étiqueté par un polygone. Si nous nous trouvons d’un côté de
ce polygone, nous devons afficher en premier les polygones qui se trouvent de l’autre côté, et
ensuite ceux qui se trouvent du même côté. Sinon, l’ordre n’a pas d’importance.
Tester votre fonction sur l’exemple de piece.ml.
Exercice 5 Instancier correctement le foncteur Bsp et l’utiliser pour un rendu correct de piece.ml.
4
Déplacement (1 point)
Afin de voir mieux visualiser si votre affichage d’une pièce est correct nous allons déplacer la
caméra en pressant certaines touches.
Exercice 6 Écrire une fonction qui réagit aux touches de déplacement du pavé numérique en modifiant l’image de la façon suivante : les flèches → et ← modifient l’orientation du regard vers la
droite ou la gauche, les flèches ↑ et ↓ font avancer ou reculer l’observateur. Nous utilisons la fonction read_key du module Graphics. (Si vous n’arrivez pas à faire fonctionner les touches du pavé
numérique, veuillez nous reporter l’erreur produite et vous pouvez utiliser d’autres touches convenablement choisies).
Remarque : Pour éviter que l’image ne clignote à chaque déplacement, nous pouvons utiliser
les primitives auto_synchronize et synchronize fournies dans la rubrique Graphics de la
librairie standard au paragraphe Double buffering .
5
2.5D (3 points)
Nous appelons jeux de tir subjectif les jeux en 3D à la Doom ou Quake, dans lesquels l’angle de vue
proposé simule le champ visuel du personnage incarné. En réalité, ces jeux ne manipulent en interne
que des représentations en 2D de l’environnement du personnage. Par exemple si tous les murs ont
une hauteur fixe alors une vue de dessus, c’est à dire une représentation en 2D, est suffisante pour
définir intégralement un espace. Tous les calculs de position des murs les uns par rapport aux autres,
de gestion de la lumière ou des collisions sont effectués en 2D, et le passage à la 3D a lieu au moment
de l’affichage. Nous parlons dans ce cas de 2,5D. Jusqu’à présent, le jeu était décrit comme une liste
de polygones 3D. Une alternative est de décrire le jeu comme une liste de segments 2D (correspondant
à des murs sans hauteurs), est d’effectuer les calculs de BSP sur cette structure. L’affichage final se
fera après conversion des segments en murs en leur attribuant une hauteur fixée.
Exercice 7 Réécrire la pièce comme une liste de segments 2D et modifier votre programme afin qu’il
utilise une instanciation du foncteur Bsp avec Geom2D et non plus Geom3D. Vous pouvez vous aider
des fonctions du module Geom.Geom2d3d.
Dans la suite, nous utilisons une description 2.5D du jeu.
6
Lecture du fichier de configuration (3 points)
Pour le moment, la description du jeu (pièce, position initiale, paramètres de luminosité, description de la caméra, pas et angles de déplacement) est faite dans le fichier main.ml. Pour rendre
l’application plus modulaire, nous nous proposons de créer un module Game2D qui fournit ces informations au reste de l’implantation via le type enregistrement Game2D.t.
Exercice 8 Implanter le module Game2D dont la signature est donnée dans game2d.mli. Les informations de jeu de main doivent être reportées dans ce nouveau module.
Écrire ces informations ”en dur” oblige à recompiler l’application à chaque changement des paramètres de jeu. Par ailleurs, cela interdit à un utilisateur non programmeur de modifier ces paramètres. Il est plus judicieux de les définir dans un fichier de configuration qui sera chargé au
démarrage de l’application.
Exercice 9 Modifier la signature de Game2D. La valeur make devient : val make: string -> t.
Dans l’implantation du module, cette fonction doit lire le fichier dont le nom est donné en entrée, et
construire la valeur de type t correspondante. Votre fonction doit pouvoir interpréter convenablement
le fichier game donné. Le format d’un fichier game devra respecter la structure du fichier donné en
exemple. Vous fournirez différents exemples de mondes de votre conception. Nous testerons aussi que
votre programme fonctionne bien sur nos propres fichiers.
Remarque : pour utiliser les constructions parser sur les flots, il faut rajouter l’option
-pp "camlp4o.opt -unsafe" à ocamlbuild pour la compilation.
7
Gestion des collisions (4 points)
Pour interdire au joueur de traverser les murs, une technique simple consiste à calculer s’il y a
une intersection entre la position de caméra courante, et la position de caméra après le déplacement
demandé par le joueur. S’il y a intersection, le déplacement n’est pas validé et la position courante
reste inchangée.
Exercice 10 Implanter la gestion des collisions. Pour cela, définir une fonction
val collision : t -> point -> point -> bool dans Bsp.mli et l’utiliser dans main.ml.
Cette fonction utilise la fonction intersection_segment du module géométrie.
Exercice 11 La technique utilisée n’est pas tout à fait satisfaisante.
1. Identifier le problème, en donnant un exemple.
2. Corriger le problème en améliorant la gestion des collisions.
Extensions (5 points)
Cette partie est facultative et fait appel à des notions qui n’ont pas forcément été abordées en
cours.
7.1
Déplacement en 3D (1 point)
Concevoir des commandes pour faire voler le personnage afin qu’il entre dans des objets 3D,
comme l’éponge de menger : http://en.wikipedia.org/wiki/Menger_sponge.
7.2
Vue arrière (2 points)
Afin de pouvoir fuir les éventuels “méchants” (comme dans Doom ou PacMan), il serait utile
d’avoir une vue arrière du monde.
Exercice 12 Comprendre les mécanismes d’affichage de fenêtres graphiques pour rajouter une vue
arrière du monde.
7.3
Porte (2 points)
En utilisant des threads, il est possible de rendre le monde “dynamique”, afin de rendre le
déplacement du joueur plus difficile. Nous construisons des portes qui s’ouvrent et se ferment toutes
seules. Ces portes ne sont en fait que des polygones qui se déplacent tous seuls dans une direction et
qui lorsqu’ils touchent un mur rebondissent et repartent dans la direction inverse.
Exercice 13 (Portes) Implémenter une porte dans un couloir.
Rendu
Le projet est à réaliser en binôme, les binômes étant ceux définis en début d’année avec Michaël
Périn pour toutes les matières.
Le barème est donné à titre indicatif. Le projet en entier est à rendre par email à l’ensemble de vos
enseignants, impérativement avant le 20 décembre 2013 minuit sous la forme d’un fichier noms.tar.
Seuls les projets respectant les contraintes suivantes seront corrigés :
1. noms correspond aux noms des membres du groupe
2. la commande tar xvf nom.tar génère un répertoire noms qui contiendra les sources de votre
application, un fichier Makefile, un fichier Readme (et rien d’autre).
3. la commande make génère un fichier exécutable main.native ou main.
Par ailleurs, Le fichier README doit contenir :
– le principe du projet en une dizaine de lignes ;
– exercice par exercice, la liste de ce qui marche et ce qui ne marche pas ;
– si vous vous êtes fait aider, par qui et à quel endroit ;
– les commandes à entrer pour compiler votre programme.
– comment utiliser votre programme, quels fichiers faut-il charger, quelles sont les touches pour
le déplacement, les différents exemples de monde que vous avez programmer etc ...
Dans les fichiers d’interface, et directement en entête de chaque fonction, vous préciserez :
– le type de la fonction ;
– le rôle de la fonction et à quoi correspondent ses entrées et ses sorties ;
– si besoin est, un commentaire sur les choix d’implémentation que vous avez faits, et plus
généralement toute information susceptible d’aider à la bonne lecture du code.

Documents pareils