Pages: [1] 2   Bas de page
Imprimer
Auteur Fil de discussion: [tuto/Wii]Intro à la 3D sur Wii  (Lu 7630 fois)
0 Membres et 1 Invité sur ce fil de discussion.
EvilTroopa Hors ligne
Administrateur
*****
Messages: 648


Voir le profil WWW
1010011010 the Number of the Beast

« 15 Octobre 2010, 19:08:14 »

Intro

Plouf !

C'est l'heure de faire un peu de 3D sur Wii !
Bon bon bon... Ce que je vous propose c'est un visualiseur de fichier 3D .obj. C'est le premier projet que j'ai fait en 3D sur Wii, il est loin d'être parfait, mais il permet quand même de voir quelques bases de la 3D.
Plutôt que de se faire ch*er avec tout ce qui est initialisation du moteur, mise en place de la caméra, gestion des matrices, etc. j'ai pris le parti de me servir de la GRRlib pour faire tout ça, parce qu'elle le fait bien et elle le fait simplement.

Les fichiers utilisés pour charger des modèles sont au format OBJ, développé pour Maya mais disponible aussi sur Blender (je ne prends pas en compte celui de 3DSMax qui m'a gonflé).

Alors déjà, si vous savez pas ce qu'est un vertex (sommet), une face, une coordonnée de texture ou une normale, je vais faire un petit topo rapide ci-dessous.
Ensuite on verra comment afficher des trucs et des machins sur votre télé grâce à la Wii.

Avant tout, je tiens à m'excuser pour le manque de commentaires dans mes fichiers, mais bon, y'a toujours les explications ici en cas de besoin.



La 3D : comment que ça se passe-t-il ?

Un objet en 3D, c'est formé de quoi ?

Un objet en 3D c'est une série de points/lignes/triangles dans un environnement 3D (qu'on appelle monde ou scène) qui sont projetés sur un plan pour créer un affichage 2D.
Pour la projection on utilise ce qu'on appelle vulgairement une Caméra. Sans rentrer dans le détail, la caméra est défini par une position, son "haut" et son "Lookat" (l'endroit qu'elle regarde) et la distance maximum à laquelle doit être une face pour être prise en compte dans la projection.
Une lumière est aussi nécessaire pour éclairer les faces et y ajouter des nuances.

Une face est (en général) un triangle, composé donc de 3 sommets, ayant chacun des coordonnées XYZ par rapport à l'origine du monde (coordonnées 0;0;0).
Chaque sommet peut aussi avoir d'autres attributs, comme la couleur, une coordonnée de texture ou une normale.

Les coordonnées de texture, c'est 2 composantes X et Y, allant de 0.0 à 1.0 et représentant la portion de pixels d'une image qui sera "plaquée" sur la face.
Les coordonnées 0.0;0.0 représentent le coin haut/gauche de l'image, 1.0;1.0 représentent le coin bas/droit de l'image. Les coordonnées du centre de l'image sont donc 0.5;0.5.

La normale symbolise le vecteur 3D perpendiculaire à une face et sert pour calculer de quelle façon une lumière impacte cette face. On donne au moteur les coordonnées des normales des 3 points, qui calculera la normale globale de la face.

Voila en gros les données dont on a besoin pour afficher une image 3D à l'écran.

C'est bien beau tout ça, mais comment j'importe ça ?

J'ai fait le choix des fichiers OBJ parce qu'ils sont au format texte et permettent donc de lire simplement leur contenu. (avec Notepad++ pour débuguer, c'est super). En plus ce format est exportable avec Blander, logiciel 3D gratuit et performant.
Cependant, ce ne sont que des modèles statiques, pas d'animation ici (d'ailleurs j'en ai pas encore fait, alors je peux pas vous aider pour le moment Langue)
Ces fichiers vont donc lister les vertices, faces, normales et coordonnées de texture, qu'on va lire et charger dans des structures afin de les afficher.

Si vous voulez plus de détails (ce que je ne ferai pas ici), sur comment sont affichés des triangles en OpenGL (dont le GX sur Wii est inspiré), je vous conseille le SiteDuZéro (comme d'hab) http://www.siteduzero.com/tutoriel-3-5014-creez-des-programmes-en-3d-avec-opengl.html



Comment va être structuré le programme
Dans mon programme en C++ (ouep on va faire un peu de POO), il y a 3 classes :
- GameApp
- Object3D
- SkySphere (hérité de Object3D)

Object3D va permettre le chargement des données depuis un fichier et leur affichage.
SkySphere va permettre de faire l'affichage du fond en 3D, d'après une technique sympatoche.
GameApp va se charger de lister les fichiers présents sur la carte SD et de gérer le monde 3D et les inputs.

On pourrait aller bien plus loin, mais pour un simple visualiseur d'objets, pas besoin de se casser le cul plus avant.



Comment créer mes objets 3D pour qu'ils puissent être lus dans le programme ?
Si vous connaissez un peu la 3D, il y a quelques contraintes, mais c'est assez simple.
Si vous ne connaissez pas la 3D, allez voir sur le site de Blender (blender.org), il y a plein d'exemples et de tutos. Et de toute façon, vous allez pas vous lancer dans un jeu en 3D si vous êtes pas foutus de faire un cube sous Blender.
De plus il ya de très bons tutos sur ... suspense ... le Site Du Zéro !! http://www.siteduzero.com/tutoriel-3-11714-debutez-dans-la-3d-avec-blender.html

Donc rapidement, les contraintes :
- Utiliser "Export" > "WaveFront .obj et n'exporter qu'un objet par fichier,
- Le modèle ne doit pas être composé de plusieurs sous-modèles, démerdez-vous pour tout relier ensemble.
- 1 seul fichier de texture par modèle
- Toutes les faces doivent être mappées, pas de texture composite ou un peu exotiques, du mega-simple : une texture image avec des UV.
- Au moment de l'export, il faut cocher :

Donc pas besoin de trianguler le modèle avant, ça se fait tout seul à l'export.



* screen_Blender.png (11.71 Ko, 369x282 - vu 1943 fois.)
« Dernière édition: 19 Octobre 2010, 12:43:04 par EvilTroopa » Journalisée

A mushroom a day, keeps the koopas away.
EvilTroopa Hors ligne
Administrateur
*****
Messages: 648


Voir le profil WWW
1010011010 the Number of the Beast

« Réponse #1 : 15 Octobre 2010, 19:09:28 »

La classe Object3D

De quelles données a-t-on besoin ?

Un face, pour être affichée, a besoin de 3 sommets, chacuns composés :
- d'une position
- d'une coordonnée de texture,
- d'une normale

Chacune de ces informations seront stockées dans des tableaux indépendants, on composera donc notre structure Face d'indices vers les différents tableaux.

Notre classe aura besoin de ces 3 tableaux :
guVector * vertices;
TexCoord * texCoords;
guVector * normals;

Le type guVector est composé de 3 floats x, y et z. C'est donc un vecteur 3D.
J'ai créé une structure TexCoord (un peu facultatif en fait...) telle que :
Code: (cpp)
typedef struct{
float x, y;
} TexCoord;
Mais un guVector aurait fait l'affaire, on aurait simplement ignoré la composante z.

Afin de dimensionner ces tableaux, on va ajouter 3 entiers :
Code: (cpp)
int nbVertices, nbTexCoords, nbNormals;

Et on va donc créer une structure Face telle que :
Code: (cpp)
typedef struct{
int va, vb, vc; //Les positions des vertex A, B et C de la face
int na, nb, nc; //Les normales des vertex A, B et C de la face
int ta, tb, tc; //Les coordonnées de texture des vertex A, B et C de la face
} Face;

On va aussi créer un tableau de faces et un entier qui indiquera sa taille :
Code: (cpp)
int nbFaces;
Face * faces;


Notre objet aura aussi besoin de se situer dans la scène. Il a besoin de ces 3 vecteurs :
Code: (cpp)
guVector position;
guVector rotation;
guVector scale;

Voila le .h au complet :
Code: (cpp)
#ifndef _OBJECT3D_H_
#define _OBJECT3D_H_

#include <grrlib.h>

typedef struct{
float x, y;
} TexCoord;


typedef struct{
int va, vb, vc;
int na, nb, nc;
int ta, tb, tc;
} Face;

class Object3D{

public:
Object3D();
~Object3D();

guVector position;
guVector rotation;
guVector scale;

int nbVertices, nbTexCoords, nbNormals, nbFaces;

guVector * vertices;
TexCoord * texCoords;
guVector * normals;
Face * faces;

bool LoadModel(const char * fileName);

GRRLIB_texImg * texture;
bool isTextured;
void SetTexture(GRRLIB_texImg * tex);

void Render();

};

#endif


Le code dans le .cpp et son explication

Lire le fichier

D'abord on va voir le cas du chargement du fichier, opéré par la méthode bool LoadModel(const char * fileName);
Le fichier qui doit être lu est composé de la sorte :
Une série de coordonnées de position de vertex :
Code:
v -8.533268 -6.530180 0.657518
Le v en début de ligne indique qu'il s'agit de la position d'un sommet (Vertex), suivi par ses coordonnées x, y et z.

Ensuite, une série de coordonnées de textures :
Code:
vt 0.375000 0.153347
Le vt en début de ligne indique qu'il s'agit de coordonnées de textures d'un sommet (Vertex Texture), suivi par ses coordonnées x, y (ou UV).

Puis, une série de coordonnées de normales :
Code:
vn -0.099634 -0.938020 0.331951
Le vn en début de ligne indique qu'il s'agit de la normale d'un sommet (Vertex Normal), suivi par ses coordonnées x, y et z.

Enfin, une série d'indice pour chaque face :
Code:
f 234/232/232 233/233/233 232/234/234
Le f en début de ligne indique qu'il s'agit d'une face, suivi par les indices des informations classées comme suit :
Code:
f va/ta/na vb/tb/nb vc/tc/nc
Soit l'indice dans chaque tableau pour la position, la texture et la normale des points A, B et C de la face.

Il se peut qu'il y ait d'autres lignes dans le fichier, mais elles seront simplement ignorées.

Nous allons lire le fichier en 2 passes :
- d'abord compter chaque type d'élémet et dimensionner chaque tableau,
- ensuite y copier toutes les valeurs.

Code: (cpp)
bool Object3D::LoadModel(const char * fileName){

FILE * f = fopen(fileName,"r");

if (f){

char lect[128];

while(fscanf(f, "%s", lect) != EOF) {

switch(lect[0]){
case 'v':
switch(lect[1]){
case 0:
nbVertices++;
break;

case 't':
nbTexCoords++;
break;

case 'n':
nbNormals++;
break;

default:
fgets(lect, sizeof(lect), f);
break;
}
break;
case 'f':
if (lect[1] == 0){
nbFaces++;
}else{
fgets(lect, sizeof(lect), f);
}
break;
default:
fgets(lect, sizeof(lect), f);
break;
}

}

vertices = (guVector*)malloc(nbVertices * sizeof(guVector));
texCoords = (TexCoord*)malloc(nbTexCoords * sizeof(TexCoord));
normals = (guVector*)malloc(nbNormals * sizeof(guVector));
faces = (Face*)malloc(nbFaces * sizeof(Face));

int idxVertices = 0;
int idxNormals = 0;
int idxTexCoords = 0;
int idxFaces = 0;

rewind(f);

while(fscanf(f, "%s", lect) != EOF){

switch(lect[0]){

case 'v':
switch(lect[1]){
case 0:
fscanf(f, "%f %f %f", &vertices[idxVertices].x, &vertices[idxVertices].y, &vertices[idxVertices].z);
idxVertices++;
break;

case 'n':
fscanf(f, "%f %f %f", &normals[idxNormals].x, &normals[idxNormals].y, &normals[idxNormals].z);
idxNormals++;
break;

case 't':
fscanf(f, "%f %f", &texCoords[idxTexCoords].x, &texCoords[idxTexCoords].y);
texCoords[idxTexCoords].y = 1.0 - texCoords[idxTexCoords].y;
idxTexCoords++;
break;
default:
fgets(lect, sizeof(lect), f);
break;
}
break;

case 'f':
if (lect[1] == 0){
fscanf(f, "%d/%d/%d %d/%d/%d %d/%d/%d",
&faces[idxFaces].va, &faces[idxFaces].ta, &faces[idxFaces].na,
&faces[idxFaces].vb, &faces[idxFaces].tb, &faces[idxFaces].nb,
&faces[idxFaces].vc, &faces[idxFaces].tc, &faces[idxFaces].nc);

faces[idxFaces].va--; faces[idxFaces].vb--; faces[idxFaces].vc--;
faces[idxFaces].na--; faces[idxFaces].nb--; faces[idxFaces].nc--;
faces[idxFaces].ta--; faces[idxFaces].tb--; faces[idxFaces].tc--;

idxFaces++;

}else{
fgets(lect, sizeof(lect), f);
}
break;
default:
fgets(lect, sizeof(lect), f);
break;
}

}


fclose(f);

return true;
}else{
return false;
}

}


Petites précisions :
Lors de la lecture de coordonnées de texture, j'ai écrit :
Code: (cpp)
texCoords[idxTexCoords].y = 1.0 - texCoords[idxTexCoords].y;
Le but de cette ligne est d'inverser les coordonnées y, qui sont inversées entre Blender et Wii.

Plus bas, je décrémente chaque indice de la face :
Code: (cpp)
faces[idxFaces].va--; faces[idxFaces].vb--; faces[idxFaces].vc--;
Le premier vertex sur Blender est le 1. Nos tableaux commencent eux par 0.

La commande
Code: (cpp)
fgets(lect, sizeof(lect), f);
permet de "manger" la ligne en cours afin de passer à la suivante.


Afficher le modèle

Voici le code de la fonction Render() :
Code: (cpp)
void Object3D::Render(){

if (isTextured){

GRRLIB_3dMode(0.1, 2000, 45, 1, 1);

GRRLIB_SetTexture(texture, false);

}else{
GRRLIB_3dMode(0.1, 2000, 45, 0, 1);
}

GRRLIB_ObjectView(position.x, position.y, position.z, rotation.x, rotation.y, rotation.z, scale.x, scale.y, scale.z);

GX_Begin(GX_TRIANGLES, GX_VTXFMT0, (nbFaces) * 3);
for (int i = 0; i < nbFaces; i++){
GX_Position3f32(vertices[faces[i].va].x, vertices[faces[i].va].y, vertices[faces[i].va].z);
GX_Normal3f32(normals[faces[i].na].x, normals[faces[i].na].y, normals[faces[i].na].z);
GX_Color1u32(0xFFFFFFFF);
if (isTextured)
GX_TexCoord2f32(texCoords[faces[i].ta].x, texCoords[faces[i].ta].y);

GX_Position3f32(vertices[faces[i].vb].x, vertices[faces[i].vb].y, vertices[faces[i].vb].z);
GX_Normal3f32(normals[faces[i].nb].x, normals[faces[i].nb].y, normals[faces[i].nb].z);
GX_Color1u32(0xFFFFFFFF);
if (isTextured)
GX_TexCoord2f32(texCoords[faces[i].tb].x, texCoords[faces[i].tb].y);

GX_Position3f32(vertices[faces[i].vc].x, vertices[faces[i].vc].y, vertices[faces[i].vc].z);
GX_Normal3f32(normals[faces[i].nc].x, normals[faces[i].nc].y, normals[faces[i].nc].z);
GX_Color1u32(0xFFFFFFFF);
if (isTextured)
GX_TexCoord2f32(texCoords[faces[i].tc].x, texCoords[faces[i].tc].y);
}
GX_End();

}

Tout d'abord, on initialise la 3D en fonction de l'objet. Si l'objet n'a pas de texture assignée, on l'affiche nu.
La fonction GRRLIB_3dMode(...) se décrit ainsi :
GRRLIB_3dMode(NearPlane, FarPlane, FoV, TextureMode, NormalMode);
Pour faire simple, les objets affichés sont ceux devant la caméra dont la distance est comprise entre NearPlane et FarPlane.
FoV représente l'angle d'ouverture de "l'objectif".
TextureMode et NormalMode indiquent au moteur si les textures et les normales sont utilisées (0 ou 1 pour chaque).

Code: (cpp)
GRRLIB_ObjectView(position.x, position.y, position.z, rotation.x, rotation.y, rotation.z, scale.x, scale.y, scale.z);
Cette ligne permet de positionner, tourner et ... scaler (définir l'echelle quoi) notre objet par rapport à la scène.

Ensuite on trouve les "classiques" d'OpenGL :
Code: (cpp)
GX_Begin(GX_TRIANGLES, GX_VTXFMT0, (nbFaces) * 3);
On déclare qu'on va afficher des triangles, selon un certain format (défini dans GRRLIB_3dMode), composés de (nbFaces * 3) sommets.

S'en suit une boucle dans laquelle on indique les positions, normales, couleur(ici blanc pour l'afficher "normalement", mais peut être modifié), et coordonnées de texture (s'il y en a) pour chaque vertex de la face en cours.

Et c'est tout !

Le reste est super simple :
- Un constructeur pour initialiser les valeurs
- Une fonction pour assigner une texture et passer la variable isTextured à true.
- Un destructeur pour dégager proprement tout ce merdier de la mémoire.

V'la le code complet de Object3D.cpp :
Code: (cpp)
#include "Object3D.h"

Object3D::Object3D(){
nbVertices = nbTexCoords = nbNormals = nbFaces = 0;
vertices = NULL;
texCoords = NULL;
normals = NULL;
faces = NULL;

texture = NULL;

isTextured = false;

position = {0.0, 0.0, 0.0};
rotation = {0.0, 0.0, 0.0};
scale = {1.0, 1.0, 1.0};
}

Object3D::~Object3D(){
if (vertices) free(vertices);
if (texCoords) free(texCoords);
if (normals) free(normals);
if (faces) free(faces);
}



bool Object3D::LoadModel(const char * fileName){

FILE * f = fopen(fileName,"r");

if (f){

char lect[128];

while(fscanf(f, "%s", lect) != EOF) {

switch(lect[0]){
case 'v':
switch(lect[1]){
case 0:
nbVertices++;
break;

case 't':
nbTexCoords++;
break;

case 'n':
nbNormals++;
break;

default:
fgets(lect, sizeof(lect), f);
break;
}
break;
case 'f':
if (lect[1] == 0){
nbFaces++;
}else{
fgets(lect, sizeof(lect), f);
}
break;
default:
fgets(lect, sizeof(lect), f);
break;
}

}

vertices = (guVector*)malloc(nbVertices * sizeof(guVector));
texCoords = (TexCoord*)malloc(nbTexCoords * sizeof(TexCoord));
normals = (guVector*)malloc(nbNormals * sizeof(guVector));
faces = (Face*)malloc(nbFaces * sizeof(Face));

int idxVertices = 0;
int idxNormals = 0;
int idxTexCoords = 0;
int idxFaces = 0;

rewind(f);

while(fscanf(f, "%s", lect) != EOF){

switch(lect[0]){

case 'v':
switch(lect[1]){
case 0:
fscanf(f, "%f %f %f", &vertices[idxVertices].x, &vertices[idxVertices].y, &vertices[idxVertices].z);
idxVertices++;
break;

case 'n':
fscanf(f, "%f %f %f", &normals[idxNormals].x, &normals[idxNormals].y, &normals[idxNormals].z);
idxNormals++;
break;

case 't':
fscanf(f, "%f %f", &texCoords[idxTexCoords].x, &texCoords[idxTexCoords].y);
texCoords[idxTexCoords].y = 1.0 - texCoords[idxTexCoords].y;
idxTexCoords++;
break;
default:
fgets(lect, sizeof(lect), f);
break;
}
break;

case 'f':
if (lect[1] == 0){
fscanf(f, "%d/%d/%d %d/%d/%d %d/%d/%d",
&faces[idxFaces].va, &faces[idxFaces].ta, &faces[idxFaces].na,
&faces[idxFaces].vb, &faces[idxFaces].tb, &faces[idxFaces].nb,
&faces[idxFaces].vc, &faces[idxFaces].tc, &faces[idxFaces].nc);

if (faces[idxFaces].va >= 0){
faces[idxFaces].va--; faces[idxFaces].vb--; faces[idxFaces].vc--;
faces[idxFaces].na--; faces[idxFaces].nb--; faces[idxFaces].nc--;
faces[idxFaces].ta--; faces[idxFaces].tb--; faces[idxFaces].tc--;
}else{

faces[idxFaces].va = faces[idxFaces].va * -1 - 1; faces[idxFaces].vb = faces[idxFaces].vb * -1 - 1; faces[idxFaces].vc = faces[idxFaces].vc * -1 - 1;
faces[idxFaces].na = faces[idxFaces].na * -1 - 1; faces[idxFaces].nb = faces[idxFaces].nb * -1 - 1; faces[idxFaces].nc = faces[idxFaces].nc * -1 - 1;
faces[idxFaces].ta = faces[idxFaces].ta * -1 - 1; faces[idxFaces].tb = faces[idxFaces].tb * -1 - 1; faces[idxFaces].tc = faces[idxFaces].tc * -1 - 1;
}

idxFaces++;

}else{
fgets(lect, sizeof(lect), f);
}
break;
default:
fgets(lect, sizeof(lect), f);
break;
}

}


fclose(f);

return true;
}else{
return false;
}

}


void Object3D::SetTexture(GRRLIB_texImg * tex){
if (!isTextured) isTextured = true;

texture = tex;
}


void Object3D::Render(){

if (isTextured){

GRRLIB_3dMode(0.1, 2000, 45, 1, 1);

GRRLIB_SetTexture(texture, false);

}else{
GRRLIB_3dMode(0.1, 2000, 45, 0, 1);
}

GRRLIB_ObjectView(position.x, position.y, position.z, rotation.x, rotation.y, rotation.z, scale.x, scale.y, scale.z);

GX_Begin(GX_TRIANGLES, GX_VTXFMT0, (nbFaces) * 3);
for (int i = 0; i < nbFaces; i++){
GX_Position3f32(vertices[faces[i].va].x, vertices[faces[i].va].y, vertices[faces[i].va].z);
GX_Normal3f32(normals[faces[i].na].x, normals[faces[i].na].y, normals[faces[i].na].z);
GX_Color1u32(0xFFFFFFFF);
if (isTextured)
GX_TexCoord2f32(texCoords[faces[i].ta].x, texCoords[faces[i].ta].y);

GX_Position3f32(vertices[faces[i].vb].x, vertices[faces[i].vb].y, vertices[faces[i].vb].z);
GX_Normal3f32(normals[faces[i].nb].x, normals[faces[i].nb].y, normals[faces[i].nb].z);
GX_Color1u32(0xFFFFFFFF);
if (isTextured)
GX_TexCoord2f32(texCoords[faces[i].tb].x, texCoords[faces[i].tb].y);

GX_Position3f32(vertices[faces[i].vc].x, vertices[faces[i].vc].y, vertices[faces[i].vc].z);
GX_Normal3f32(normals[faces[i].nc].x, normals[faces[i].nc].y, normals[faces[i].nc].z);
GX_Color1u32(0xFFFFFFFF);
if (isTextured)
GX_TexCoord2f32(texCoords[faces[i].tc].x, texCoords[faces[i].tc].y);
}
GX_End();

}
« Dernière édition: 15 Octobre 2010, 19:14:54 par EvilTroopa » Journalisée

A mushroom a day, keeps the koopas away.
EvilTroopa Hors ligne
Administrateur
*****
Messages: 648


Voir le profil WWW
1010011010 the Number of the Beast

« Réponse #2 : 15 Octobre 2010, 19:09:36 »

La classe SkySphere

Qu'est-ce qu'une SkySphere ?

Une SkySphere (ou aussi SkyDome, SkyBox, selon la forme) est un objet qui permet de simuler un grand fond autour de la scène.
Cette petite sphère est positionnée à la même position que la caméra, mais elle conserve toujours son orientation. De cette façon, on a l'illusion d'un fond situé à très grande distance.
On pourrait très bien faire une sphère géante qui contiendrait toute la scène, mais on prend le risque que ses faces soient au délà de la distance limite de la caméra, et ne soit donc pas affichée à l'écran.

L'astuce ici est de créer un petit objet (mais plus grand que la distance minimum de la caméra) qui sera rendu avant tout le reste. Le reste des objets s'afficheront après et par-dessus.
Mais alors quel intérêt de créer une nouvelle classe pour ça ? Il y en a 2, ou plutôt 1.5.
Déjà, il faut comprendre quelque chose qui s'appelle le Z-Buffer.
En gros (ouais vous avez vu, je rentre pas dans les détail, ça m'évite de dire des conneries), le Z-Buffer représente l'écran, mais contient seulement la distance des faces déjà affichés.
Si la distance d'un pixel projeté sur l'écran est supérieure à la valeur en cours, alors on laisse le pixel tel qu'il est. Ca veut dire qu'il y a déjà une face plus proche affichée, donc qui se trouve devant par rapport à la caméra.
Si la distance est inférieure, on affiche ce nouveau pixel et on met à jour la distance du pixel dans le Z-Buffer.
Donc problème, si on affiche une petite sphère, que ce soit en premier ou en dernier, y'a rien d'autre qui va s'afficher par dessus.
Il faut donc désactiver l'écriture dans le Z-Buffer au moment de l'affichage de la SkySphere.
Ensuite, il n'y a qu'une seule SkySphere par monde, c'est une utilisation assez spécifique, donc pas besoin de rajouter un paramètre à ma classe Object3D, autant faire une classe fille.


L'implémentation

La classe SkySphere est héritée de Object3D.
Il n'y a pas grand chose à modifier, au chargement du monde on va lui faire charger un fichier précis qui contient un modèle de sphère aux normales inversées.
Reste à retoucher la méthode Render :

Code: (cpp)
void SkyDome::Render(){
if (isTextured){

GRRLIB_3dMode(0.1, 2000, 45, 1, 0);

GRRLIB_SetTexture(texture, false);

}else{
GRRLIB_3dMode(0.1, 2000, 45, 0, 0);
}

GX_SetZMode(GX_FALSE, GX_LEQUAL, GX_FALSE);
GRRLIB_ObjectView(position.x, position.y, position.z, rotation.x, rotation.y, rotation.z, scale.x, scale.y, scale.z);

GX_Begin(GX_TRIANGLES, GX_VTXFMT0, (nbFaces) * 3);
for (int i = 0; i < nbFaces; i++){
GX_Position3f32(vertices[faces[i].va].x, vertices[faces[i].va].y, vertices[faces[i].va].z);
GX_Color1u32(0xFFFFFFFF);
if (isTextured)
GX_TexCoord2f32(texCoords[faces[i].ta].x, texCoords[faces[i].ta].y);

GX_Position3f32(vertices[faces[i].vb].x, vertices[faces[i].vb].y, vertices[faces[i].vb].z);
GX_Color1u32(0xFFFFFFFF);
if (isTextured)
GX_TexCoord2f32(texCoords[faces[i].tb].x, texCoords[faces[i].tb].y);

GX_Position3f32(vertices[faces[i].vc].x, vertices[faces[i].vc].y, vertices[faces[i].vc].z);
GX_Color1u32(0xFFFFFFFF);
if (isTextured)
GX_TexCoord2f32(texCoords[faces[i].tc].x, texCoords[faces[i].tc].y);
}
GX_End();

GX_SetZMode(GX_TRUE, GX_LEQUAL, GX_TRUE);

}

Vous pourrez retrouver le fichier complet dans l'archive en pièce jointe.
« Dernière édition: 15 Octobre 2010, 22:20:20 par EvilTroopa » Journalisée

A mushroom a day, keeps the koopas away.
EvilTroopa Hors ligne
Administrateur
*****
Messages: 648


Voir le profil WWW
1010011010 the Number of the Beast

« Réponse #3 : 15 Octobre 2010, 19:09:45 »

La classe GameApp


Le déroulement de l'application

Une fois lancé, le programme récupère la liste des fichiers présents dans un certain dossier de la carte SD.
Le menu est affiché. Il est composé d'un liste paginée des fichiers présents sur la carte, sans extensions.
Grace au pointeur, on cible le fichier voulu, A pour valider. Les boutons + et - permettent de changer de page.
Au clic, le fichier .obj désigné est chargé dans un Object3D.
Il cherche ensuite si un fichier png ayant le même nom existe. Si oui, il le charge et l'assigne à l'objet. Sinon, il lui assigne une texture par défaut.
On arrive sur notre scène 3D, le D-Pad et 1 et 2 permettent de naviguer autour du modèle. Le bouton A décharge la scène et fait revenir à la liste.
Pour quitter l'application, on presse le bouton Home quand on est sur la liste.


Les attributs et méthodes

J'ai cherché longtemps à comprendre comment marchait l'initialisation du moteur GX, j'ai compris quelques trucs, mais certains aspects me sont encore assez obscures.
J'ai donc décidé de pas trop me faire chier en utilisant ce qui existe déjà dans la GRRLIB. Donc on limite un peu le nombre de variables présentes dans GameApp pour lancer l'appli.

Les objets de la scène :
Code: (cpp)
Object3D * myObj;
SkySphere * skySphere;

Les ressources images :
Code: (cpp)
GRRLIB_texImg * texFont;
GRRLIB_texImg * texPointer;
GRRLIB_texImg * texDefault; // La texture appliquée par défaut si pas de PNG présent
GRRLIB_texImg * texObj; // La variable dans laquelle est chargé le PNG texture de l'objet
GRRLIB_texImg * texSky; // La variable dans laquelle est chargé le PNG "ciel" qui servira de fond

De quoi gérer la liste de fichiers :
Code: (cpp)
int numFiles;
char filesNames[MAX_FILES][80];
int fileToPlay;

Les différentes méthodes :
GameApp(); // Le constructeur, dans lequel on initialisera le moteur
void GetFiles(); // Récupère la liste des fichiers sur la SD
void Menu(); // Affiche la liste de fichiers
void View(); // Visualisation des modèles
void ComputeCamPos(float angle, float dist); // Calculer la position de la caméra pour sa rotation autour de l'objet



GameApp.h

Code: (cpp)
#include <grrlib.h>

#include <stdio.h>
#include <string.h>   
#include <stdlib.h>
#include <math.h>
#include <malloc.h>
#include <sys/dir.h>
#include <fat.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/stat.h>
#include <wiiuse/wpad.h>

#include "Object3D.h"
#include "SkyDome.h"

#include "font_png.h"
#include "pointer_png.h"
#include "test_png.h"
#include "sky_png.h"

#define MAX_FILES 120

class GameApp{

public:
void GetFiles();
GameApp();
~GameApp();

private:
GRRLIB_texImg * texFont;
GRRLIB_texImg * texPointer;
GRRLIB_texImg * texDefault;
GRRLIB_texImg * texObj;
GRRLIB_texImg * texSky;

Object3D * myObj;
SkyDome * skyDome;

void ComputeCamPos(float angle, float dist);
void Menu();
void View();

guVector camPos, camUp, camLookAt;

int numFiles;
char filesNames[MAX_FILES][80];
int fileToPlay;
};


Le constructeur :

Code: (cpp)
GameApp::GameApp(){
GRRLIB_Init();
    WPAD_Init();
   
WPAD_SetVRes(WPAD_CHAN_0,rmode->fbWidth,rmode->xfbHeight);
WPAD_SetDataFormat(WPAD_CHAN_0,WPAD_FMT_BTNS_ACC_IR);

GRRLIB_Settings.antialias = false;
texFont = GRRLIB_LoadTexture(font_png);
GRRLIB_InitTileSet(texFont, 8, 16, 0);

GRRLIB_Settings.antialias = true;
texSky = GRRLIB_LoadTexture(sky_png);
texDefault = GRRLIB_LoadTexture(test_png);
texPointer = GRRLIB_LoadTexture(pointer_png);
GRRLIB_SetMidHandle(texPointer, true);

GRRLIB_2dMode();
GRRLIB_SetBackgroundColour(0x00, 0x00, 0x00, 0xFF);

GetFiles();

skyDome = new SkyDome();
skyDome->LoadModel("sd:/apps/ETModelViewer/SkySphere.obj");
skyDome->SetTexture(texSky);

camPos = {0.0f, 5.0f, 40.0f};
camUp = {0.0, 1.0, 0.0};
camLookAt = {0.0, 0.0, 0.0};
}
Ici, rien de bien mystérieux.
Il faut faire attention toutefois à l'antialiasing des textures. Si on l'applique sur les polices ou sur des tiles, ça peut baver.


La méthode Menu

Code: (cpp)
void GameApp::Menu(){
bool over = false;
ir_t IR; //Pour le pointeur de la Wiimote
int currPage = 0;

while (!over){

GRRLIB_2dMode();

WPAD_ScanPads();  // Scan the Wiimotes
WPAD_IR(0, &IR);

fileToPlay = -1;

if (WPAD_ButtonsDown(0) & WPAD_BUTTON_HOME)  { over = true; }

if ((WPAD_ButtonsDown(0) & WPAD_BUTTON_PLUS) && currPage < numFiles / 8) currPage++;
if ((WPAD_ButtonsDown(0) & WPAD_BUTTON_MINUS) && currPage > 0) currPage--;

bool pressedA = (WPAD_ButtonsDown(0) & WPAD_BUTTON_A);


GRRLIB_Printf(640 / 2 - 24, 32, texFont, 0xFFFFFFFF, 1, "MODELS");

GRRLIB_Rectangle(64, 64, 640 - 128, 480 - 128, 0xA5B2F4FF, 1);


for (int i = 8 * currPage; i < 8 * currPage + 8 && i < numFiles; i++){

int bgClr = 0x525B82FF;

if (IR.x > 80 && IR.x < 640 - 160 && IR.y > 80 + i * 40 - (currPage * 8 * 40) && IR.y < 80 + i * 40 + 32 - (currPage * 8 * 40)){

bgClr = 0x262D41FF;

if (pressedA){
fileToPlay = i;
}
}

GRRLIB_Rectangle(80, 80 + i * 40 - (currPage * 8 * 40), 640 - 160, 32, bgClr, 1);
GRRLIB_Printf(80 + 12, 80 + i * 40 + 10 - (currPage * 8 * 40), texFont, 0xFFFFFFFF, 1, filesNames[i]);

}

GRRLIB_Printf(640 / 2 - 104, 400, texFont, 0xFFFFFFFF, 1, "+/- to change current page");

GRRLIB_DrawImg(IR.x, IR.y, texPointer, 0, 1, 1, 0xFFFFFFFF);

GRRLIB_Render();


if (fileToPlay != -1){
View();
GRRLIB_Render();
}

}
}
Bon y'a un peu de code dégueu là-dedans, mais c'est de la 2D, alors j'ai un peu la fleme de le décrypter.
On teste la position du pointeur, s'il y a clic sur A et qu'il pointe un élément du menu, on modifie la variable fileToPlay.
Arrivé en fin de boucle, si la valeur fileToPlay est différente de -1, on lance le fichier concerné.


La méthode View

Code: (cpp)
void GameApp::View(){

bool over = false;

float camDist = 10.0;
float camAngle = 0.0;


GRRLIB_Printf(80, 80, texFont, 0xFFFFFFFF, 1, "Creating Object");
GRRLIB_Render();
myObj = new Object3D();
GRRLIB_Printf(80, 80, texFont, 0xFFFFFFFF, 1, "Loading Object");
GRRLIB_Render();

char modelName[80];
sprintf(modelName, "/apps/ETModelViewer/models/%s.obj", filesNames[fileToPlay]);
bool checkLoading = myObj->LoadModel(modelName);
GRRLIB_Printf(80, 80, texFont, 0xFFFFFFFF, 1, "Loading Texture");
GRRLIB_Render();

char texName[80];
sprintf(texName, "/apps/ETModelViewer/models/%s.png", filesNames[fileToPlay]);
if (!(texObj = GRRLIB_LoadTextureFromFile(texName))){
texObj = texDefault;
}

GRRLIB_Printf(80, 80, texFont, 0xFFFFFFFF, 1, "Assigning Texture");
GRRLIB_Render();
myObj->SetTexture(texObj);

GRRLIB_Render();
GRRLIB_Render();


if (checkLoading){

over = false;

while(!over){

WPAD_ScanPads();
if(WPAD_ButtonsDown(0) & WPAD_BUTTON_A) over = true;

if (WPAD_ButtonsHeld(0) & WPAD_BUTTON_UP) camDist -= 0.3;
if (WPAD_ButtonsHeld(0) & WPAD_BUTTON_DOWN) camDist += 0.3;
if (WPAD_ButtonsHeld(0) & WPAD_BUTTON_LEFT) camAngle -= 0.03;
if (WPAD_ButtonsHeld(0) & WPAD_BUTTON_RIGHT) camAngle += 0.03;
if (WPAD_ButtonsHeld(0) & WPAD_BUTTON_2) myObj->rotation.x -= 2;
if (WPAD_ButtonsHeld(0) & WPAD_BUTTON_1) myObj->rotation.x += 2;

ComputeCamPos(camAngle, camDist); // On calcule la position de la caméra.

GRRLIB_3dMode(0.1, 2000, 45, 1, 1);
GRRLIB_Camera3dSettings(camPos.x, camPos.y, camPos.z, camUp.x, camUp.y, camUp.z, camLookAt.x, camLookAt.y, camLookAt.z);

GRRLIB_SetLightOff(); // On coupe la lumière pour que la SkySphere ait une illumination constante.
skyDome->position = camPos;
skyDome->Render();

//On rallume la lumière pour éclairer notre objet.
guVector myPos = {300, 500, 1000};
GRRLIB_SetLightAmbient(0x444444FF); // Lumière globale
GRRLIB_SetLightDiff(0, myPos, 400, 0.6, 0xF6F4E8FF); // Spot légèrement coloré

myObj->Render(); // On affiche l'objet

GRRLIB_Render(); //On affiche le rendu à l'écran
}

}else{ // S'il y a eu une erreur lors du chargement du modèle.

GRRLIB_2dMode();

while(!over){

WPAD_ScanPads();
if(WPAD_ButtonsDown(0)) over = true;

GRRLIB_Printf(80, 80, texFont, 0xFFFFFFFF, 1, "Loading error");

GRRLIB_Render();
}

}

}

Dans cette méthode on commence par charger le fichier dans le modèle.
En cas d'erreur un message est affiché et on retourne au menu.
Si tout va bien on peut naviguer autour de l'objet. Rien de bien complexe ici non plus.
Reste le code qui calcule la position de la caméra.



La méthode ComputeCamPos

Code: (cpp)
void GameApp::ComputeCamPos(float angle, float dist){
camPos.x = sin(angle) * dist;
camPos.z = cos(angle) * dist;
}
J'ai commencé cette fonction pendant la rendre plus complexe, en utilisant par exemple une rotation le long d'une sphère, mais je me contente d'un simple cerle, dont on peut faire varier le rayon.
De plus pour simuler la rotation de la caméra sur X, je fais plutôt tourner l'objet sur l'axe X. Et oui c'est naze de faire comme ça, mais j'avais pas envie de m'emmerder avec ça.



La méthode GetFiles

Code: (cpp)
void GetFiles(){

DIR * dp;
    struct dirent *entry;

numFiles = 0;

char * dir = "sd:/apps/ETModelViewer/models";

    dp = opendir(dir);

if (dp){

chdir(dir);

while((entry = readdir(dp)) != NULL){
if(strcmp(".", entry->d_name) == 0 ||
strcmp("..", entry->d_name) == 0)
continue;

if (numFiles < MAX_FILES - 1){

if (strstr(entry->d_name, ".obj") != NULL){
char shortName[80];

int nameLength = strlen(entry->d_name);

strncpy(shortName, entry->d_name, nameLength - 4);
shortName[nameLength - 4] = '\0';
sprintf(filesNames[numFiles], shortName);
numFiles++;
}
}
}

closedir(dp);

}

}
Ici on parcoure le dossier en question et on teste si l'extension du fichier est bien ".obj" (un peu à l'arrache je l'avoue).
Si c'est le cas, on copie le nom du fichier sans extension dans la variable fileNames.
On tâchera de reconstruire le nom de fichier complet au moment du chargement.
« Dernière édition: 15 Octobre 2010, 19:19:02 par EvilTroopa » Journalisée

A mushroom a day, keeps the koopas away.
EvilTroopa Hors ligne
Administrateur
*****
Messages: 648


Voir le profil WWW
1010011010 the Number of the Beast

« Réponse #4 : 15 Octobre 2010, 19:09:54 »

Conclusion


Voila !! On a tout qui marche !!

Je mets ci-dessous une archive contenant tout le code et une autre contenant le dossier à mettre sur la Wii dans sd:/apps/.
Si vous voulez compiler vous-même le code, faites-vous plaisir, mais oubliez pas de mettre un dossier contenant les modèles et images en gardant ma structure.
Et oubliez pas de mettre le SkySphere.obj dans le dossier principal. Sinon pas de fond.

Parmi les modèles que je joins dans l'archive 2 sont des rips de Zelda TP (vous les reconnaitrez tous seuls), le mecha est de mon graphiste (c'est celui qui a servir pour faire NanoMechas, oui madame), le reste est de moi.

Alors, petit bug connu et incompréhensible : sur la Wii de mon graphiste (le grand Olyfno) les noms de fichier qui font moins de 8 caractères ne marchent pas et ne sont pas lus. J'ai jamais compris pourquoi. Et surtout j'ai pas cherché.

J'espère que ce petit tuto vous aura plu et vous donnera envie de faire de la 3D sur Wii.


Pour les fans de syntaxe et d'orthographe, merci de me PM les éventuelles coquilles qui auraient échappé à mon attention.


Bon les pièces jointes partent en live, alors voila des liens vers les archives :
L'appli
Les sources
« Dernière édition: 17 Octobre 2010, 23:00:51 par EvilTroopa » Journalisée

A mushroom a day, keeps the koopas away.
Sheeft Hors ligne
Petit slip
Vraiment petit
*
Messages: 924


Voir le profil WWW
sexe : oui (ndPyroh : Ouais ouais...)

« Réponse #5 : 15 Octobre 2010, 20:58:54 »

Hum, sympa tout ça, j'avais pas vu ce tuto…
Peut-être que je me mettrais à GRRlib pour faire de la 3D plutôt qu'a libogc directement…
Journalisée


Pas si petit…
Synthesis Hors ligne
Global Moderator
*****
Messages: 176


Voir le profil WWW
Membre n° 341

« Réponse #6 : 15 Octobre 2010, 21:14:52 »

(SkySphere)
Cette petite sphère est positionnée à la même position que la caméra, mais elle conserve toujours son orientation. De cette façon, on s
wait... wha-?
Huh?
Journalisée

The Turrican DS Project
NitroTracker songs
Scape Original Soundtrack (Includes 3 leftovers)
EvilTroopa Hors ligne
Administrateur
*****
Messages: 648


Voir le profil WWW
1010011010 the Number of the Beast

« Réponse #7 : 15 Octobre 2010, 21:36:26 »

(SkySphere)
Cette petite sphère est positionnée à la même position que la caméra, mais elle conserve toujours son orientation. De cette façon, on s
wait... wha-?
Huh?

Mmmh, je vois pas si tu piges pas, ou si j'ai dit une connerie...
Si ta SkySphere tourne en même temps que la caméra, tu auras toujours les mêmes faces devant la caméra. Là l'intérêt c'est que si tu tournes ta caméra de 90° sur ta gauche, tu verras la gauche du "ciel". Sinon tu verras toujours le "devant" quelle que soit l'orientation de la caméra.
Journalisée

A mushroom a day, keeps the koopas away.
Synthesis Hors ligne
Global Moderator
*****
Messages: 176


Voir le profil WWW
Membre n° 341

« Réponse #8 : 15 Octobre 2010, 22:13:45 »

De cette façon, on s

C'est moi, ou t'as pas percuté ?

Edit: Oui, désolé, en fait j'aurais dû préc
« Dernière édition: 15 Octobre 2010, 22:18:37 par Synthesis » Journalisée

The Turrican DS Project
NitroTracker songs
Scape Original Soundtrack (Includes 3 leftovers)
EvilTroopa Hors ligne
Administrateur
*****
Messages: 648


Voir le profil WWW
1010011010 the Number of the Beast

« Réponse #9 : 15 Octobre 2010, 22:18:46 »

De cette façon, on s

C'est moi, ou t'as pas percuté ?

Ha ouais je suis super con Azn Je vais corriger de suite Langue

Edit: Oui, désolé, en fait j'aurais dû préc

Hahaha Cheesy
« Dernière édition: 15 Octobre 2010, 22:21:21 par EvilTroopa » Journalisée

A mushroom a day, keeps the koopas away.
EvilTroopa Hors ligne
Administrateur
*****
Messages: 648


Voir le profil WWW
1010011010 the Number of the Beast

« Réponse #10 : 17 Octobre 2010, 23:01:34 »

Il y avait une erreur dans les liens vers les archives, j'ai mis à jour.
Journalisée

A mushroom a day, keeps the koopas away.
lasouze Hors ligne
Newbie
*
Messages: 13


Voir le profil
« Réponse #11 : 19 Octobre 2010, 10:08:26 »

Je viens de voir ce topic, et je dis bravo Smiley
Ca va en aider plus d'un a faire de la 3d pour la Wii.
Reste plus qu'une chose : les lumières

Lasouze
Journalisée
Sheeft Hors ligne
Petit slip
Vraiment petit
*
Messages: 924


Voir le profil WWW
sexe : oui (ndPyroh : Ouais ouais...)

« Réponse #12 : 05 Février 2011, 14:00:22 »

J'ai enfin testé ça, c'est quand même franchement cool.
Je vais essayer de trouver le temps de bien comprendre ton code pour pouvoir t'en voler le fonctionnement et commencer à essayer de faire quelques petits trucs sympa.
Vivement la suite de tes tutos.

Sinon, y'a des trucs exprès pour créer des textures mappées comme ça non ? Parce que, pour celle du mécha (qui d'ailleurs est vraiment bien réussi) ne me semble pas évidente à faire… (Va vraiment falloir que je me trouve un graphiste 3D).
Journalisée


Pas si petit…
EvilTroopa Hors ligne
Administrateur
*****
Messages: 648


Voir le profil WWW
1010011010 the Number of the Beast

« Réponse #13 : 05 Février 2011, 14:50:21 »

J'ai enfin testé ça, c'est quand même franchement cool.
Je vais essayer de trouver le temps de bien comprendre ton code pour pouvoir t'en voler le fonctionnement et commencer à essayer de faire quelques petits trucs sympa.
Vivement la suite de tes tutos.

Merci ! Azn
Par contre, je trouve que c'est vraiment pas terrible par rapport à ce que j'ai fait depuis.
Une fois que j'aurais sorti un ou deux jeux avec mon nouveau moteur (quand il marchera pour de vrai), je ferai la suite !

Sinon, y'a des trucs exprès pour créer des textures mappées comme ça non ? Parce que, pour celle du mécha (qui d'ailleurs est vraiment bien réussi) ne me semble pas évidente à faire… (Va vraiment falloir que je me trouve un graphiste 3D).
Ouais pour les map un peu plus complexes, il faudrait apprendre à faire des UV sur Blender.
C'est pas ce qu'il y a de plus difficile, mais ça demande quand même un peu d'apprentissage.
Fais une recherche sur google ou youtube, tu devrais trouver des tutos vidéo assez sympa ! Smiley
Journalisée

A mushroom a day, keeps the koopas away.
Sheeft Hors ligne
Petit slip
Vraiment petit
*
Messages: 924


Voir le profil WWW
sexe : oui (ndPyroh : Ouais ouais...)

« Réponse #14 : 05 Février 2011, 15:09:28 »

1 ou 2 jeux ? Smiley
On doit s'attendre à quelques surprises pour la fin de l'année scolaire ?

Le truc, c'est que je pense que j'aurai pas vraiment le temps de faire des graphismes en plus du code, pour ce que je veux faire ça risque de demander trop de temps et comme j'ai le bac et tout…

Sinon, chez moi aussi, les modèles qui font moins de 9 caractères ne sont pas listés…

Aussi, en regardant un peu le code, j'ai pensé à truc… Tu codes à moitié en C et en C++ mais si tu codais juste en C++, il y aurait moyen de ne faire qu'une seule passe pour charger le modèle, en utilisant des std::vector par exemple… Après je ne sais pas si ce serait réellement moins lourd mais  bon…
« Dernière édition: 06 Février 2011, 10:13:08 par Sheeft » Journalisée


Pas si petit…
Pages: [1] 2   Haut de page
Imprimer

Aller à: