Affichage 3D sur un écran tactile Arduino - Projet en temps réel

Oui oui, vous avez bien lu, on va parler d'affichage 3D, tourné sur un Arduino.
Et je vais écrire là où j'en suis en même temps que mon avancement.

1 - Matériel Prérequis

  • Un support de programmation Arduino IDE (PC/Mac/en ligne etc.) (voir le site pour les spécifications minimales)
  • Un Arduino
  • Un écran compatible avec Arduino (ici un ILI9341)

2 - Le format de données

On va tenter de varier l'affichage, sinon je demande à l'Arduino de me faire quelques lignes et c'est fini.

Donc on va faire un format de données permettant de varier.
On peut imaginer qu'il pourra être stocké dans un fichier.

2.1 - Brève description de la 3D

La 3D est une dimension, allez voir Wikipédia.
Non, je rigole :slight_smile:

Je tient à préciser que je vais simplifier l'explication pour notre usage.

La 3D comporte trois axes, que l'on va nommer X, Y et Z :

Dans notre format de données, tout ce qui va être affiché aura des informations pour qu'on puisse le localiser.

Comme localiser signifies connaître la position sur les axes, l'objet va recevoir des valeurs pour chaque axe, donc évidemment un type de valeur qui peut s'incrémenter : les nombres (pas de lettres ou je ne sais quoi !)

Nom X Y Z
Objet 1 64 32 20
Objet 2 4 -4 40

2.2 - Les inforamtions sur l'objet 3D

Problème, même si notre programme sait comment convertir un objet 3D en image 2D pour notre écran, on ne lui a pas fourni assez d'informations.

On ne lui a pas dit à quoi l'objet ressemble ni où l'afficher.

Regardez bien, les axes n'ont pas encore de valeur.
Ni une, ni deux, on le fait :

Notre fichier ressemble pour l'instant à ça :

Objet_1 64 32 20
Objet_2 4 -4 40

Passons à l'autre information manquante :
On aurait pu faire

Objet_1 64 32 20 CUBE
Objet_2 4 -4 40 CYLINDRE

Mais il faut leur rajouter des informations (dimensions, rotation, autre) pour chaqun, ce qui fera que le fichier sera beaucoup moins compréhensible et moins régulier.

Alors j'ai eu une idée : le format va dicter directement chaque face.

Une face est composée au minimum par trois segments, ce qui forme un triangle.

N'importe quelle autre face à un nombre supérieur de segments à 3 peut être divisé en plusieurs triangles.
image

Alors autant choisir universellement (dans notre format) comme face le triangle.

N'oubliez pas que le rtiangle est en 2D. On va donc rajouter l'axe manquant à chaque sommet de ce triangle.

Pourquoi ne pas faire point par point pour les objets ?

Parce qu'on ne saurait pas relier tel à tel, et que rajouter cette inforamtion alourdirait le programme et me donnera mal à la tête de tenter cette approche.

Voici actuellement notre format de données :

X1 Y1 Z1 X2 Y2 Z2 X3 Y3 Z3
64 32 20 6 32 0 6 32 20
4 -4 40 64 2 10 64 54 15

Donc

64 32 20 6 32 0 6 32 20
4 -4 40 64 2 10 64 5415

On peut désormais créer notre modèle 3D !

2.3 - Les versions

On va juste rajouter au départ de notre fichier le numéro de version pour que le programme sache comprendre le fichier.

version 1.0

64 32 20 6 32 0 6 32 20
4 -4 40 64 2 10 64 5415

3 - La vue 3D

Vous voyez le problème ? En tout cas, moi oui. On tente d'afficher de la 3D sur un écran 2D. Il faut donc convertir un point à trois axes en coordonnées 2D.

3.1 - L'axe en trop

Il faut maintenant savoir quel axe est ajouté par rapport 2D.

C'est donc l'axe Z qui doit être converti car on voit bien que c'est l'axe Z qui a été supprimé.

3.2 - Le type de perspective

Les perspectives les plus utilisées sont les perspectives réelles et l'orthogonale.
Dans la réalité, la perspective réelle est utilisée.
Dans certains logiciels de modélisation / affichage 3D, la perspective orthogonale peut être utilisée.

Mais pour le réalisme, nous allons utiliser la perspective réelle.

3.3 - Comment afficher de la 3D en 2D ?

Il existe plusieurs techniques.

J'ai cherché et j'ai trouvé deux techniques principales :

  • Le ray casting
  • Le ray tracing

Le ray tracing est assez connu, notamment car des jeux vidéos réalistes peuvent l'utiliser mais est tout de même coûteux en performance et son fonctionnement est plus compliqué.

Je vais donc utiliser le ray casting.

C'est plus du rendu des surface en 3D, je ne crois pas que pour faire un rendu du fil de fer ou plein mais sans lumière, on utilise le ray casting ou ray tracing

4 - Le ray casting en théorie

Il faut trois choses au minimum :

  • Une caméra
  • Un écran
  • La scène 3D

Le tout est virtuel. Voici ce que le programme va nous simuler :

La caméra est sur les coordonnées X = 0 et Y = 0, mais l'axe Z peut varier.
La distance entre la caméra et le centre de l'écran s'appelle la focale.
Seulement, si on modifie la taille de l'écran, on affichera plus de choses et tout paraîtra plus gros.

La focale joue sur la perspective en réalité.
Sur ce lien, vous pourrez voir la même image avec pour seule différence la focale qui change, et la perspective change vraiment.

Comme focale implique la FOV (champ de vision), et que le champ de vision implique la quantité de choses à afficher sur l'écran, on préférera garder la FOV plutôt que la focale.

version 1.0
FOV 50 (en degrés)

64 32 20 6 32 0 6 32 20
4 -4 40 64 2 10 64 5415

4.1 - Étape 1 : Lancer un rayon (tracer une ligne)

Il y a deux possibilités :

  • Soit on lance un rayon de la caméra vers chaque pixel et on prolonge
  • Soit on lance un rayon de chaque pixel de l'objet 3D vers la caméra

4.2 - Étape 2 : Afficher un pixel

