Page 1 of 1

[TUTO] Réaliser un tunnel en 3D

Unread postPosted: 08 Oct 2012, 17:15
by matref
Salut les gens !

Pour ceux qui auraient vu mon récent projet SpeedX 3D, vous vous êtes peut-être possiblement éventuellement demandé (mais c'est pas sûr) comment j'avais réalisé cet effet tunnel 3D, qui est ma foi fort sexy si je ne m'abuse !

Et bien cet effet, non seulement il est super pratique pour épater la galerie, mais en plus il est complètement facile à coder ! :D

Et c'est justement le sujet de ce tuto, coder un tunnel en 3D avec textures, rotations et mouvements du centre s'il vous plaît !

Le principe

Pour réaliser ce tunnel 3D, et avant la pratique, il va forcément falloir un peu de théorie (un peu hein, moi même j'aime pas la théorie :P), de façon à savoir comment réaliser tout ça avant de le faire pour de vrai.

Le principe donc, c'est qu'on va utiliser une texture, c'est à dire une image qu'on va appliquer sur les murs du tunnel mais en la déformant pour donner l'illusion de profondeur. Sauf qu'on peut pas afficher l'image déformée comme ça, donc on va parcourir tous les pixels de l'écran pour afficher notre texture point par point, comme ça on pourra y appliquer l'effet de profondeur via des formules mathématiques inaccessibles au commun des mortels simples comme tout ;)

Réaliser une texture

Pour dessiner une texture, et bien c'est complètement facile : vous prenez une image et c'est fini. Bawé.

Sauf que n'importe quelle image ça peut soit donner des résultats complètement dégueu, soit être difficile à utiliser, donc le mieux est encore de prendre une texture carrée (même largeur que longueur, pour ceux qui auraient oublié leurs cours de CE1 :D).

Comme je suis trèèès gentil (mais si mais si) je vous offre une texture gratuite à utiliser pour vos tunnels 3D (et qu'on va utiliser pour le tuto) :

Image

C'est une image bitmap 16*16, donc c'est facile à utiliser. Je sais qu'elle a l'air pitite, mais ça s'adapte très bien en code. :)

Calculer l'angle

Notre tunnel est composé de plein de répétitions de cette texture, ça ok, mais comment savoir où afficher quoi ?

En fait, chaque point (et pas pixel) à afficher a une abscisse et une ordonnée, qui en fait représentent respectivement un angle et une profondeur :

Image
http://benryves.com/tutorials/tunnel/ (Anglais)

Pourquoi pas pixel ? Hé bien parce qu'un pixel est allumé sur l'écran, alors qu'un point est un objet géométrique. Je développerai ça un peu plus loin.

On doit donc calculer l'angle de chaque point par rapport au centre de l'image. Seulement, un écran de Nspire fait 320*240 pixels, soit ... 76800 angles à calculer :~o Je sais pas vous, mais une boucle où on fait 76800 calculs d'angle, ça me semble pas hyper rapide. On va donc créer ce qu'on appelle une Look Up Table (ou LUT), c'est à dire un tableau de même taille que notre écran qu'on va remplir d'une donnée une bonne fois pour toute, comme ça au lieu de recalculer X fois une valeur via un calcul qui prend une minute 30, on aura juste à aller chercher cette valeur dans la LUT. Mais ça c'est dans la partie pratique :D

Voici une image qui dit à elle toute seul comment calculer notre angle :

Image
http://benryves.com/tutorials/tunnel/ (Anglais)

Donc l'abscisse X de notre point sera calculée avec la formule :

point.x = atan(x, y)


Calculer la profondeur

Maintenant qu'on sait comment calculer l'angle, c'est à dire l'abscisse de notre point, on va passer à l'ordonnée, c'est à dire la profondeur du point dans le tunnel. Il faut effectivement penser au tunnel comme un objet en 3D et pas comme une projection sur un plan.

Ici c'est encore plus simple, puisque la profondeur se calcule à partir de la distance de chaque pixel au centre.

point.y = screen_resolution / (x² + y²)


Par contre, attention ! Pendant ce calcul, il FAUT que le centre de l'écran ait les coordonnées (0,0) ! Il faudra donc appliquer un calcul sur les valeurs x et y.

Même chose que pour l'angle, on ne va pas répéter une grosse division comme ça 76800 fois, donc on va créer une Look Up Table pour sauvegarder nos valeurs.

Calculer la couleur et afficher le point

Maintenant qu'on a l'angle et la profondeur du point, on va pouvoir calculer sa couleur et l'afficher.

"Comment ça, calculer sa couleur ?" me direz-vous (mais si vous le dites, je vous entends d'ici). Et bien je vous rappelle qu'on veut utiliser une texture, donc il faut savoir exactement quel pixel allumer ou pas.

Bah vous savez pas la meilleur ? C'est encore plus simple que le reste.

pixelColor(x,y) = texture[angle][profondeur]


Et oui, on a juste à utiliser notre texture comme une palette de couleur, et on met le pixel de l'écran de la couleur située à l'adresse (angle, profondeur) sur notre texture. C'est pas beau ça ?

La pratique

Voilà, maintenant qu'on sait de quoi il en ressort, on va pouvoir appliquer tout ça ! :#fou#:

Munissez-vous donc de votre moyen préféré de faire des programmes Ndless, et on attaque !

Prérequis

Je sais pas vous, mais j'ai pas spécialement envie de réinventer toutes les commandes du C :D On va donc utiliser quelques includes :
Code: Select all
#include <os.h>
#include <common.h>
#include <fdlibm.h>


On va avoir besoin de fonctions mathématiques (d'une seule en fait, l'arc tangente à deux arguments), donc on va devoir utiliser fdlibm.h, qui est une full implémentation de toutes les fonctions mathématiques de math.h, par Hoffa (le créateur de la nSDL en fait). Vous pouvez télécharger fdlibm sur le site de Hoffa.

Une fois téléchargée, il faut lier fdlibm à votre programme pour pouvoir utiliser ses fonctions, vu que c'est une librairie statique. Éditez le makefile de votre programme, et dans la ligne LDFLAGS écrivez ceci :
Code: Select all
LDFLAGS = -lfdm
Cette simple ligne vous permettra d'utiliser toutes les fonctions de math.h dans vos programmes Ndless ;)

Ensuite, il vous faudra aussi les commandes pour allumer un pixel et autres trucs de base relatifs à Ndless. Je vous ai créé deux petits fichiers que vous pourrez utiliser pour vos programmes (je ne vais pas détailler toutes les fonctions, vous comprendrez en creusant un peu).

Pour les utiliser, ajoutez-les dans le dossier de votre programme et ajoutez la ligne d'include à votre code :
Code: Select all
#include "utils.h"


Définir les variables

Maintenant qu'on a nos includes de prêts, on va définir les variables qu'on va utiliser. Elles ne sont pas très nombreuses :
  • Un buffer, où on va allumer les pixels plutôt que sur l'écran pour ne pas voir la scène se dessiner petit à petit.
    Code: Select all
    char *screen;
  • Nos Look Up Tables. Comme elles sont très grosses, on ne va déclarer qu'une dimension et malloc le reste.
    Code: Select all
    double (*angle_lut)[240];
    double (*depth_lut)[240];
  • Des variables qui vont contenir les coordonnées relatives du point, c'est à dire où le centre de l'écran est à (0,0).
    Code: Select all
    double relativeX, relativeY;
  • Des variables qui vont contenir les coordonnées de la couleur sur la texture.
    Code: Select all
    int textureX, textureY;
  • Des variables pour les boucles For qui vont parcourir l'écran.
    Code: Select all
    int x, y;
  • Une variable qui va contenir la couleur (on pourra éventuellement la modifier).
    Code: Select all
    char color;
  • Des variables qui vont contenir les dimensions de l'écran.
    Code: Select all
    int w = 320, h = 240;
  • Et pour finir, notre texture !
    Code: Select all
    char texture[256] = {
          0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
          0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
          0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
          0xFF,0xFF,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x00,0xFF,
          0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
          0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
          0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
          0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
          0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
          0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
          0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
          0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
          0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
          0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
          0xFF,0xFF,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x00,0xFF,
          0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF
       };
    Pour vos textures futures, vous devrez trouver un moyen de convertir des images en hexa. Pour celle-ci, j'ai utilisé SourceCoder 2.5 de Cemetech, mais il a le défaut de ne convertir pour Nspire qu'en niveaux de gris.
Code: Select all
int main(void)
{
   char *screen;
   double (*angle_lut)[240];
   double (*depth_lut)[240];
   double relativeX, relativeY;
   int textureX, textureY;
   
   int w = 320, h = 240, x, y;
   char color;
   
   char texture[256] = {
      0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
      0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
      0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0xFF,0xFF,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x00,0xFF,
      0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0xFF,0xFF,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x00,0xFF,
      0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF
   };


Initialiser les variables

On n'a que trois variables à initialiser : le buffer (parce qu'on ne connaît pas sa taille) et les LUT (parce qu'elles sont très grandes).

Pour les LUT, on connaît leur taille en éléments, mais il faut faire attention, car on ne connaît pas leur taille en octets, ni même la taille en octets de leurs éléments ; c'est donc un peu compliqué.
Code: Select all
angle_lut = malloc(w * sizeof(*angle_lut));  // un tableau de la largeur de l'écran, rempli avec des éléments de la bonne taille
if(!angle_lut) exit(0); // l'initialisation a raté

depth_lut = malloc(w * sizeof(*depth_lut));
if(!depth_lut)
{
  free(angle_lut);
  exit(0);
}


Pour le buffer, on doit le faire de la taille de l'écran. Heureusement, le SDK Ndless propose une constante appelée SCREEN_BYTES_SIZE qui est en fait la taille de l'écran en nombre d'éléments (dans os.h ou common.h, je sais plus :D). L'initialisation devient alors toute simple :
Code: Select all
screen = malloc(SCREEN_BYTES_SIZE * sizeof(char));
if(!screen)
{
  free(angle_lut);
  free(depth_lut);
  exit(0);
}



On a donc notre code entier de pour l'instant :
Code: Select all
#include <os.h>
#include <common.h>
#include <fdlibm.h>
#include "utils.h"

#define TEXTURE_WIDTH 16     // pourquoi pas
#define TEXTURE_HEIGHT 16

int main(void)
{
   char *screen;
   double (*angle_lut)[240];
   double (*depth_lut)[240];
   double relativeX, relativeY;
   int textureX, textureY;
   
   int w = 320, h = 240, x, y;
   char color;
   
   char texture[256] = {
      0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
      0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
      0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0xFF,0xFF,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x00,0xFF,
      0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
      0xFF,0xFF,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x00,0xFF,
      0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF
   };
   
   lcd_ingray();        // Pour fonctionner sur toutes caltos

   angle_lut = malloc(w * sizeof(*angle_lut));
   if(!angle_lut) exit(0);
   
   depth_lut = malloc(w * sizeof(*depth_lut));
   if(!depth_lut)
   {
      free(angle_lut);
      exit(0);
   }

   screen = malloc(SCREEN_BYTES_SIZE * sizeof(char));
   if(!screen)
   {
      free(angle_lut);
      free(depth_lut);
      exit(0);
   }


Voilà, maintenant on peut vraiment y aller ;)

Remplir les LUT

Maintenant qu'on a tout bien initialisé, on va pouvoir commencer à remplir les LUT comme il se doit.

On va donc parcourir tout l'écran pour enregistrer une valeur correspondant à chaque pixel.
Code: Select all
for(y = 0; y < h; y++)
{
  for(x = 0; x < w; x++)
  {
   
  }
}


Ensuite, on va calculer les coordonnées relatives du pixel courant. Pour cela, il faut que le milieu de l'écran soit à (0,0), donc on a juste à retrancher la moitié de la dimension à x et y.
Code: Select all
for(y = 0; y < h; y++)
{
  relativeY = h / 2 - y;   // le positif est en haut et le négatif en bas
  for(x = 0; x < w; x++)
  {
    relativeX = x - w / 2;   // le négatif est à gauche et le positif à droite
  }
}


Et on est bon pour calculer l'angle et la profondeur !

Remarque sur l'angle

En général, les commandes de trigo en programmation renvoient des radians. Ce cas-ci n'est pas une exception, la commande arc tangente renvoie bien des radians. Heureusement, il existe un moyen simple d'avoir la valeur avec l'unité de notre choix via une formule non moins simple :

angle = atan2(x,y) * (TEXTURE_WIDTH / M_PI) * (nombre_de_textures_horizontales / 2)


Avec M_PI la constante p et nombre_de_textures_horizontales le nombre de répétitions horizontales de la texture. En français (:D), ça veut dire que si vous voulez 4 répétitions horizontales de la texture, la texture sera étirée pour que seulement 4 textures mises côte à côte fasse le tour du tunnel. Pour moi ça fait peu, j'ai l'habitude d'afficher 8 répétitions.

On peut donc calculer nos coordonnées et remplir nos LUT :
Code: Select all
for(y = 0; y < h; y++)
{
  relativeY = h / 2 - y;
  for(x = 0; x < w; x++)
  {
    relativeX = x - w / 2;

    depth_lut[x][y] = w*h*2 / max(relativeX * relativeX + relativeY * relativeY, 1);   // on évite une division par 0

    angle_lut[x][y] = atan2(relativeX, relativeY) * (TEXTURE_WIDTH / M_PI) * 4;    // 8 répétitions
  }
}


Et voilà, nos tables sont remplies, c'était rapide !

Calculer la couleur

Maintenant, on entre dans la boucle du jeu, qu'on ne va quitter qu'avec l'appui sur, disons, :nses:.
Code: Select all
while(!isKeyPressed(KEY_NSPIRE_ESC))
{

}


À chaque tour de boucle, on efface le buffer, puis on parcourt l'écran en entier pour calculer la couleur.
Code: Select all
while(!isKeyPressed(KEY_NSPIRE_ESC))
{
  clearBuf(screen);
  for(y = 0; y < h; y++)
  {
    for(x = 0; x < w; x++)
    {
      textureX = angle_lut[x][y];
      textureY = depth_lut[x][y];
    }
  }
}


Le calcul de la couleur est simple : on prend l'angle correspondant dans la LUT, on lui applique un modulo pour qu'il reste dans la limite de la largeur de la texture, et on fait la même chose avec la profondeur et la hauteur de la texture. Ensuite, on multiplie la profondeur ainsi modulée par la largeur de la texture (car on pense en lignes remplies de colonnes) et on ajoute l'angle modulé.

Code: Select all
while(!isKeyPressed(KEY_NSPIRE_ESC))
{
  clearBuf(screen);
  for(y = 0; y < h; y++)
  {
    for(x = 0; x < w; x++)
    {
      textureX = angle_lut[x][y];
      textureY = depth_lut[x][y];

      color = texture[(textureY % TEXTURE_HEIGHT) * TEXTURE_WIDTH + (textureX % TEXTURE_WIDTH)];
    }
  }
}

Une optimisation très intéressante si votre texture a des dimensions en puissances de 2 (2, 4, 8, 16, 32 ...) est de remplacer le modulo par un AND bit à bit.
Code: Select all
while(!isKeyPressed(KEY_NSPIRE_ESC))
{
  clearBuf(screen);
  for(y = 0; y < h; y++)
  {
    for(x = 0; x < w; x++)
    {
      textureX = angle_lut[x][y];
      textureY = depth_lut[x][y];

       color = texture[(textureY & (TEXTURE_HEIGHT - 1)) * TEXTURE_WIDTH + (textureX & (TEXTURE_WIDTH - 1))];
    }
  }
}
Voyons pourquoi je fais ça. Prenons un nombre au hasard, disons 549. Appliquons à ce nombre un modulo 16 puis un AND 15.

549 / 16 = 34 ; reste = 5. Donc 549 % 16 = 5.

549 & 15
?
0000001000100101
&
0000000000001111
=
0000000000000101 ? 5.

On a bien le même résultat, les opérations sont donc équivalentes.

Notre variable color contient donc l'octet qu'il faut ? la couleur du pixel courant.

On a plus qu'à allumer le pixel de la bonne couleur :
Code: Select all
while(!isKeyPressed(KEY_NSPIRE_ESC))
{
  clearBuf(screen);
  for(y = 0; y < h; y++)
  {
    for(x = 0; x < w; x++)
    {
      textureX = angle_lut[x][y];
      textureY = depth_lut[x][y];

      color = texture[(textureY & (TEXTURE_HEIGHT - 1)) * TEXTURE_WIDTH + (textureX & (TEXTURE_WIDTH - 1))];
      setPixelBuf(screen, x, y, color);
    }
  }
  refresh(screen);       // on copie le buffer à l'écran
}


Et c'est fini ! On a notre tunnel 3D.

Voici le code complet :
Code: Select all
#include <os.h>
#include <common.h>
#include <fdlibm.h>
#include "utils.h"

#define TEXTURE_WIDTH 16
#define TEXTURE_HEIGHT 16

int main(void)
{
  char *screen;
  double (*angle_lut)[240];
  double (*depth_lut)[240];
  double relativeX, relativeY;
  int textureX, textureY;
   
  int w = 320, h = 240, x, y;
  char color;
   
  char texture[256] = {
    0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0xFF,0xFF,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x00,0xFF,
    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0xFF,0xFF,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x00,0xFF,
    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF
    };
   
    lcd_ingray();

    angle_lut = malloc(w * sizeof(*angle_lut));
    if(!angle_lut) exit(0);
   
    depth_lut = malloc(w * sizeof(*depth_lut));
    if(!depth_lut)
    {
      free(angle_lut);
      exit(0);
    }

    screen = malloc(SCREEN_BYTES_SIZE * sizeof(char));
    if(!screen)
    {
      free(angle_lut);
      free(depth_lut);
      exit(0);
    }

  for(y = 0; y < h; y++)
  {
    relativeY = h / 2 - y;
    for(x = 0; x < w; x++)
    {
      relativeX = x - w / 2;
 
      depth_lut[x][y] = w*h*2 / max(relativeX * relativeX + relativeY * relativeY, 1);
 
      angle_lut[x][y] = atan2(relativeX, relativeY) * (TEXTURE_WIDTH / M_PI) * 4;
    }
  }
 
  while(!isKeyPressed(KEY_NSPIRE_ESC))
  {
    clearBuf(screen);
    for(y = 0; y < h; y++)
    {
      for(x = 0; x < w; x++)
      {
        textureX = angle_lut[x][y];
        textureY = depth_lut[x][y];
 
        color = texture[(textureY & (TEXTURE_HEIGHT - 1)) * TEXTURE_WIDTH + (textureX & (TEXTURE_WIDTH - 1))];
        setPixelBuf(screen, x, y, color);
      }
    }
    refresh(screen);
  }

  // On n'oublie pas de libérer tous les malloc
  free(screen);
  free(angle_lut);
  free(depth_lut);

  return 0;
}


Résultat :

Image

Animer le tunnel

Déplacer le tunnel

Maintenant qu'on arrive à dessiner correctement notre tunnel 3D, pourquoi ne pas le faire avancer, reculer, tourner dans les deux sens ? Trop facile !

Pour cela, on a besoin de deux variables supplémentaires, tunnel_rotate et tunnel_zoom.
Code: Select all
int tunnel_rotate = 0, tunnel_zoom = 0;

Et vous savez ce que vous en faites ? Vous les ajoutez à textureX et textureY.
Code: Select all
textureX = angle_lut[x][y] + tunnel_rotate;
textureY = depth_lut[x][y] + tunnel_zoom;

Et c'est tout, c'est fini. :0

Vous n'avez plus qu'à changer la valeur de ces deux variables pour animer le tunnel :)
Code: Select all
while(!isKeyPressed(KEY_NSPIRE_ESC))
{
  clearBuf(screen);
  for(y = 0; y < h; y+=2)
  {
    for(x = 0; x < w; x+=2)
    {
      textureX = angle_lut[x][y] + tunnel_rotate;
      textureY = depth_lut[x][y] + tunnel_zoom;

      color = texture[(textureY & (TEXTURE_HEIGHT - 1)) * TEXTURE_WIDTH + (textureX & (TEXTURE_WIDTH - 1))];
      setPixelBuf(screen, x, y, color);
    }
  }
  refresh(screen);

  if(isKeyPressed(KEY_NSPIRE_LEFT)) tunnel_rotate++;
  if(isKeyPressed(KEY_NSPIRE_RIGHT)) tunnel_rotate--;
  if(isKeyPressed(KEY_NSPIRE_UP)) tunnel_zoom++;
  if(isKeyPressed(KEY_NSPIRE_DOWN)) tunnel_zoom--;
}


Ce qui donne :

Image

Houlà ... un peu lent nan ? Mais ça ne servirai à rien d'avancer ou de tourner de plus de pixel à la fois, ça resterait saccadé ...

Alors pourquoi ne pas calculer qu'un point sur quatre ? On ferait quatre fois moins de calculs ! La seule chose qui change (en mal hein) c'est que la résolution est réduite de moitié du coup, mais sur un écran aussi grand on peut se le permettre :D

Donc, il n'y a quasiment rien à faire pour changer ça, si ce n'est remplacer x++ et y++ dans les boucles for par x += 2 et y += 2.

Image

Oups ... je crois qu'on a oublié quelque chose :D Il faut effectivement afficher 4 pixels à la fois au lieu d'un.
Code: Select all
setPixelBuf(screen, x, y, color);
setPixelBuf(screen, x+1, y, color);
setPixelBuf(screen, x, y+1, color);
setPixelBuf(screen, x+1, y+1, color);


Image

Voilà, là on est bon !

Déplacer l'origine

Et pourquoi on ferait pas un joueur capable de regarder en haut, en bas, à droite, à gauche ? C'est possible, mais c'est un peu plus compliqué.

Le principe est qu'on va travailler avec 2 fois plus de valeurs X et 2 fois plus de valeurs Y (ce qui résulte en un écran 4 fois plus large), puis on va afficher les bons pixels en utilisant un offset au moment de récupérer les valeurs des deux LUT. Pour vous aider, imaginer les LUT comme deux tableaux posés sur une table, et l'écran comme un calque 4 fois plus petit que ces tableaux, que l'on va passer par dessus et déplacer en fonction des offsets.

Là logiquement vous vous dites "bah on n'a qu'à multiplier la taille des LUT par deux". HÉ BAH NON parce que je suis tellement fort que j'ai déjà pensé à ça, du coup la taille des LUT ne change pas.

Vous vous rappelez de vos boucles For d'initialisation et de dessin ? Là où on allait de 2 en 2 avec x et y ?
Code: Select all
for(y = 0; y < h; y += 2)
Et bah on n'a qu'à aller d'1 en 1 !

Code: Select all
for(y = 0; y < h; y++)
...
for(w = 0; y < w; w++)
C'est la seule chose à changer pour la phase de remplissage. Pas trop dur donc :D

Avant d'attaquer l'affichage, on va ajouter deux variables à notre liste ; elles vont contenir les offsets à utiliser pour déplacer l'origine du tunnel (le centre quoi).
Code: Select all
int tunnel_offsetX, tunnel_offsetY;


Ensuite, pour la phase d'affichage, c'est pas extrêmement plus dur : on va aussi de 1 en 1, par contre on va plus que de 0 à w / 2 (et donc aussi h / 2) :
Code: Select all
for(y = 0; y < h / 2; y++)
{
...
  for(x = 0; x < w / 2; x++)

Après ça, on prend nos deux lignes où on extrait les valeurs des LUT et on ajoute nos offsets dedans :)
Code: Select all
textureX = angle_lut[x + tunnel_offsetX][y + tunnel_offsetY] + tunnel_rotate;
textureY = depth_lut[x + tunnel_offsetX][y + tunnel_offsetY] + tunnel_zoom;


Attention ! Comme maintenant nos x et y ne vont plus que jusque w/2 et h/2, il faut les multiplier par 2 au moment d'afficher les pixels :
Code: Select all
setPixelBuf(screen, x*2, y*2, color);
setPixelBuf(screen, x*2+1, y*2, color);
setPixelBuf(screen, x*2, y*2+1, color);
setPixelBuf(screen, x*2+1, y*2+1, color);

Et c'est presque fini ! Il ne reste plus qu'à donner un moyen à l'utilisateur de gérer ces offsets.
Code: Select all
if(isKeyPressed(KEY_NSPIRE_4)) tunnel_offsetX -= (tunnel_offsetX > 0) * 2;
if(isKeyPressed(KEY_NSPIRE_6)) tunnel_offsetX += (tunnel_offsetX < w/2) * 2;
if(isKeyPressed(KEY_NSPIRE_8)) tunnel_offsetY -= (tunnel_offsetY > 0) * 2;
if(isKeyPressed(KEY_NSPIRE_2)) tunnel_offsetY += (tunnel_offsetY < h/2) * 2;

Ici, j'utilise les touches 2, 4, 6 et 8 pour augmenter ou diminuer les offsets de 2 en 2, à condition que le centre ne sorte pas de l'écran, sinon ça lit hors de la table et c'est pas bon.

Pour si peu de travail, résultat :

Image

Et je poste le code complet de chez complet :D :
Code: Select all
#include <os.h>
#include <common.h>
#include <fdlibm.h>
#include "utils.h"

#define TEXTURE_WIDTH 16
#define TEXTURE_HEIGHT 16

int main(void)
{
  char *screen;
  double (*angle_lut)[240];
  double (*depth_lut)[240];
  double relativeX, relativeY;
 
  int w = 320, h = 240, x, y;
  int textureX, textureY, tunnel_rotate = 0, tunnel_zoom = 0, tunnel_offsetX = w/4, tunnel_offsetY = h/4;
   
  char color;
   
  char texture[256] = {
    0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0xFF,0xFF,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x00,0xFF,
    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
    0xFF,0xFF,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x00,0xFF,
    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF
    };
   
    lcd_ingray();

    angle_lut = malloc(w * sizeof(*angle_lut));
    if(!angle_lut) exit(0);
   
    depth_lut = malloc(w * sizeof(*depth_lut));
    if(!depth_lut)
    {
      free(angle_lut);
      exit(0);
    }

    screen = malloc(SCREEN_BYTES_SIZE * sizeof(char));
    if(!screen)
    {
      free(angle_lut);
      free(depth_lut);
      exit(0);
    }

  for(y = 0; y < h; y++)
  {
    relativeY = h/2 - y;
    for(x = 0; x < w; x++)
    {
      relativeX = x - w/2;
         
      depth_lut[x][y] = w*h*2 / max(relativeX * relativeX + relativeY * relativeY, 1);
         
      angle_lut[x][y] = atan2(relativeX, relativeY) * (TEXTURE_WIDTH / M_PI) * 4;
    }
  }
 
  while(!isKeyPressed(KEY_NSPIRE_ESC))
{
  clearBuf(screen);
  for(y = 0; y < h/2; y++)
  {
    for(x = 0; x < w/2; x++)
    {
      textureX = angle_lut[x + tunnel_offsetX][y + tunnel_offsetY] + tunnel_rotate;
      textureY = depth_lut[x + tunnel_offsetX][y + tunnel_offsetY] + tunnel_zoom;

      color = texture[(textureY & (TEXTURE_HEIGHT - 1)) * TEXTURE_WIDTH + (textureX & (TEXTURE_WIDTH - 1))];
      setPixelBuf(screen, x*2, y*2, color);
      setPixelBuf(screen, x*2+1, y*2, color);
      setPixelBuf(screen, x*2, y*2+1, color);
      setPixelBuf(screen, x*2+1, y*2+1, color);
    }
  }
  refresh(screen);

  if(isKeyPressed(KEY_NSPIRE_LEFT)) tunnel_rotate += 2;
  if(isKeyPressed(KEY_NSPIRE_RIGHT)) tunnel_rotate -= 2;
  if(isKeyPressed(KEY_NSPIRE_UP)) tunnel_zoom += 2;
  if(isKeyPressed(KEY_NSPIRE_DOWN)) tunnel_zoom -= 2;
  if(isKeyPressed(KEY_NSPIRE_4)) tunnel_offsetX -= (tunnel_offsetX > 0) * 2;
  if(isKeyPressed(KEY_NSPIRE_6)) tunnel_offsetX += (tunnel_offsetX < w/2) * 2;
  if(isKeyPressed(KEY_NSPIRE_8)) tunnel_offsetY -= (tunnel_offsetY > 0) * 2;
  if(isKeyPressed(KEY_NSPIRE_2)) tunnel_offsetY += (tunnel_offsetY < h/2) * 2;
}

  // On n'oublie pas de libérer tous les malloc
  free(screen);
  free(angle_lut);
  free(depth_lut);

  return 0;
}


Voilà, nous sommes au bout de ce tutoriel. J'espère vous avoir appris quelque chose, et que vous pourrez ensuite en profiter pour créer vos propres jeux ou animations ;)

À plus les gens !

Re: [TUTO] Réaliser un tunnel en 3D

Unread postPosted: 08 Oct 2012, 17:35
by Lionel Debroux
Excellent travail ;)

Re: [TUTO] Réaliser un tunnel en 3D

Unread postPosted: 08 Oct 2012, 17:39
by matref
Merci :) rien que pour l'écriture, j'y ai mis 4 jours, alors pour faire le tunnel et tous les effets ... houlà :D

Re: [TUTO] Réaliser un tunnel en 3D

Unread postPosted: 08 Oct 2012, 18:15
by nikitouzz
parfait mais... il y a encore des optimisation a faire :P

XD mais beau travaille :)

Re: [TUTO] Réaliser un tunnel en 3D

Unread postPosted: 08 Oct 2012, 18:17
by matref
Évidemment qu'il reste plein de trucs à optimiser, mais c'est un tutoriel je rappelle :D

Re: [TUTO] Réaliser un tunnel en 3D

Unread postPosted: 08 Oct 2012, 18:17
by Adriweb
Excellent boulot !

Re: [TUTO] Réaliser un tunnel en 3D

Unread postPosted: 01 Jan 2013, 20:40
by mdr1
Bravo !