jeudi 2 février 2012

Le début d'une classe WaveFront OBJ Loader Fichier

Une des questions les plus fréquentes J'ai reçu sur la programmation iPhone est de savoir comment vous chargez des modèles 3D à partir de programmes tiers. La plupart des gens qui sont nouveaux pour OpenGL et / ou nouvelles de la programmation iPhone supposer qu'il existe une certaine fonctionnalité intégrée pour charger des modèles à partir du fichier.

Nope. Il n'est pas. Vous devez faire vous-même.

J'ai commencé à écrire une classe qui peut charger des fichiers OBJ Wavefront 3D et dessiner le contenu en OpenGL ES. J'ai choisi ce format de fichier parce que c'est un format simple à base de texte, ce qui le rendra facile pour illustrer l'idée de base. Maintenant, il ya beaucoup de fonctionnalités que vous pourriez avoir besoin dans vos programmes que le format de fichier OBJ ne peut pas gérer, comme les animations, par exemple. Les trois objets utilisés dans ce projet sont des formes simples exportés à partir de Blender, mais le programme 3D que vous utilisez n'a pas d'importance, aussi longtemps que vous vous souvenez de ne pas exporter les quads ou plus polygones. La plupart des programmes ont une option pour exporter uniquement des triangles, alors assurez-vous de l'utiliser. OpenGL ES n'aime pas quads, souvenez-vous. Je suppose, que nous pourrions rendre cette classe plus robuste en subdivisant les polygones en triangles plus grands, mais pour l'instant, je vais juste de supposer que le fichier a des triangles et, il incombe au moi d'utiliser uniquement les fichiers exportés de cette façon.



Comme vous pouvez le voir, les objets peuvent être chargées et affichées. Actuellement, les données de vertex est chargé dans un tableau de vertex et les données polygone est chargé dans un tableau de visages. Je n'ai pas fait les normales ou les coordonnées de texture encore ou tout travail de surface à tout, mais ce n'est pas un mauvais départ pour un dimanche après-midi.

Remarquez comment les objets plats regard, cependant. C'est parce que, sans les normales de surface, OpenGL n'a aucun moyen de calculer combien la lumière devrait rebondir.

Vous pouvez trouver le Xcode projet ici.

Voici l'approche de base que j'ai pris. D'abord, je définis deux structures - dont l'un vous pourriez avoir vu dans ma post sur les normales de surface.
typedef struct {
GLfloat x;
GLfloat y;
GLfloat z;
} Vertex3D, Vector3D, Rotation3D;

// A Face 3D contains three indices to vertices, generally faster to use...
typedef struct {
GLushort v1;
GLushort v2;
GLushort v3;
} Face3D;
Je déclare l'interface de ma nouvelle classe comme ceci:

@interface OpenGLWaveFrontObject : NSObject {
Vertex3D *vertices;
int numberOfFaces;
Face3D *faces;
Vertex3D currentPosition;
Rotation3D currentRotation;
}
@property Vertex3D currentPosition;
@property Rotation3D currentRotation;
- (id)initWithPath:(NSString *)path;
- (void)drawSelf;
@end
Donc, il ya un pointeur sur une Vertex3D. Quand je charge les données, je vais compter le nombre de sommets il ya dans le fichier, et d'allouer une portion de mémoire suffisamment grand pour contenir que de nombreuses Vertex3D objets. Cela signifie que nous pouvons voir le premier sommet comme sommets [0], le second comme sommets [1], etc Cela signifie également que les sommets pointeur peut être passé directement dans glVertexPtr.

Yep, même si je suis déclarer mes propres structures de données pour stocker les données, parce que mon structures contenant les données que les besoins d'OpenGL, dans un format qu'il comprend et dans l'ordre dont il a besoin, je peux juste passer le pointeur vers elle. Rappelez-vous, l'ordre est important. Si avait fait le premier élément dans ma struct Vertex3D y au lieu de x, alors il n'aurait pas fonctionné.

Aussi, n'oubliez pas que "sauter" des paramètres que je vous raconte gardé à ignorer? Eh bien, si nous avions des éléments supplémentaires dans cette structure, on pouvait encore passer le pointeur à OpenGL en utilisant cet argument pour lui dire de sauter les éléments qu'il n'a pas besoin, dans ce cas, les éléments qui ne sont pas des données de vertex. Nous n'avons pas ici, mais j'ai pensé que je le mentionne si vous savez ce que cela a été pour le paramètre skip.

Généralement, j'évite de faire sauter à l'aide. Il ya des situations où vous pouvez obtenir un gain de performance par l'emballage de multiples types de données dans un tableau unique et en utilisant la variable qui sautent à raconter OpenGL lesquels utiliser. C'est une optimisation, et dans mon esprit, ne devraient pas être utilisées sauf si vous avez des problèmes de performance qui doivent être abordées. Je n'aime pas ajouter plus de complexité que ce qui est nécessaire, donc je commence avec le scénario le plus simple - contenant chaque type de données dans son propre tableau.

