jeudi 2 février 2012

Normales de surface en OpenGL

Avant que nous puissions apprendre à charger des objets à partir d'un programme 3D, nous avons besoin de parler une autre caractéristique de l'OpenGL - Normales de surface (généralement appelée simplement comme «normales»). Une normale de la surface n'est rien de plus un vecteur (ou ligne) qui est perpendiculaire à la surface d'un polygone donné. Voici une belle illustration du concept (celui-ci est tirée de Wikipedia, et non pas de moi:)

OpenGL n'a pas besoin de connaître les normales pour rendre une forme, mais il ne leur avez besoin quand vous commencez à utiliser un éclairage directionnel. OpenGL a besoin des normales de surface de savoir comment la lumière interagit avec les polygones individuels.

Normales peut être calculé, mais OpenGL ES ne le ferai pas pour vous. Pour trouver la normale pour un triangle, il suffit de calculer le produit croisé vecteur des trois sommets. C'est assez facile, non? Eh bien, c'est peut-être été un moment depuis que vous avez pris la géométrie, alors laissez-plongée dans un peu plus ici (autant pour moi que pour toi - ma classe Geometry dernière a été plus de quinze ans).

Avant de regarder le code de calcul de la normale de la surface, nous allons faire une configuration peu. Jusqu'ici, tout au long de notre code, nous venons de traiter les vertex arrays où le premier élément est la position X, la seconde est la position Y, et la troisième est la position Z, alors il recommence avec le quatrième étant le X position pour le sommet suivant, etc. Pour plus de clarté dans notre exemple de code, cependant, nous allons définir une struct de tenir un sommet unique:
typedef struct {
GLfloat x;
GLfloat y;
GLfloat z;
} Vertex3D, Vector3D;
Cela ne devrait pas besoin de beaucoup d'explication, non? Trois valeurs en virgule flottante pour représenter un point dans l'espace. Notez, cependant, que nous avons en fait défini deux types basés sur cette structure,: Vertex3D et Vector3D. Informatiquement parlant, les vecteurs et les sommets sont les mêmes - ils sont représentés par un seul point dans l'espace tridimensionnel. Théoriquement, ils sont différents, cependant. Un sommet représente un point unique dans l'espace, tout en un vecteur représente à la fois une direction et une distance (généralement appelé «grandeur»).

C'est l'une de ces petites choses que les gens qui travaillent avec des graphismes en 3D et la géométrie euclidienne prendre pour acquis, mais rarement la peine d'expliquer aux nouveaux arrivants. Comment un seul point dans l'espace représentent une distance? Il prend deux points pour faire une ligne, pas vrai? Tout le monde sait cela.

La réponse est simple - l'autre point est supposé être à l'origine. Un vecteur est un segment de droite reliant l'origine au point dans l'espace représenté par la structure de données. En pratique, la distinction entre un vecteur et un sommet est souvent académique, puisque les structures de données sont les mêmes. Normales sont des vecteurs, et non pas des sommets, nous avons donc défini un type distinct pour chaque juste pour être sémantiquement précis.

Quoi qu'il en soit, de retour de cette tangente peu, pour rendre le code plus lisible, nous allons également définir une struct de tenir un seul triangle:
typedef struct {
Vertex3D v1;
Vertex3D v2;
Vertex3D v3;
} Triangle3D;
Donc, étant donné un triangle, voici comment nous calculons la surface normale:
Vector3D calculateTriangleSurfaceNormal(Triangle3D triangle)
{
Vector3D surfaceNormal;
surfaceNormal.x = (triangle.v1.y * triangle.v2.z) - (triangle.v1.z * triangle.v2.y);
surfaceNormal.y = -((triangle.v2.z * triangle.v1.x) - (triangle.v2.x * triangle.v1.z));
surfaceNormal.z = (triangle.v1.x * triangle.v2.y) - (triangle.v1.y * triangle.v2.x);
return surfaceNormal;
}
Normales de surface, cependant, sont généralement «normalisé» de sorte qu'ils ont une longueur d'un. Cela rend les calculs plus rapides. Ainsi, afin de normaliser une normale (dire que dix fois rapide debout sur la tête), nous devons d'abord comprendre l'ampleur (longueur) de la surface normale:
GLfloat vectorMagnitude(Vector3D vector)
{
return sqrt((vector.x * vector.x) + (vector.y * vector.y) + (vector.z * vector.z));
}
Eww ... maintenant que vous commencez à voir pourquoi OpenGL ne fait pas cela pour nous à l'exécution - les performances. Racine carrée est un calcul coûteux, et il aurait à le faire pour chaque polygone. Mais nous ne sommes toujours pas fait. Nope, une fois que nous avons la longueur du vecteur, alors nous pouvons normaliser comme ceci:
void normalizeVector(Vector3D *vector)
{
GLfloat vecMag = vectorMagnitude(*vector);
vector->x /= vecMag;
vector->y /= vecMag;
vector->z /= vecMag;
}
Remarque: Les choses peuvent devenir de la confusion avec la terminologie ici. «Normaliser» un vecteur n'a rien à voir avec une surface "normale". Une normale de surface est appelé un "normal" basée sur l'utilisation du mot «normal» comme un synonyme de «perpendiculaire». D'autre part, quand vous êtes «normaliser» un vecteur, vous réduisez ce vecteur d'une norme (ou "normal") grandeur. Ainsi, en normalisant une normale est cool - une normale ne doit pas être normalisé, mais il est souvent, et avec OpenGL, ils devraient être d'éviter de causer le shader de travail supplémentaire.

Seulement, si vous regardez l'illustration ci-dessus, il ya effectivement deux normales, pointant dans des directions opposées - des triangles ont deux côtés, et donc deux normales potentiels. Les polygones dans la plupart des objets 3D sur ordinateur qu'une seule normale, et c'est pointant vers l'extérieur de l'objet. Il n'y a aucun point dans la définition de la normale pour le côté que la lumière ne sera jamais frappé parce qu'il est à l'intérieur d'un objet (le côté sans une normale de surface est souvent appelé un "contreface»). OpenGL aurait aucun moyen de savoir ce qui normal d'utiliser si elle essayait de les calculer pour vous, alors qu'il aurait à utiliser les deux - une autre performance, car il serait à faire des calculs sur des polygones qui ne pourrait jamais être vu.

Les deux normales à la surface d'un triangle (ou tout autre polygone) pointent dans des directions complètement opposées - 180 ^ 0 - il est donc facile à "retourner une normale» de sorte qu'il est représentant de l'autre côté du triangle:
void flipVector(Vector3D *vector)
{
vector->x = -vector->x;
vector->y = -vector->y;
vector->z = -vector->z;
}
Maintenant que je vous ai montré comment calculer, normaliser, et les vecteurs de flip, tout oublier. En général, vous n'aurez pas besoin de s'inquiéter sur le calcul des normales de surface 3D, car les programmes de modélisation informatique comme Maya, 3D Max, LightWave, et Blender va calculer ces normales pour vos objets, et vous pouvez exporter vos données 3D de sorte que vous avez le pré -calculé, les normales normalisé disponible dans le cadre de votre modèle de données (quelque chose que nous allons examiner avant trop longtemps)

Voici le processus de base pour utiliser les normales en OpenGL. Pour dire OpenGL que vous allez lui fournir des normales, vous devez appeler
glEnableClientState(GL_NORMAL_ARRAY);
Ceci est habituellement fait une fois lors du démarrage, mais pourrait également être activé et désactivé lors de l'exécution si vous aviez seulement des normales pour certains objets.

Données normales, comme à peu près tout le reste, nous avons travaillé avec OpenGL ES 1.1, doivent être emballés dans un tableau (un «tableau normal»). Avant d'appeler glDrawElements (), vous avez à nourrir OpenGL tableau en utilisant l'glNormalPointer () fonction, comme:
glNormalPointer(GL_FLOAT, 0, myNormalArray);
Le premier paramètre indique à OpenGL que notre tableau est un tableau de GLfloats. La seconde ... simplement ignorer le second pour l'instant - c'est celui-là même je continue à vous raconter d'ignorer, celle qui vous permet d'ignorer les données dans le tableau. Le troisième et dernier argument est un pointeur sur les données réelles normale.

C'est tout ce qu'il ya à faire. Vous ne remarquerez aucune différence dans le dessin de l'objet avec ou sans les normales de surface jusqu'à ce que vous commencez à ajouter un éclairage directionnel (que nous allons faire dans un futur blog affichage), mais je voulais que vous avez une bonne compréhension sur ce que les normales ont été et pourquoi nous en avons besoin avant que nous obtenions à essayer de charger un modèle 3D.

Une dernière note - si vous vous demandez pourquoi nous travaillons autant avec structures C et les fonctions C plutôt que de déclarer classes Objective-C, la réponse est encore une fois la performance. Il ya un peu des frais généraux additionnels associés à l'instanciation d'objets Objective-C et avec la messagerie dynamique. C'est frais généraux est trivial dans la plupart des applications, mais un modèle 3D sera généralement constituée de milliers de triangles. Vous ne souhaitez pas l'instanciation supplémentaires ou d'expédition aérienne dans cette situation, croyez-moi, surtout pas sur un petit appareil comme l'iPhone. Il peut être judicieux de déclarer une classe de détenir un modèle 3D, mais de ne pas tenir les polygones individuels ou des sommets. Trop frais, trop peu d'avantages.

Aucun commentaire: