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.