Si cela est un peu déroutant, il suffit de penser de cette façon:
Vertex3D vertices[5]
alloue exactement la même quantité de mémoire que
GLfloat vertices[15]
Vertex3D contient trois GLfloats, donc cinq d'entre eux est le même que quinze GLFloats. C'est juste une façon différente d'organiser le même bloc de données. Cela nous permet de consulter en utilisant leurs sommets x, y, z et des valeurs dans notre code sans avoir à payer n'importe quel prix la performance, parce que le code compilé aura le même aspect. Doux, hein?

Nous avons aussi une variable d'instance pour garder une trace du nombre de visages qui ont été chargées à partir du fichier. Il sera utilisé pour conduire la boucle qui appelle glDrawElements().

The Face3D pointeur fonctionne exactement de la même manière que le Vertex3D pointeur travaille - ça va être le tableau qui a des indices aux sommets utilisés dans chacun des triangles en forme et nous allons l'alimenter dans le glDrawElements () appel. Là encore, la structure nous donne juste une autre façon d'organiser les données que gros morceau o 'qui a besoin d'OpenGL. Elle rend plus facile à traiter dans notre code, mais ne modifie pas les données réelles d'aucune façon.

Nous avons également deux variables d'instance plus à garder la trace de la position actuelle et la rotation actuelle de cet objet. Comme nous allons faire de cet objet autonome, de sorte qu'il sait se dessiner, il a besoin de savoir où il se trouve dans le monde virtuel, et où il est confronté.

Notez que nous avons deux méthodes - l'une pour initialiser l'objet basée sur le chemin vers un fichier, et un autre qui raconte cet objet pour lui-même daw. Examinons tout d'abord à la méthode init. Maintenant, ce n'est pas fait - que nous allons faire plus de travail sur toute la ligne pour charger les normales, coordonnées de texture, etc. Je ne voulais pas attendre que tout cela était fait pour le blog, cependant, parce que ça va être un peu écrasante à ce point, avec toutes les données que nous allons tirer po Ce message, nous nous concentrons sur les sommets et les visages seulement.

Voici la méthode init:
- (id)initWithPath:(NSString *)path
{
if ((self = [super init]))
{

NSString *objData = [NSString stringWithContentsOfFile:path];
NSUInteger vertexCount = 0, faceCount = 0;
// Iterate through file once to discover how many vertices, normals, and faces there are
NSArray *lines = [objData componentsSeparatedByString:@"\n"];
for (NSString * line in lines)
{
if ([line hasPrefix:@"v "])
vertexCount++;
else if ([line hasPrefix:@"f "])
faceCount++;
}
NSLog(@"Vertices: %d, Normals: %d, Faces: %d", vertexCount, faceCount);
vertices = malloc(sizeof(Vertex3D) * vertexCount);
faces = malloc(sizeof(Face3D) * faceCount);

// Reuse our count variables for second time through
vertexCount = 0;
faceCount = 0;
for (NSString * line in lines)
{
if ([line hasPrefix:@"v "])
{
NSString *lineTrunc = [line substringFromIndex:2];
NSArray *lineVertices = [lineTrunc componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
vertices[vertexCount].x = [[lineVertices objectAtIndex:0] floatValue];
vertices[vertexCount].y = [[lineVertices objectAtIndex:1] floatValue];
vertices[vertexCount].z = [[lineVertices objectAtIndex:2] floatValue];
// Ignore weight if it exists..
vertexCount++;
}
else if ([line hasPrefix:@"f "])
{
NSString *lineTrunc = [line substringFromIndex:2];
NSArray *faceIndexGroups = [lineTrunc componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
// Unrolled loop, a little ugly but functional
/*
From the WaveFront OBJ specification:
o The first reference number is the geometric vertex.
o The second reference number is the texture vertex. It follows the first slash.
o The third reference number is the vertex normal. It follows the second slash.
*/
NSString *oneGroup = [faceIndexGroups objectAtIndex:0];
NSArray *groupParts = [oneGroup componentsSeparatedByString:@"/"];
faces[faceCount].v1 = [[groupParts objectAtIndex:kGroupIndexVertex] intValue]-1; // indices in file are 1-indexed, not 0 indexed
oneGroup = [faceIndexGroups objectAtIndex:1];
groupParts = [oneGroup componentsSeparatedByString:@"/"];
faces[faceCount].v2 = [[groupParts objectAtIndex:kGroupIndexVertex] intValue]-1;
oneGroup = [faceIndexGroups objectAtIndex:2];
groupParts = [oneGroup componentsSeparatedByString:@"/"];
faces[faceCount].v3 = [[groupParts objectAtIndex:kGroupIndexVertex] intValue]-1;
faceCount++;

}
}
numberOfFaces = faceCount;

}
return self;
}
Il commence assez simple en chargeant les données d'objet dans une chaîne. Comme il s'agit d'un format de fichier basé sur du texte, il est sûr de le charger dans une chaîne. Cela fait en sorte que notre vie plus facile grâce à toutes les fonctionnalités d'une grande NSString. Nous déclarons deux variables qui seront utilisées pour suivre le nombre de sommets dans le fichier, et le nombre de polygones.

Nous avons en fait itérer sur les données deux fois. La première fois à travers, on ne fait que compter les sommets et les faces dans le fichier de sorte que nous savons combien de mémoire à allouer. Une fois que nous savons combien de chaque, nous avons, nous allouer un bloc de mémoire pour chaque:
  vertices = malloc(sizeof(Vertex3D) * vertexCount);
faces = malloc(sizeof(Face3D) * faceCount);
Ensuite, nous avons réinitialisé les variables de comptage et de recommencer en boucle à travers le fichier. Cette fois, nous analysons les vecteurs et les données visage et de les stocker dans les endroits appropriés dans notre tableau de vertex array et de visage. La seule véritable chasse aux sorcières ici est que les indices dans le fichier OBJ sont 1-indexés, et les tableaux C sont 0-indexés, mais c'est facilement manipulé par soustraction d'un chemin. Quand nous sommes tous fait une boucle, on sauve hors du nombre de faces dans notre dernière variable d'instance numberOfFaces donc nous savons combien de triangles que nous avons à dessiner.

Si tout a été un succès, nous revenons de soi, comme toute méthode qui se respecte init.

Tout ce qui reste est d'écrire le code pour dessiner ces données de vertex. Encore une fois, cette méthode sera un peu plus complexe que nous ajoutons des normales et des données de texture. Voici ce que l'aspect méthode de tirage comme maintenant:
- (void)drawSelf
{
// Save the current transformation by pushing it on the stack
glPushMatrix();

// Load the identity matrix to restore to origin
glLoadIdentity();

// Translate to the current position
glTranslatef(currentPosition.x, currentPosition.y, currentPosition.z);

// Rotate to the current rotation
glRotatef(currentRotation.x, 1.0, 0.0, 0.0);
glRotatef(currentRotation.y, 0.0, 1.0, 0.0);
glRotatef(currentPosition.z, 0.0, 0.0, 1.0);

// Enable and load the vertex array
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3, GL_FLOAT, 0, vertices);

// Loop through faces and draw them
for (int i = 0; i {
glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, &faces[i]);
}

glDisableClientState(GL_VERTEX_ARRAY);

// Restore the current transformation by popping it off
glPopMatrix();
}
Pas tant que ça à elle, est là. Le premier appel et le dernier ne rien faire de plus de sauvegarder et de restaurer la transformation actuelle - de cette façon, nous pouvons charger identité, déplacer et faire pivoter si nécessaire, puis restaurer la transformation lorsque nous sommes Don de telle sorte que le dessin fait par d'autres objets fonctionnent comme prévu.

Nous réinitialiser la transformation en utilisant glLoadIdentity (), puis utilisez glTranslate () et glRotate () pour placer cet objet basé sur la position actuelle et la rotation tel qu'il est stocké dans nos variables d'instance.

Parce que nous ne savons pas ce que d'autres objets seront faites, nous permettent GL_VERTEX_ARRAY avant de tirer, et le désactiver quand nous aurons terminé. Puis nous appelons glVertexPointer () pour passer en sommets - voir comment la vie est facile grâce à cette structure Vertex3D?

Après cela, on boucle à travers nos visages et les passer à glDrawElements () exactement comme nous l'avons fait revenir à la leçon NeHe 05.

Maintenant, pour utiliser cette fonctionnalité, il est sacrément facile. D'abord, bien sûr, nous devons créer une instance d'objet basé sur un fichier afin de charger la forme plane, nous le faisons dans notre méthode setupView:
 NSString *path = [[NSBundle mainBundle] pathForResource:@"plane" ofType:@"obj"];
OpenGLWaveFrontObject *theObject = [[OpenGLWaveFrontObject alloc] initWithPath:path];
Vertex3D position;
position.z = -8.0;
position.y = 3.0;
position.x = 0.0;
theObject.currentPosition = position;
self.plane = theObject;
[theObject release];
Cette charge l'objet en mémoire et définit sa position initiale. Le même processus est utilisé pour les autres formes aussi. Puis, quand nous voulons dessiner la forme de notre méthode drawView, tout ce que nous avons à faire est de définir la couleur (qui ne sera pas nécessaire lorsque nous aurons texturage et de matériaux de travail) et ensuite lui dire de se dessiner.
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glColor4f(0.0, 0.5, 1.0, 1.0);
[plane drawSelf];
Vachement facile, n'est-ce pas? Si vous n'avez pas, téléchargez le Xcode projet et de lui donner un essai pour vous-même.

Je vais être en ajoutant des normales, des textures et des couleurs face dans un proche avenir, mais j'espère que cela vous permet de vous familiariser avec le concept de base de chargement du modèle.

Aucun commentaire: