π
<-
Chat plein-écran
[^]

[TUTO] Réaliser un tunnel en 3D

Regroupement de tous les tutoriaux Nspire.

[TUTO] Réaliser un tunnel en 3D

Message non lude matref » 08 Oct 2012, 17:15

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: Tout sélectionner
#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: Tout sélectionner
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: Tout sélectionner
#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: Tout sélectionner
    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: Tout sélectionner
    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: Tout sélectionner
    double relativeX, relativeY;
  • Des variables qui vont contenir les coordonnées de la couleur sur la texture.
    Code: Tout sélectionner
    int textureX, textureY;
  • Des variables pour les boucles For qui vont parcourir l'écran.
    Code: Tout sélectionner
    int x, y;
  • Une variable qui va contenir la couleur (on pourra éventuellement la modifier).
    Code: Tout sélectionner
    char color;
  • Des variables qui vont contenir les dimensions de l'écran.
    Code: Tout sélectionner
    int w = 320, h = 240;
  • Et pour finir, notre texture !
    Code: Tout sélectionner
    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: Tout sélectionner
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: Tout sélectionner
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: Tout sélectionner
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: Tout sélectionner
#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: Tout sélectionner
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: Tout sélectionner
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: Tout sélectionner
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: Tout sélectionner
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: Tout sélectionner
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: Tout sélectionner
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: Tout sélectionner
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: Tout sélectionner
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: Tout sélectionner
#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: Tout sélectionner
int tunnel_rotate = 0, tunnel_zoom = 0;

Et vous savez ce que vous en faites ? Vous les ajoutez à textureX et textureY.
Code: Tout sélectionner
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: Tout sélectionner
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: Tout sélectionner
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: Tout sélectionner
for(y = 0; y < h; y += 2)
Et bah on n'a qu'à aller d'1 en 1 !

Code: Tout sélectionner
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: Tout sélectionner
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: Tout sélectionner
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: Tout sélectionner
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: Tout sélectionner
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: Tout sélectionner
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: Tout sélectionner
#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 !
Avatar de l’utilisateur
matref
Niveau 15: CC (Chevalier des Calculatrices)
Niveau 15: CC (Chevalier des Calculatrices)
Prochain niv.: 25%
 
Messages: 506
Inscription: 11 Déc 2011, 03:08
Localisation: France, Châteaurenard
Genre: Homme
Calculatrice(s):
MyCalcs profile
Classe: Prépa MPSI

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

Message non lude Lionel Debroux » 08 Oct 2012, 17:35

Excellent travail ;)
Membre de la TI-Chess Team.
Co-mainteneur de GCC4TI (documentation en ligne de GCC4TI), TIEmu et TILP.
Avatar de l’utilisateur
Lionel DebrouxSuper Modo
Niveau 14: CI (Calculateur de l'Infini)
Niveau 14: CI (Calculateur de l'Infini)
Prochain niv.: 11.2%
 
Messages: 6859
Inscription: 23 Déc 2009, 00:00
Localisation: France
Genre: Homme
Calculatrice(s):
MyCalcs profile
Classe: -
GitHub: debrouxl

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

Message non lude matref » 08 Oct 2012, 17:39

Merci :) rien que pour l'écriture, j'y ai mis 4 jours, alors pour faire le tunnel et tous les effets ... houlà :D
Avatar de l’utilisateur
matref
Niveau 15: CC (Chevalier des Calculatrices)
Niveau 15: CC (Chevalier des Calculatrices)
Prochain niv.: 25%
 
Messages: 506
Inscription: 11 Déc 2011, 03:08
Localisation: France, Châteaurenard
Genre: Homme
Calculatrice(s):
MyCalcs profile
Classe: Prépa MPSI

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

Message non lude nikitouzz » 08 Oct 2012, 18:15

parfait mais... il y a encore des optimisation a faire :P

XD mais beau travaille :)
Mes records personnels :
2x2x2 : 2.18 secondes / 2x2x2 une main : 21.15 secondes / 2x2x2 yeux bandés : 47.59
3x3x3 : 5.97 secondes / 3x3x3 une main : 49.86 secondes
4x4x4 : 1.49 minutes / 4x4x4 une main : 6.50 minutes
5x5x5 : 4.10 minutes / 5x5x5 une main : 18.02 minutes
6x6x6 : 8.10 minutes
7x7x7 : 16.03 minutes
9x9x9 : 58.26 minutes

megaminx : 5.59 minutes / pyraminx : 7.91 secondes / square-one : 1.07 minutes

Image
Avatar de l’utilisateur
nikitouzzModo
Niveau 16: CC2 (Commandeur des Calculatrices)
Niveau 16: CC2 (Commandeur des Calculatrices)
Prochain niv.: 42.7%
 
Messages: 1016
Images: 1
Inscription: 16 Fév 2012, 18:39
Genre: Homme
Calculatrice(s):
MyCalcs profile
Classe: Fac de maths

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

Message non lude matref » 08 Oct 2012, 18:17

Évidemment qu'il reste plein de trucs à optimiser, mais c'est un tutoriel je rappelle :D
Avatar de l’utilisateur
matref
Niveau 15: CC (Chevalier des Calculatrices)
Niveau 15: CC (Chevalier des Calculatrices)
Prochain niv.: 25%
 
Messages: 506
Inscription: 11 Déc 2011, 03:08
Localisation: France, Châteaurenard
Genre: Homme
Calculatrice(s):
MyCalcs profile
Classe: Prépa MPSI

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

Message non lude Adriweb » 08 Oct 2012, 18:17

Excellent boulot !
Image

MyCalcs: Help the community's calculator documentations by filling out your calculators info!
MyCalcs: Aidez la communauté à documenter les calculatrices en donnant des infos sur vos calculatrices !
Inspired-Lua.org: All about TI-Nspire Lua programming (tutorials, wiki/docs...)
Avatar de l’utilisateur
AdriwebAdmin
Niveau 16: CC2 (Commandeur des Calculatrices)
Niveau 16: CC2 (Commandeur des Calculatrices)
Prochain niv.: 80.2%
 
Messages: 14613
Images: 1218
Inscription: 01 Juin 2007, 00:00
Localisation: France
Genre: Homme
Calculatrice(s):
MyCalcs profile
Twitter/X: adriweb
GitHub: adriweb

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

Message non lude mdr1 » 01 Jan 2013, 20:40

Bravo !
Image ImageImage
Avatar de l’utilisateur
mdr1Premium
Niveau 14: CI (Calculateur de l'Infini)
Niveau 14: CI (Calculateur de l'Infini)
Prochain niv.: 44%
 
Messages: 1083
Images: 12
Inscription: 28 Mar 2011, 00:00
Genre: Non spécifié
Calculatrice(s):
MyCalcs profile
Classe: Je voyage toujours en première.


Retourner vers Tutoriaux

Qui est en ligne

Utilisateurs parcourant ce forum: Aucun utilisateur enregistré et 18 invités

-
Rechercher
-
Social TI-Planet
-
Sujets à la une
Comparaisons des meilleurs prix pour acheter sa calculatrice !
Aidez la communauté à documenter les révisions matérielles en listant vos calculatrices graphiques !
Phi NumWorks jailbreak
123
-
Faire un don / Premium
Pour plus de concours, de lots, de tests, nous aider à payer le serveur et les domaines...
Faire un don
Découvrez les avantages d'un compte donateur !
JoinRejoignez the donors and/or premium!les donateurs et/ou premium !


Partenaires et pub
Notre partenaire Jarrety Calculatrices à acheter chez Calcuso
-
Stats.
1433 utilisateurs:
>1389 invités
>39 membres
>5 robots
Record simultané (sur 6 mois):
6892 utilisateurs (le 07/06/2017)
-
Autres sites intéressants
Texas Instruments Education
Global | France
 (English / Français)
Banque de programmes TI
ticalc.org
 (English)
La communauté TI-82
tout82.free.fr
 (Français)