Ensuite, on affiche un pixel à l'intersection, sur le vrai écran. Pour limiter les calculs, on attribue le même nombre de pixels à l'écran viruel que sur le vrai écran. Si on lance un rayon de la caméra en passant par l'écran en prolongeant, il faut vérifier si on croise une face/sommet/arrête, et on ne peux pas calculer indéfiniment la ligne.

Alors je vais choisir la deuxième option.

Il faudra penser à vérifier s'il n'y a pas d'autre choses derrière un première objet, car on affichera un objet caché, ce qui ne fonctionnera pas...

4.3 - Test sur papier

J'ai tracé les rayons sur papier et à chaque intersection, puis j'ai regroupé les intersections sur les deux axes et tracé les traits. Voici le résultat :

J'avais mis en ligne plusieurs cubes, fait de la transparence et mis des nuances de gris pour mieux voir à quoi ça ressemble.

Ça fonctionne à merveille, on va utiliser ça !

5 - Convertir en code

5.1 - Début du code

Je vais utiliser la librairie MCUFRIEND_kbv pour gérer l'écran.

On commence par faire la base pour préparer l'écran :

Programme (v. 0.0.0 alpha)
#include "MCUFRIEND_kbv.h"
MCUFRIEND_kbv tft;

const char version [] = "0.0.0 alpha";

#define PORTRAIT 0
#define PAYSAGE 1
#define PORTRAIT_RENVERSE 2
#define PAYSAGE_RENVERSE 3

#define BLACK   0x0000
#define BLUE    0x001F
#define RED     0xF800
#define GREEN   0x07E0
#define CYAN    0x07FF
#define MAGENTA 0xF81F
#define YELLOW  0xFFE0
#define WHITE   0xFFFF

void setup()
{
  Serial.begin(115200);
  tft.reset();
  tft.begin(tft.readID());
  tft.setRotation(PAYSAGE);

  tft.fillScreen(BLACK);
}

void loop()
{
}

5.2 - Ajout du fichier

On va simplement créer un tableau de caractères dans le programme, qui, ici, va synthétiser un cube (j'ai aussi mis les coordonnées de chaque sommet dans le code pour pouvoir simplifier la reconstitution du cube) :

Programme (v. 0.1.0 alpha)
#include "MCUFRIEND_kbv.h"
MCUFRIEND_kbv tft;

const char version [] = "0.1.0 alpha";
const char fichier [][48] =
{
  "version 1.0",
  "FOV 50",
  
  "-40 40 40 40 40 40 -40 -40 40",
  "40 40 40 -40 -40 40 40 -40 40",

  "-40 40 40 -40 40 80 40 40 80",
  "40 40 80 -40 40 40 40 40 40",

  "40 40 80 40 40 40 40 -40 40",
  "40 40 80 40 -40 40 40 -40 80",

  "-40 -40 40 40 -40 40 -40 -40 80",
  "-40 -40 80 40 -40 40 40 -40 80",

  "-40 40 40 -40 40 80 40 -40 40",
  "-40 40 40 40 -40 40 -40 -40 40",

  "-40 40 80 40 40 80 40 -40 80",
  "40 -40 80 -40 40 80 40 -40 40",
};

/* Positions de chaque sommet du cube :
1 : -40 40 40 
2 : 40 40 40
3 : -40 -40 40 
4 : 40 -40 40 
5 : -40 40 80 
6 : 40 40 80 
7 : -40 -40 80 
8 : 40 -40 80 
*/

#define PORTRAIT 0
#define PAYSAGE 1
#define PORTRAIT_RENVERSE 2
#define PAYSAGE_RENVERSE 3
#define ORIENTATION PAYSAGE

#define BLACK   0x0000
#define BLUE    0x001F
#define RED     0xF800
#define GREEN   0x07E0
#define CYAN    0x07FF
#define MAGENTA 0xF81F
#define YELLOW  0xFFE0
#define WHITE   0xFFFF

void setup()
{
  Serial.begin(115200);
  tft.reset();
  tft.begin(tft.readID());
  tft.setRotation(ORIENTATION);

  tft.fillScreen(BLACK);
}

void loop()
{
}

5.3 - Calcul de la focale

Il faut calculer la focale à partir de la FOV afin de pouvoir tracer les traits.

La FOV indique l'angle sur l'axe X.

J'avoue, j'ai bien galéré à comprendre et trouver un docuement permettant de calculer une longueur à partir d'un angle dans un triangle rectangle vu que le collège ne me l'a TOUJOURS pas appris. Finalement, ce site m'a permis de calculer la focale.

En modifiant la formule, on obtient

a = b / tan(a)

Avec cette figure :

Mais sur papier, ça ne fonctionne pas.
Est-ce que quelqu'un aurait une formule afin de calculer le scôté a s'il vous plaît ?

tan(a) = a/b

la tangente est égale a l'opposé sur adjacent de la : tan(a)*b = a

enfin si mes notions de math vielles de 50 années ne sont pas oubliées

Merci beaucoup !

5.3 - Calcul de la focale - suite

Bon, grâce à @jfs59, j'ai pu faire correctement la formule, donc j'ai mis dans le code et ça fonctionne parfaitement !

Programme (v. 0.2.1 alpha)
#include "MCUFRIEND_kbv.h"
MCUFRIEND_kbv tft;

#define LONGUEUR_CHAINE_CARACTERES 48

#define PORTRAIT 0
#define PAYSAGE 1
#define PORTRAIT_RENVERSE 2
#define PAYSAGE_RENVERSE 3
#define ORIENTATION PAYSAGE

#define BLACK   0x0000
#define BLUE    0x001F
#define RED     0xF800
#define GREEN   0x07E0
#define CYAN    0x07FF
#define MAGENTA 0xF81F
#define YELLOW  0xFFE0
#define WHITE   0xFFFF


const char version_programme [] = "0.2.1 alpha";
const char fichier [][LONGUEUR_CHAINE_CARACTERES] =
{
  "version 1.0",
  "FOV 50",

  "-40 40 40 40 40 40 -40 -40 40",
  "40 40 40 -40 -40 40 40 -40 40",

  "-40 40 40 -40 40 80 40 40 80",
  "40 40 80 -40 40 40 40 40 40",

  "40 40 80 40 40 40 40 -40 40",
  "40 40 80 40 -40 40 40 -40 80",

  "-40 -40 40 40 -40 40 -40 -40 80",
  "-40 -40 80 40 -40 40 40 -40 80",

  "-40 40 40 -40 40 80 40 -40 40",
  "-40 40 40 40 -40 40 -40 -40 40",

  "-40 40 80 40 40 80 40 -40 80",
  "40 -40 80 -40 40 80 40 -40 40",
};

/* Positions de chaque sommet du cube :
  1 : -40 40 40
  2 : 40 40 40
  3 : -40 -40 40
  4 : 40 -40 40
  5 : -40 40 80
  6 : 40 40 80
  7 : -40 -40 80
  8 : 40 -40 80
*/


uint16_t focale;
uint16_t FOV;

void setup()
{
  Serial.begin(115200);
  tft.reset();
  tft.begin(tft.readID());
  tft.setRotation(ORIENTATION);

  tft.fillScreen(BLACK);

  //Conversion du champ de vision (FOV) en focale
  char temp[LONGUEUR_CHAINE_CARACTERES - 4] = ""; //4 pour enlever le texte
  for (uint8_t i = 0; i < (LONGUEUR_CHAINE_CARACTERES - 1) - 4; i++)  temp[i] = fichier[1][i + 4];
  FOV = atoi(temp);
  focale = (tft.width() / 2) * tan((90 - FOV) * DEG_TO_RAD);

  tft.setTextSize(1);
  tft.setCursor(0, 0);
  tft.setTextColor(WHITE);

  tft.print("Ray casting v. "); tft.println(version_programme);
  tft.println();
  tft.print("FOV : ");      tft.println(FOV);
  tft.print("Focale : ");   tft.println(focale);

  delay(2000);
}

void loop()
{
}

5.3 - Calcul de la focale - suite 2

Je me suis un peu trompé dans la formule.
Voici le bon programme :

Programme (v. 0.2.2 alpha)
#include "MCUFRIEND_kbv.h"
MCUFRIEND_kbv tft;

#define LONGUEUR_CHAINE_CARACTERES 48

#define PORTRAIT 0
#define PAYSAGE 1
#define PORTRAIT_RENVERSE 2
#define PAYSAGE_RENVERSE 3
#define ORIENTATION PAYSAGE

#define BLACK   0x0000
#define BLUE    0x001F
#define RED     0xF800
#define GREEN   0x07E0
#define CYAN    0x07FF
#define MAGENTA 0xF81F
#define YELLOW  0xFFE0
#define WHITE   0xFFFF


const char version_programme [] = "0.2.2 alpha";
const char fichier [][LONGUEUR_CHAINE_CARACTERES] =
{
  "version 1.0",
  "FOV 50",

  "-40 40 40 40 40 40 -40 -40 40",
  "40 40 40 -40 -40 40 40 -40 40",

  "-40 40 40 -40 40 80 40 40 80",
  "40 40 80 -40 40 40 40 40 40",

  "40 40 80 40 40 40 40 -40 40",
  "40 40 80 40 -40 40 40 -40 80",

  "-40 -40 40 40 -40 40 -40 -40 80",
  "-40 -40 80 40 -40 40 40 -40 80",

  "-40 40 40 -40 40 80 40 -40 40",
  "-40 40 40 40 -40 40 -40 -40 40",

  "-40 40 80 40 40 80 40 -40 80",
  "40 -40 80 -40 40 80 40 -40 40",
};

/* Positions de chaque sommet du cube :
  1 : -40 40 40
  2 : 40 40 40
  3 : -40 -40 40
  4 : 40 -40 40
  5 : -40 40 80
  6 : 40 40 80
  7 : -40 -40 80
  8 : 40 -40 80
*/


uint16_t focale;
uint16_t FOV;

void setup()
{
  Serial.begin(115200);
  tft.reset();
  tft.begin(tft.readID());
  tft.setRotation(ORIENTATION);

  tft.fillScreen(BLACK);


  //Conversion du champ de vision (FOV) en focale :
  char temp[LONGUEUR_CHAINE_CARACTERES - strlen("FOV ")] = "";
  for (uint8_t i = 0; i < (LONGUEUR_CHAINE_CARACTERES - 1) - strlen("FOV "); i++)  temp[i] = fichier[1][i + strlen("FOV ")];
  FOV = atoi(temp);
  focale = (tft.width() / 2) * tan(DEG_TO_RAD * (90 - (FOV / 2)));


  tft.setTextSize(1);
  tft.setCursor(0, 0);
  tft.setTextColor(WHITE);

  tft.print("Ray casting v. "); tft.println(version_programme);
  tft.println();
  tft.print("FOV : ");      tft.println(FOV);
  tft.print("Focale : ");   tft.println(focale);

  delay(3000);
  tft.fillScreen(BLACK);
}

void loop()
{
}

5.4 - Convertion des coordonnées 3D en coordonnées 2D

Bon j'ai déjà terminé le programmme il y a déjà un petit moment mais le plus long est d'écrire les explications et de faire les schémas.

Voici le programme, en vous passant les correctifs de bug :

Programme (v. 0.3.5 beta)
#include "MCUFRIEND_kbv.h"
MCUFRIEND_kbv tft;

#define LONGUEUR_CHAINE_CARACTERES 48

#define PORTRAIT 0
#define PAYSAGE 1
#define PORTRAIT_RENVERSE 2
#define PAYSAGE_RENVERSE 3
#define ORIENTATION PAYSAGE

#define BLACK   0x0000
#define BLUE    0x001F
#define RED     0xF800
#define GREEN   0x07E0
#define CYAN    0x07FF
#define MAGENTA 0xF81F
#define YELLOW  0xFFE0
#define WHITE   0xFFFF


const char version_programme [] = "0.3.5 beta";
const char fichier [][LONGUEUR_CHAINE_CARACTERES] =
{
  "version 1.0",
  "FOV 50",

  "-100 100 100 100 100 100 -100 -100 100",
  "100 100 100 -100 -100 100 100 -100 100",

  "-100 100 100 100 100 100 -100 100 200",
  "100 100 100 -100 100 200 100 100 200",

  "-100 100 200 100 100 200 -100 -100 200",
  "100 100 200 -100 -100 200 100 -100 200",

  "-100 -100 100 100 -100 100 -100 -100 200",
  "100 -100 100 -100 -100 200 100 -100 200",

  "-100 100 100 -100 -100 100 -100 100 200",
  "-100 -100 100 -100 100 200 -100 -100 200",

  "100 100 100 100 -100 100 100 100 200",
  "100 -100 100 100 100 200 100 -100 200",
};


uint16_t focale;
uint16_t FOV;


void setup()
{
  Serial.begin(115200);
  tft.reset();
  tft.begin(tft.readID());
  tft.setRotation(ORIENTATION);

  tft.fillScreen(BLACK);


  //Conversion du champ de vision (FOV) en focale :
  char temp[LONGUEUR_CHAINE_CARACTERES - strlen("FOV ")] = "";
  for (uint8_t i = 0; i < (LONGUEUR_CHAINE_CARACTERES - 1) - strlen("FOV "); i++)  temp[i] = fichier[1][i + strlen("FOV ")];
  FOV = atoi(temp);
  focale = (tft.width() / 2) * tan(DEG_TO_RAD * (90 - (FOV / 2)));


  tft.setTextSize(1);
  tft.setCursor(0, 0);
  tft.setTextColor(WHITE);

  tft.print("Ray casting v. "); tft.println(version_programme);
  tft.println();
  tft.print("FOV : ");      tft.println(FOV);
  tft.print("Focale : ");   tft.println(focale);

  delay(3000);
  tft.fillScreen(BLACK);

  actualiser_frame();
}

void loop()
{
}



void actualiser_frame()
{
  static uint8_t avancee;
  static uint8_t avancee_ligne;
  static uint8_t nbr_espaces;
  static int16_t XYZ1 [3];
  static int16_t XYZ2 [3];
  static int16_t XYZ3 [3];
  static int16_t XY1 [2];
  static int16_t XY2 [2];
  static int16_t XY3 [2];
  static char temp_frame[64];

  for (uint8_t a = 0; a < sizeof(temp_frame) / sizeof(temp_frame[0]); a++)temp_frame[a] = 0;

  for (uint16_t i = 2; fichier[i][0] != 0; i++)
  {
    //Récupération de chaque coordonnée 3D de chaque face :
    avancee = 0;
    avancee_ligne = 0;
    nbr_espaces = 0;
    while (nbr_espaces <= 2)
    {
      if (fichier[i][avancee_ligne] == ' ' or fichier[i][avancee_ligne] == 0)
      {
        XYZ1 [nbr_espaces] = atoi(temp_frame);
        for (uint8_t a = 0; a < sizeof(temp_frame) / sizeof(temp_frame[0]); a++)temp_frame[a] = 0;
        avancee = 0;
        nbr_espaces++;
      }
      else
      {
        temp_frame[avancee] = fichier[i][avancee_ligne];
        avancee++;
      }
      avancee_ligne++;
    }
    nbr_espaces = 0;
    while (nbr_espaces <= 2)
    {
      if (fichier[i][avancee_ligne] == ' ' or fichier[i][avancee_ligne] == 0)
      {
        XYZ2 [nbr_espaces] = atoi(temp_frame);
        for (uint8_t a = 0; a < sizeof(temp_frame) / sizeof(temp_frame[0]); a++)temp_frame[a] = 0;
        avancee = 0;
        nbr_espaces++;
      }
      else
      {
        temp_frame[avancee] = fichier[i][avancee_ligne];
        avancee++;
      }
      avancee_ligne++;
    }
    nbr_espaces = 0;
    while (nbr_espaces <= 2)
    {
      if (fichier[i][avancee_ligne] == ' ' or fichier[i][avancee_ligne] == 0)
      {
        XYZ3 [nbr_espaces] = atoi(temp_frame);
        for (uint8_t a = 0; a < sizeof(temp_frame) / sizeof(temp_frame[0]); a++)temp_frame[a] = 0;
        avancee = 0;
        nbr_espaces++;
      }
      else
      {
        temp_frame[avancee] = fichier[i][avancee_ligne];
        avancee++;
      }
      avancee_ligne++;
    }


    //Convertion des coordonnées 3D en coordonnées 2D :
    if (XYZ1[2] >= 0 and XYZ2[2] >= 0 and XYZ3[2] >= 0)
    {
      static int64_t adjacent;

      adjacent = focale + XYZ1[2];
      XY1[0] = (focale * ((XYZ1[0] * 1000000) / adjacent)) / 1000000;
      XY1[1] = (focale * ((XYZ1[1] * 1000000) / adjacent)) / 1000000;

      adjacent = focale + XYZ2[2];
      XY2[0] = (focale * ((XYZ2[0] * 1000000) / adjacent)) / 1000000;
      XY2[1] = (focale * ((XYZ2[1] * 1000000) / adjacent)) / 1000000;

      adjacent = focale + XYZ3[2];
      XY3[0] = (focale * ((XYZ3[0] * 1000000) / adjacent)) / 1000000;
      XY3[1] = (focale * ((XYZ3[1] * 1000000) / adjacent)) / 1000000;
    }


    XY1[0] = (tft.width()  / 2) + XY1[0];
    XY1[1] = (tft.height() / 2) + XY1[1];
    XY2[0] = (tft.width()  / 2) + XY2[0];
    XY2[1] = (tft.height() / 2) + XY2[1];
    XY3[0] = (tft.width()  / 2) + XY3[0];
    XY3[1] = (tft.height() / 2) + XY3[1];

    //Affichage sur l'écran :
    //tft.fillTriangle(XY1[0], XY1[1], XY2[0], XY2[1], XY3[0], XY3[1], tft.color565(random(10, 256), random(10, 256), random(10, 256)));
    tft.drawTriangle(XY1[0], XY1[1], XY2[0], XY2[1], XY3[0], XY3[1], tft.color565(255, 255, 255));
    //tft.drawTriangle(XY1[0], XY1[1], XY2[0], XY2[1], XY3[0], XY3[1], tft.color565(random(120, 256), random(120, 256), random(120, 256)));
  }
}

Le programme n'affiche que les lignes des faces mais il suffit de modifier cette ligne tft.drawTriangle(XY1[0], XY1[1], XY2[0], XY2[1], XY3[0], XY3[1], tft.color565(255, 255, 255)); en cette ligne tft.fillTriangle(XY1[0], XY1[1], XY2[0], XY2[1], XY3[0], XY3[1], tft.color565(255, 255, 255)); pour faire le changement.

Et aussi un fichier 3D à mettre dans le programme (voir plus loin) :

Voici le résultat du programme !
image

(La carré le plus éloigné est la face la plus écartée de nous du cube / Les faces sont transparentes car on n'affiche que les lignes)

Explication :

Afin de convertir les coordonnées 3D en coordonnée 2D, il faut calculer la valeur Xproj et faire la même chose mais avec l'axe Y. (J'ai d'abord récupéré les valeurs dans le fichier)

Pour cela on va utiliser les fonctions trigonométriques, et la formule est :
Xproj = focale x (X / (focale + Z))

Et puis on affiche notre résultat du calcul.
J'ai mis ça dans le programme et comme vous pouvez le voir avec l'image (plus haut), ça fonctionne !




L'affichage 3D est terminé, mais je ne compte pas m'arrêter là...

Si je peux me permettre du coup tu as abandonné le raycasting au bénéfice de la projection de sommet ?

Pas vraiment. En fait je veux partir du principe de projection jusqu'à avoir un rendu réaliste peut-être jusqu'à simuler les ombres, la réflexion, la transparences etc. !

Mise à jour - Ajout du versionnage (v. 1.1.0 RC-1)

Programme (v. 1.1.0 RC-1)
#include "MCUFRIEND_kbv.h"
MCUFRIEND_kbv tft;

#define LONGUEUR_CHAINE_CARACTERES 48

#define PORTRAIT 0
#define PAYSAGE 1
#define PORTRAIT_RENVERSE 2
#define PAYSAGE_RENVERSE 3
#define ORIENTATION PAYSAGE

#define BLACK   0x0000
#define BLUE    0x001F
#define RED     0xF800
#define GREEN   0x07E0
#define CYAN    0x07FF
#define MAGENTA 0xF81F
#define YELLOW  0xFFE0
#define WHITE   0xFFFF


const char version_programme [] = "1.1.0 RC-1";
const char fichier [][LONGUEUR_CHAINE_CARACTERES] =
{
  "version 1.0",
  "FOV 50",

  "-100 100 100 100 100 100 -100 -100 100",
  "100 100 100 -100 -100 100 100 -100 100",

  "-100 100 100 100 100 100 -100 100 200",
  "100 100 100 -100 100 200 100 100 200",

  "-100 100 200 100 100 200 -100 -100 200",
  "100 100 200 -100 -100 200 100 -100 200",

  "-100 -100 100 100 -100 100 -100 -100 200",
  "100 -100 100 -100 -100 200 100 -100 200",

  "-100 100 100 -100 -100 100 -100 100 200",
  "-100 -100 100 -100 100 200 -100 -100 200",

  "100 100 100 100 -100 100 100 100 200",
  "100 -100 100 100 100 200 100 -100 200",
};


uint16_t focale;
uint16_t FOV;
char version_fichier[LONGUEUR_CHAINE_CARACTERES] = "";

void setup()
{
  Serial.begin(115200);
  tft.reset();
  tft.begin(tft.readID());
  tft.setRotation(ORIENTATION);

  tft.fillScreen(BLACK);
  tft.setTextSize(1);
  tft.setCursor(0, 0);
  tft.setTextColor(WHITE);




  //Récupération de la version du fichier
  uint8_t _i = 0;
  while (fichier[0][_i + strlen("version ")] != 0) {
    version_fichier[_i] = fichier[0][_i + strlen("version ")];
    _i++;
  }



  Serial.print("Ray casting v."); Serial.println(version_programme);  Serial.println();
  tft.print("Ray casting v. ");   tft.println(version_programme);     tft.println();

  Serial.print("Version du fichier : "); Serial.println(version_fichier);
  tft.print("Version du fichier : ")   ; tft.println(version_fichier);




  if (strcmp(version_fichier, "1.0") == 0)
  {
    //Récupération de la FOV dans le fichier
    _i = 0;
    char temp[LONGUEUR_CHAINE_CARACTERES] = "";
    while (fichier[1][_i + strlen("FOV ")] != 0) {
      temp[_i] = fichier[1][_i + strlen("FOV ")];
      _i++;
    }
    FOV = atoi(temp);

    //Conversion du champ de vision (FOV) en focale
    focale = (tft.width() / 2) * tan(DEG_TO_RAD * (90 - (FOV / 2)));


    Serial.print("FOV : ")  ; Serial.println(FOV);
    tft.print("FOV : ")     ; tft.println(FOV);
    Serial.print("Focale :"); Serial.println(focale);
    tft.print("Focale : ")  ; tft.println(focale);

    delay(3000);
    tft.fillScreen(BLACK);

    actualiser_frameV1_0();
  }
  else
  {
    Serial.println("Version du fichier imcompatible avec la version du programme");
    tft.println("Version du fichier imcompatible avec la version du programme");
    while (true == true);
  }
}

void loop()
{
}

void actualiser_frameV1_0()
{
  static uint8_t avancee;
  static uint8_t avancee_ligne;
  static uint8_t nbr_espaces;
  static int16_t XYZ1 [3];
  static int16_t XYZ2 [3];
  static int16_t XYZ3 [3];
  static int16_t XY1 [2];
  static int16_t XY2 [2];
  static int16_t XY3 [2];
  static char temp_frame[64];

  for (uint8_t a = 0; a < sizeof(temp_frame) / sizeof(temp_frame[0]); a++)temp_frame[a] = 0;

  for (uint16_t i = 2; fichier[i][0] != 0; i++)
  {
    //Récupération de chaque coordonnée 3D de chaque face :
    avancee = 0;
    avancee_ligne = 0;
    nbr_espaces = 0;
    while (nbr_espaces <= 2)
    {
      if (fichier[i][avancee_ligne] == ' ' or fichier[i][avancee_ligne] == 0)
      {
        XYZ1 [nbr_espaces] = atoi(temp_frame);
        for (uint8_t a = 0; a < sizeof(temp_frame) / sizeof(temp_frame[0]); a++)temp_frame[a] = 0;
        avancee = 0;
        nbr_espaces++;
      }
      else
      {
        temp_frame[avancee] = fichier[i][avancee_ligne];
        avancee++;
      }
      avancee_ligne++;
    }
    nbr_espaces = 0;
    while (nbr_espaces <= 2)
    {
      if (fichier[i][avancee_ligne] == ' ' or fichier[i][avancee_ligne] == 0)
      {
        XYZ2 [nbr_espaces] = atoi(temp_frame);
        for (uint8_t a = 0; a < sizeof(temp_frame) / sizeof(temp_frame[0]); a++)temp_frame[a] = 0;
        avancee = 0;
        nbr_espaces++;
      }
      else
      {
        temp_frame[avancee] = fichier[i][avancee_ligne];
        avancee++;
      }
      avancee_ligne++;
    }
    nbr_espaces = 0;
    while (nbr_espaces <= 2)
    {
      if (fichier[i][avancee_ligne] == ' ' or fichier[i][avancee_ligne] == 0)
      {
        XYZ3 [nbr_espaces] = atoi(temp_frame);
        for (uint8_t a = 0; a < sizeof(temp_frame) / sizeof(temp_frame[0]); a++)temp_frame[a] = 0;
        avancee = 0;
        nbr_espaces++;
      }
      else
      {
        temp_frame[avancee] = fichier[i][avancee_ligne];
        avancee++;
      }
      avancee_ligne++;
    }


    //Convertion des coordonnées 3D en coordonnées 2D :
    if (XYZ1[2] >= 0 and XYZ2[2] >= 0 and XYZ3[2] >= 0)
    {
      static int64_t adjacent;

      adjacent = focale + XYZ1[2];
      XY1[0] = (focale * ((XYZ1[0] * 1000000) / adjacent)) / 1000000;
      XY1[1] = (focale * ((XYZ1[1] * 1000000) / adjacent)) / 1000000;

      adjacent = focale + XYZ2[2];
      XY2[0] = (focale * ((XYZ2[0] * 1000000) / adjacent)) / 1000000;
      XY2[1] = (focale * ((XYZ2[1] * 1000000) / adjacent)) / 1000000;

      adjacent = focale + XYZ3[2];
      XY3[0] = (focale * ((XYZ3[0] * 1000000) / adjacent)) / 1000000;
      XY3[1] = (focale * ((XYZ3[1] * 1000000) / adjacent)) / 1000000;
    }


    XY1[0] = (tft.width()  / 2) + XY1[0];
    XY1[1] = (tft.height() / 2) + XY1[1];
    XY2[0] = (tft.width()  / 2) + XY2[0];
    XY2[1] = (tft.height() / 2) + XY2[1];
    XY3[0] = (tft.width()  / 2) + XY3[0];
    XY3[1] = (tft.height() / 2) + XY3[1];

    //Affichage sur l'écran :
    //tft.fillTriangle(XY1[0], XY1[1], XY2[0], XY2[1], XY3[0], XY3[1], tft.color565(random(10, 256), random(10, 256), random(10, 256)));
    tft.drawTriangle(XY1[0], XY1[1], XY2[0], XY2[1], XY3[0], XY3[1], tft.color565(255, 255, 255));
    //tft.drawTriangle(XY1[0], XY1[1], XY2[0], XY2[1], XY3[0], XY3[1], tft.color565(random(120, 256), random(120, 256), random(120, 256)));
  }
}

Cette version ajoute le versionnage des fichiers, il permet d'éviter les bugs.
J'en ai aussi profité pour revoir le fonctionnement de certains points du code.

La mention RC signifie que la version est en théorie fonctionnelle mais qu'il peut comporter encofre des bugs mineurs à trouver.

Fichiers disponibles

Ce post va être mis à jour à chaque nouveau fichier 3D disponible.
C'est ici que vous pouvez trouver des fichiers à inclure dans le programme d'affichage 3D.

Version 1.0

Version 1.1

Salut !
J'ai suivi ce topic sans vraiment comprendre, mais avec admiration :wink:
Penses-tu qu'il serai possible de faire tourner, par exemple, une pyramide ? Un truc comme ça :



Ça boufferai certainement BEAUCOUP de mémoire, non ?

Bonne journée

Amitiés
Pandaroux007

Et bien je l'ai fait. Depuis tout ce temps j'ai cherché, me suis documenté, testé, et surtout essayé de résoudre des bugs.

Mais il en reste un tout petit dernier.
Il allongit la forme 3D sur un axe.

Donc je vais publier la version alpha mais malheureusement publier une version avec un bug connu non patché ce n'est pas possible donc je suis contraint de mettre la verison en alpha :

Programme (v. 1.2.1 alpha)
#include "MCUFRIEND_kbv.h"
MCUFRIEND_kbv tft;

#define LONGUEUR_CHAINE_CARACTERES 48

#define PORTRAIT 0
#define PAYSAGE 1
#define PORTRAIT_RENVERSE 2
#define PAYSAGE_RENVERSE 3
#define ORIENTATION PAYSAGE

#define BLACK   0x0000
#define BLUE    0x001F
#define RED     0xF80
#define GREEN   0x07E0
#define CYAN    0x07FF
#define MAGENTA 0xF81F
#define YELLOW  0xFFE0
#define WHITE   0xFFFF


const char version_programme [] = "1.2.1 alpha";
const char fichier [][LONGUEUR_CHAINE_CARACTERES] =
{
  "version 1.1",
  "FOV 50",
  "centre 0 0 150",
  "rotation 0 0 0",

  "0 100 150 -100 -100 100 100 -100 100",
  "0 100 150 -100 -100 100 -100 -100 200",
  "0 100 150 -100 -100 200 100 -100 200",
  "0 100 150 100 -100 100 100 -100 200",

  "-100 -100 100 100 -100 100 -100 -100 200",
  "100 -100 100 -100 -100 200 100 -100 200",
};


uint16_t focale;
uint16_t FOV;
int16_t centre[3];
uint16_t rotation[3];
char version_fichier[LONGUEUR_CHAINE_CARACTERES] = "";
char temp[LONGUEUR_CHAINE_CARACTERES];


void setup()
{
  Serial.begin(115200);
  tft.reset();
  tft.begin(tft.readID());
  tft.setRotation(ORIENTATION);

  tft.fillScreen(BLACK);
  tft.setTextSize(1);
  tft.setCursor(0, 0);
  tft.setTextColor(WHITE);




  //Récupération de la version du fichier
  uint8_t _i = 0;
  while (fichier[0][_i + strlen("version ")] != 0) {
    version_fichier[_i] = fichier[0][_i + strlen("version ")];
    _i++;
  }



  Serial.print("Ray casting v."); Serial.println(version_programme);  Serial.println();
  tft.print("Ray casting v. ");   tft.println(version_programme);     tft.println();

  Serial.print("Version du fichier : "); Serial.println(version_fichier);
  tft.print("Version du fichier : ")   ; tft.println(version_fichier);




  if (strcmp(version_fichier, "1.0") == 0)
  {
    //Récupération de la FOV dans le fichier
    _i = 0;
    for (uint8_t _r = 0; _r < sizeof(temp) / sizeof(temp[0]); _r++) temp[_r] = 0;
    while (fichier[1][_i + strlen("FOV ")] != 0)
    {
      temp[_i] = fichier[1][_i + strlen("FOV ")];
      _i++;
    }
    FOV = atoi(temp);

    //Conversion du champ de vision (FOV) en focale
    focale = (tft.width() / 2) * tan(DEG_TO_RAD * (90 - (FOV / 2)));


    Serial.print("FOV : ")  ; Serial.println(FOV);
    tft.print("FOV : ")     ; tft.println(FOV);
    Serial.print("Focale :"); Serial.println(focale);
    tft.print("Focale : ")  ; tft.println(focale);

    delay(3000);
    tft.fillScreen(BLACK);

    actualiser_frameV1_0();
  }
  if (strcmp(version_fichier, "1.1") == 0)
  {
    //Récupération de la FOV dans le fichier
    _i = 0;
    for (uint8_t _r = 0; _r < sizeof(temp) / sizeof(temp[0]); _r++) temp[_r] = 0;
    while (fichier[1][_i + strlen("FOV ")] != 0)
    {
      temp[_i] = fichier[1][_i + strlen("FOV ")];
      _i++;
    }
    FOV = atoi(temp);

    //Conversion du champ de vision (FOV) en focale
    focale = (tft.width() / 2) * tan(DEG_TO_RAD * (90 - (FOV / 2)));

    //Récupération du centre dans le fichier
    _i = strlen("centre ");
    uint16_t _a;
    for (uint8_t a = 0; a < 3; a++)
    {
      for (uint8_t _r = 0; _r < sizeof(temp) / sizeof(temp[0]); _r++) temp[_r] = 0;
      _a = 0;
      while (fichier[2][_i] != 0 and fichier[2][_i] != ' ')
      {
        temp[_a] = fichier[2][_i];
        _i++;
        _a++;
      }
      _i++;
      centre[a] = atoi(temp);
    }



    //Récupération de la rotation dans le fichier
    _i = strlen("rotation ");
    for (uint8_t a = 0; a < 3; a++)
    {
      for (uint8_t _r = 0; _r < sizeof(temp) / sizeof(temp[0]); _r++) temp[_r] = 0;
      _a = 0;
      while (fichier[3][_i] != 0 and fichier[3][_i] != ' ')
      {
        temp[_a] = fichier[3][_i];
        _i++;
        _a++;
      }
      _i++;
      rotation[a] = atoi(temp);
    }


    Serial.print("FOV : ")  ; Serial.println(FOV);
    tft.print("FOV : ")     ; tft.println(FOV);
    Serial.print("Focale :"); Serial.println(focale);
    tft.print("Focale : ")  ; tft.println(focale);
    Serial.print("Centre :"); Serial.print(centre[0]); Serial.print(' '); Serial.print(centre[1]); Serial.print(' '); Serial.println(centre[2]);
    tft.print("Centre : ")  ; tft.print(centre[0]); tft.print(' '); tft.print(centre[1]); tft.print(' '); tft.println(centre[2]);
    Serial.print("Rotation :"); Serial.print(rotation[0]); Serial.print(' '); Serial.print(rotation[1]); Serial.print(' '); Serial.println(rotation[2]);
    tft.print("Rotation : ")  ; tft.print(rotation[0]); tft.print(' '); tft.print(rotation[1]); tft.print(' '); tft.println(rotation[2]);


    delay(1000);
    tft.fillScreen(BLACK);

    /*
        float X;
        float Y;
        for (uint16_t nbr = 0; nbr < 320; nbr++)
        {
          for (uint16_t Rotation = 0; Rotation < 360; Rotation++)
          {
            X = (cos(Rotation * DEG_TO_RAD) * nbr) + tft.width() / 2;
            Y = (sin(Rotation * DEG_TO_RAD) * nbr) + tft.height() / 2;

            tft.drawPixel(X, Y, tft.color565(map(Rotation, 0, 360, 255, 0), 255, 255));
          }
        }
    */

    actualiser_frameV1_1(rotation, centre, 0xFFFF);
    //tft.fillScreen(BLACK);
  }
  else
  {
    Serial.println(F("Version du fichier imcompatible avec la version du programme"));
    tft.println(F("Version du fichier imcompatible avec la version du programme"));
    while (true == true);
  }
}

void loop()
{
  ///*
  for (uint16_t _c = 0; _c < 360; _c += 5)
  {
    rotation[0] = _c;
    actualiser_frameV1_1(rotation, centre, 0xFFFF);
    rotation[0] = (rotation[0] >= 5 ? rotation[0] - 5 : (360 + rotation[0]) - 5);
    actualiser_frameV1_1(rotation, centre, 0x0000);
    rotation[0] = _c;
    actualiser_frameV1_1(rotation, centre, 0xFFFF);
  }
  //*/

  /*
    for (uint16_t _c = 0; _c < 360; _c += 5)
    {
    rotation[0] = _c;
    actualiser_frameV1_1(rotation, centre, 0xFFFF);
    actualiser_frameV1_1(rotation, centre, 0x0000);
    }
  */
}

void actualiser_frameV1_0()
{
  static int16_t XYZ [9];
  static int16_t XY1 [2];
  static int16_t XY2 [2];
  static int16_t XY3 [2];

  for (uint16_t i = 2; fichier[i][0] != 0; i++)
  {
    //Récupération des coordonnées pour une ligne
    uint8_t _i = 0;
    uint16_t _a;
    for (uint8_t a = 0; a < 9; a++)
    {
      for (uint8_t _r = 0; _r < sizeof(temp) / sizeof(temp[0]); _r++) temp[_r] = 0;
      _a = 0;
      while (fichier[i][_i] != 0 and fichier[i][_i] != ' ')
      {
        temp[_a] = fichier[i][_i];
        _i++;
        _a++;
      }
      _i++;
      XYZ[a] = atoi(temp);
    }

    //Convertion des coordonnées 3D en coordonnées 2D :
    if (XYZ[2] >= 0 and XYZ[5] >= 0 and XYZ[8] >= 0)
    {
      static int64_t adjacent;

      adjacent = focale + XYZ[2];
      XY1[0] = (focale * ((XYZ[0] * 1000000) / adjacent)) / 1000000;
      XY1[1] = (focale * ((XYZ[1] * 1000000) / adjacent)) / 1000000;

      adjacent = focale + XYZ[5];
      XY2[0] = (focale * ((XYZ[3] * 1000000) / adjacent)) / 1000000;
      XY2[1] = (focale * ((XYZ[4] * 1000000) / adjacent)) / 1000000;

      adjacent = focale + XYZ[8];
      XY3[0] = (focale * ((XYZ[6] * 1000000) / adjacent)) / 1000000;
      XY3[1] = (focale * ((XYZ[7] * 1000000) / adjacent)) / 1000000;
    }

    //Centrage des valeurs
    XY1[0] += (tft.width()  / 2);
    XY1[1] = (tft.height() / 2) - XY1[1];
    XY2[0] += (tft.width()  / 2);
    XY2[1] = (tft.height() / 2) - XY2[1];
    XY3[0] += (tft.width()  / 2);
    XY3[1] = (tft.height() / 2) - XY3[1];

    //Affichage sur l'écran :
    //tft.fillTriangle(XY1[0], XY1[1], XY2[0], XY2[1], XY3[0], XY3[1], tft.color565(random(10, 256), random(10, 256), random(10, 256)));
    //tft.fillTriangle(XY1[0], XY1[1], XY2[0], XY2[1], XY3[0], XY3[1], tft.color565(random(120, 256), random(120, 256), random(120, 256)));
    tft.drawTriangle(XY1[0], XY1[1], XY2[0], XY2[1], XY3[0], XY3[1], tft.color565(255, 255, 255)); //Lignes
  }
}

void actualiser_frameV1_1(uint16_t _rotation[3], int16_t _centre[3], uint16_t couleur)
{
  static int16_t XYZ [9];
  static int16_t XY1 [2];
  static int16_t XY2 [2];
  static int16_t XY3 [2];
  static int64_t adjacent;

  static int16_t distance[3];
  static int16_t Angle;
  static uint16_t rayon;

  for (uint16_t i = 4; fichier[i][0] != 0; i++) //Pour chaque ligne
  {
    //Récupération des coordonnées 3D
    uint8_t _i = 0;
    uint16_t _a;
    for (uint8_t a = 0; a < 9; a++)
    {
      for (uint8_t _r = 0; _r < sizeof(temp) / sizeof(temp[0]); _r++) temp[_r] = 0;
      _a = 0;
      while (fichier[i][_i] != 0 and fichier[i][_i] != ' ')
      {
        temp[_a] = fichier[i][_i];
        _i++;
        _a++;
      }
      _i++;
      XYZ[a] = atoi(temp);
    }



    //Calcul de la distance avec le centre pour chaque point, axe par axe :
    for (uint8_t _o = 0; _o < 3; _o++)
    {
      distance[0] = XYZ[_o * 3 + 0] - centre[0];
      distance[1] = XYZ[_o * 3 + 1] - centre[1];
      distance[2] = XYZ[_o * 3 + 2] - centre[2];
      rayon = sqrt(sq(distance[1]) + sq(distance[2]));

      if (distance[1] <= 0 and distance[2] > 0)         Angle = atan((0 - distance[1]) / distance[2])         + 0   + _rotation[0];
      else if (distance[1] < 0 and distance[2] <= 0)    Angle = atan((0 - distance[2]) / (0 - distance[1]))   + 90  + _rotation[0];
      else if (distance[1] >= 0 and distance[2] < 0)    Angle = atan(distance[1] / (0 - distance[2]))         + 180 + _rotation[0];
      else if (distance[1] > 0 and distance[2] >= 0)    Angle = atan(distance[2] / distance[1])               + 270 + _rotation[0];
      while (Angle >= 360) Angle = Angle - 360;

      XYZ[_o * 3 + 1] = centre[1] + (0 - sin(Angle * DEG_TO_RAD) * rayon);
      XYZ[_o * 3 + 2] = centre[2] + (cos(Angle * DEG_TO_RAD) * rayon);
    }

    //Convertion des coordonnées 3D en coordonnées 2D :
    if (XYZ[2] >= 0 and XYZ[5] >= 0 and XYZ[8] >= 0)
    {
      adjacent = focale + XYZ[2];
      XY1[0] = (focale * ((XYZ[0] * 1000000) / adjacent)) / 1000000;
      XY1[1] = (focale * ((XYZ[1] * 1000000) / adjacent)) / 1000000;

      adjacent = focale + XYZ[5];
      XY2[0] = (focale * ((XYZ[3] * 1000000) / adjacent)) / 1000000;
      XY2[1] = (focale * ((XYZ[4] * 1000000) / adjacent)) / 1000000;

      adjacent = focale + XYZ[8];
      XY3[0] = (focale * ((XYZ[6] * 1000000) / adjacent)) / 1000000;
      XY3[1] = (focale * ((XYZ[7] * 1000000) / adjacent)) / 1000000;
    }

    //Centrage des valeurs
    XY1[0] += (tft.width()  / 2);
    XY1[1] = (tft.height() / 2) - XY1[1];
    XY2[0] += (tft.width()  / 2);
    XY2[1] = (tft.height() / 2) - XY2[1];
    XY3[0] += (tft.width()  / 2);
    XY3[1] = (tft.height() / 2) - XY3[1];

    tft.drawTriangle(XY1[0], XY1[1], XY2[0], XY2[1], XY3[0], XY3[1], couleur); //Affichage sur l'écran
  }
}

Ray_casting.ino (10,8 Ko)

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.