dimanche 18 mars 2012

OpenGL ES 2.0 pour iOS, Chapitre 4 - Présentation du pipeline programmable

Le code de ce chapitre peut être trouvé here.
Je l'ai mentionné OpenGL ES 2.0 de l ' pipeline programmable, Mais il peut ne pas être clair pour vous exactement ce que ce terme signifie. Si c'est le cas, disons clairement jusqu'à maintenant. Le terme «pipeline» se réfère à la séquence des événements, à partir de quand vous dites OpenGL ES de dessiner quelque chose (généralement appelé rendering), Par le point où les objets présentés ont été entièrement dessinée. Typiquement, un programme OpenGL ES est à plusieurs reprises le dessin comme le programme s'exécute, avec chaque image achevée début considéré comme un frame.

Versions d'OpenGL ES 2.0 avant (y compris 1.1, qui est soutenu par tous les appareils iOS) utilisé ce qu'on appelle un pipeline de rendu fixe, Ce qui signifie que l'image finale a été générée par OpenGL ES sans aucune chance pour vous de faire quelque chose. Un meilleur terme car il pourrait avoir été «pipeline fermé», parce que vous fourrer des choses dans un seul but, et il sort de l'autre bout et vous n'avez aucune possibilité de l'influencer une fois qu'il commence à descendre le pipeline

Dans le pipeline fixe, la totalité de l'image est rendue sur la base des valeurs que vous soumettez à OpenGL ES dans les appels de votre application API précédentes. Chaque fois que l'OpenGL ES 1.x rend quelque chose, il le fait en utilisant le même ensemble d'algorithmes et de calculs. Si vous voulez une lumière, par exemple, vous appelez une poignée de fonctions OpenGL ES dans votre code d'application pour définir le type de lumière que vous voulez, la position de la lumière, la force de la lumière, et peut-être un peu d'attributs d'autres. OpenGL ES 1.1 prend alors les informations que vous avez fournis et fait tout les calculs nécessaires pour ajouter la lumière à votre scène. Il figure sur la manière de l'ombre de vos objets afin qu'ils ressemblent à la lumière les frappe et les entraîne en conséquence. Le pipeline fixe, vous isole de beaucoup de choses. Il dit essentiellement: «Oh, miel ... me donner des informations sur votre scène, et ne vous inquiétez pas de votre jolie petite tête de toutes les mathématiques."

La bonne chose au sujet des programmes de pipeline fixe, c'est qu'il est conceptuellement simple et facile. Oh, je sais ... il ne feel facile quand vous êtes à l'apprendre, mais par rapport au pipeline programmable, l'idée de base est beaucoup plus facile à saisir. Vous souhaitez créer une vue qui simule la perspective? OpenGL ES sera essentiellement le faire pour vous si vous lui donnez une poignée d'entrées en utilisant un couple d'appels API. Souhaitez déplacer, faire pivoter ou redimensionner un objet? Il ya des fonctions pour le faire pour vous aussi. Voulez-vous ajouter une lumière ou six à la scène? Il suffit de faire quelques appels par la lumière avant de tirer, et vous êtes bon à faire.

La mauvaise chose au sujet de la canalisation fixe est qu'elle limite ce que vous pouvez faire. Un grand nombre de l'éclairage de fantaisie et d'effets de texture que vous voyez dans les jeux 3D modernes, par exemple, ne peut être créé facilement (ou pas du tout) en utilisant le pipeline fixe. Avec le pipeline fixe, vous êtes seulement en mesure de faire ce que les auteurs de la bibliothèque graphique anticipé vous pourriez avoir besoin de faire, dans la façon dont ils ont anticipé vous auriez besoin de le faire. Vous voulez un lens flare ou la profondeur de champ? Eh bien, vous pouvez probablement trouver un moyen de faire ce genre de choses en utilisant le pipeline fixe, mais il ne sera pas facile ni simple. Des gens sont venus avec quelques contournements vraiment ingénieux pour déjouer les limites de la canalisation fixe, mais même si vous parvenez à trouver un moyen de contourner les limitations de la canalisation fixe pour atteindre un certain effet, votre code est susceptible d'être un peu un hack --- et plus important encore, une partie du code que vous écrivez pour l'implémentation de cette fonctionnalité va probablement être exécuté dans le mauvais endroit pour de meilleures performances. Parlons pourquoi c'est, parce que c'est un élément essentiel de l'information une fois que nous commencer à travailler avec le pipeline programmable.

Architecture matérielle

OpenGL ES, vous isole d'avoir à code à tout un matériel spécifique, mais il est important de comprendre, au moins à un niveau très élevé, comment les appareils iOS calculer et d'afficher du contenu graphique. Chaque appareil iOS jamais fait a deux processeurs à l'intérieur. Ils ont tous un processeur à usage général a appelé le processeur, ainsi que d'un second processeur appelé GPU, ce qui signifie unité de traitement graphique.^ 1Le CPU peut ne plus rien vous devez faire, et c'est là où le code de votre application fonctionne principalement. Le processeur est très rapide à faire des opérations entières, mais pas n'importe où près aussi rapide quand il s'agit de faire des opérations en virgule flottante ^ 2. Le GPU, d'autre part, est beaucoup plus spécialisé. C'est bon pour faire un grand nombre de petits calculs en virgule flottante très rapidement. Il a été conçu pour travailler comme aide à la CPU pour gérer ces tâches que le CPU n'est pas particulièrement bon, plutôt que de servir comme un processeur autonome. La CPU, en substance, les mains hors de certaines tâches que le GPU est mieux à la scène. Avec deux processeurs travaillant en parallèle, l'appareil est capable de faire beaucoup plus de travail à la fois. Mais cette «aide» ne se fait pas automatiquement dans vos programmes.

Lorsque vous écrivez C, Objective-C ou C + + code dans Xcode, le code binaire compilé s'exécute sur le processeur. Il ya quelques bibliothèques, telles que Core Animation, qui implicitement la main hors tâches au GPU en votre nom, mais en général, vous devez utiliser des bibliothèques spéciales pour avoir accès au GPU. Heureusement, OpenGL ES, est juste une telle bibliothèque. Tous les deux avec le pipeline fixe et le pipeline programmable, la plupart du rendu OpenGL ES qui se passe sur le GPU. Cela fait sens, si vous pensez cela: des calculs pour les graphismes sont ce que le GPU a été conçu pour faire (d'où le «G» dans le GPU). Beaucoup de pipeline OpenGL ES 2.0 's, comme l'ensemble des canalisations fixes, est hors de votre contrôle. Il ya, cependant, deux endroits spécifiques où vous pouvez (et, en fait, doit) écrire du code. Le code que vous écrivez s'exécute sur le GPU et ne peut pas être écrite en Objective C, C ou C + +. Elle doit être écrite dans une langue particulière spécialement conçu à cet effet. Programmes que vous écrivez pour le pipeline programmable fonctionner sur le GPU et sont appelés shaders. La langue que vous écrivez dans les shaders GLSL est appelé, ce qui signifie GL Shading Language.

Le terme shader est un autre exemple de nommer non intuitif en OpenGL. Les shaders sont rien de plus que de petits morceaux de code qui s'exécutent sur le GPU au lieu du CPU. Parmi les tâches qu'ils accomplissent est le calcul de l'ombrage (ou couleur) de chaque pixel qui représente un des objets virtuels, mais ils peuvent faire beaucoup plus que cela. Les shaders sont des programmes à part entière écrite dans un langage de programmation Turing-complet.

OpenGL ES Shaders

Il existe deux types de shaders que vous pouvez écrire pour OpenGL ES: vertex shaders and fragment shaders. Ces deux shaders forment le "programmable" partie du pipeline OpenGL ES 2.0 programmable. Le langage GLSL que vous programmez ces shaders avec utilise une syntaxe C-like. Nous allons étudier quelques exemples simples de GLSL dans ce chapitre, et nous allons travailler avec lui longuement dans le reste du livre.

Une chose importante à réaliser sur les shaders, c'est qu'ils ne sont pas compilées lorsque vous générez votre application. Le code source de votre shader est stockée dans votre application bundle comme un fichier texte, ou dans votre code comme une chaîne littérale. A l'exécution, avant d'utiliser les shaders, votre application doit charger et de les compiler. La raison de cette compilation différée est de préserver l'indépendance de périphérique. Si les shaders ont été compilées quand vous avez construit votre application, puis d'Apple ont été changer pour un autre fabricant GPU pour une version iPhone de l'avenir, les shaders compilés pourrait très bien ne pas fonctionner sur le nouveau GPU. Reporter le compiler pour l'exécution évite ce problème, et tout le GPU --- même ceux qui n'existent pas lorsque vous créez votre application --- peut être entièrement pris en charge sans besoin de reconstruire votre application.

Vertex Shaders

Le shader qui passe d'abord dans le pipeline OpenGL ES est appelé le vertex shader parce qu'il s'exécute une fois pour chaque vertex que vous soumettez à OpenGL ES. Si vous soumettez une scène 3D avec un millier de sommets, le vertex shader sera appelée mille fois lorsque vous soumettez cette scène d'OpenGL ES à tirer, une fois par sommet. Le vertex shader est l'endroit où vous faites ce que des appels OpenGL ES imaginative vertex. C'est là que vous manipulez le déplacement, le détartrage, ou des objets en rotation, en simulant la perspective de la vision humaine, et de faire tout autre calcul qui affecte sommets ou dépend d'un certain morceau de données que vous avez sur une base par vertex.

Shaders n'ont pas de valeurs de retour, mais deux shaders de vertex et fragment ont exigé des variables de sortie que vous devez écrire une valeur avant la fin des années shaders main() fonction. Ces variables de sortie fonctionnent essentiellement comme des valeurs de retour exigé. Pour le vertex shader, la puissance requise est la position finale de l'actuel sommet. Rappelez-vous, le shader est appelée une fois pour chaque sommet, alors la sortie du shader est la position finale du sommet, le shader est en cours d'exécution pour. Dans certains cas, il peut juste être la valeur d'origine le sommet, mais le plus souvent, chaque sommet sera modifié en quelque sorte. Faire des calculs sur les sommets à l'échelle, faire pivoter ou déplacer un objet est quelque chose que le GPU est beaucoup mieux que le CPU, si typiquement, nous ne cherchons pas à mettre en œuvre ce genre de calculs dans notre code de l'application, mais au lieu de les faire ici le vertex shader. Une fois votre shader a calculé la position finale d'un sommet donné, il a besoin de définir une variable de sortie spécial appelé gl_Position. Si votre vertex shader ne pas écrire gl_Position, Il en résulte une erreur.

Il ya un hic cependant minimes. L' gl_Position variable est une vec4 variable, qui correspond à vecteur de 4. L' vec4 est un type de données qui contient quatre valeurs à virgule flottante. Vous vous souvenez probablement que dans un système de coordonnées cartésiennes, nous utilisons trois valeurs (X, Y, Z), et non pas quatre, et il semblerait donc que la puissance requise doit être un vec3, Qui contient trois valeurs à virgule flottante juste le Vertex3D struct nous avons écrit le dernier chapitre. Les trois premières valeurs dans gl_Position représenter les X cartésien, Y, Z et les valeurs pour le courant de vertex. La quatrième valeur est généralement appelée W. Ne vous inquiétez pas trop pourquoi il ya un élément supplémentaire. Il deviendra important quelques chapitres à partir de maintenant quand nous commençons à parler de quelque chose appelé matrice de transformations, Mais pour l'instant, il suffit de penser W comme une valeur de travail (qui n'est pas vraiment ce qu'il représente, cependant) que nous aurons besoin pour faire certains calculs à manipuler notre sommets. Sauf si vous savez w devrait être fixé à une valeur différente, elle a mis à 1,0.

Voici un exemple très simple d'un vertex shader:

void main() {     gl_Position = vec4(0.0, 0.0, 0.0, 1.0); } 

Tous ce shader ne se déplacer chaque vertex à l'origine. La fonction vec4() est intégré dans GLSL, et tout ce qu'il fait est de créer un type de vecteur avec quatre membres. Nous l'utilisons pour créer un sommet à l'origine (0,0,0) avec une valeur aw à 1.0. En attribuant une valeur à gl_Position, Nous sommes en indiquant la position finale de l'actuel sommet. Ce n'est pas, peut-être, un exemple très pratique fragment shader - tout modèle que vous soumettez à ce shader aurez transformé en un point à l'origine - mais il est un simple qui illustre comment vous définissez la valeur finale du sommet, qui est la tâche un tous les vertex shader doit faire chaque fois qu'il s'exécute.

Nous allons faire beaucoup de travail avec les vertex shaders au long du livre, ne vous inquiétez pas si vous n'avez pas bien les comprendre encore. C'est un sujet complexe, mais ils vont commencer à donner un sens une fois que vous les avez utilisés. Pour l'instant, les points importants à retenir à propos de vertex shaders sont les suivants:
  • Vertex shaders exécuté une fois pour chaque vertex qui attire OpenGL ES.
  • Vertex shaders doit définir gl_Position pour indiquer l'emplacement du sommet courant en utilisant les coordonnées cartésiennes (x, y, z), avec une valeur supplémentaire appelé W. Pour le moment, nous serons toujours ensemble W à à 1.

Fragment shaders

La deuxième partie du pipeline programmable OpenGL ES 2.0 programmable est appelé un fragment shader, Et il est appelé parce que, bien ... le fragment shader s'exécute une fois pour chaque fragment dans l'opération de dessin. Ce n'est probablement pas très utile, hein? Alors ... Qu'est-ce qu'un «fragment»?

Pensez à un fragment comme possibles pixel établi. Un fragment comprend toutes les différentes choses dans le monde virtuel qui pourrait potentiellement affecter la couleur finale d'un pixel est. Imaginez que d'une vue OpenGL ES sur votre iPhone ou iPad d'écran est une fenêtre sur un monde virtuel. Maintenant prenez un seul pixel de votre vue OpenGL. Si vous deviez prendre une tranche de votre monde virtuel à partir de ce pixel, et le déménagement dans le monde virtuel aussi loin que l'œil peut voir, tout ce qui se cache derrière ce pixel-ci constitue le fragment pour ce pixel. Parfois, vous verrez fragment shaders appelé pixel shaders. C'est effectivement un peu un abus de langage, mais il est utile pour visualiser un fragment.

Comme les vertex shaders, fragment shaders ont besoin d'une sortie, qui est la couleur finale du pixel qui correspond au fragment courant. Vous indiquez la couleur du pixel en définissant une variable spéciale appelée GLSL gl_FragColor. Voici le fragment shader simple possible; elle fixe simplement la couleur du fragment d'un bleu opaque:

void main() {     gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);   } 

Couleurs, comme nous l'avons vu dans le dernier chapitre, sont représentés par quatre composants dans OpenGL ES (rouge, vert, bleu et alpha) et OpenGL ES attend de ces composants dans cet ordre précis. GLSL n'a pas un type de données spécialement conçue pour les couleurs de portefeuille. Au lieu de cela, il utilise le même type de données qu'il utilise pour les vecteurs et les sommets, donc par la construction d'un vec4 (Un type de vecteur avec quatre membres de virgule flottante) avec ces quatre valeurs, nous créons une couleur dans laquelle le rouge et le vert sont mis à zéro, et le bleu et alpha sont mis à un, ce qui est d'un bleu opaque. En attribuant cette valeur à gl_FragColor, Nous disons à OpenGL ES comment dessiner le pixel correspondant à ce fragment.

On pourrait s'attendre à ce fragment shader pour créer une vue qui est entièrement rempli avec du bleu, mais ce n'est pas forcément ce qu'il fait. Comprendre cela vous aidera à comprendre la différence entre un fragment et un pixel. Chaque trame commence vide, avec l'arrière-plan définie sur une couleur spécifique - souvent noir, mais il peut être réglé à n'importe quelle couleur. Les données de vertex (et autres données) décrivant la scène à en tirer sont soumis à OpenGL ES, et une fonction est appelée pour lancer le pipeline de rendu. Si il n'y a rien dans le monde virtuel qui peut affecter un pixel d'écran particulier, le fragment shader ne fonctionne pas pour ce pixel; on obtient juste à gauche à la couleur de fond. C'est la raison pour laquelle le "pixel shader" terme n'est pas techniquement correct: un pixel dont aucun fragment correspondant ne sont pas traitées par le shader. Un fragment a un et un seul pixel, mais un pixel ne doit pas nécessairement avoir un fragment.


pixel_no_fragment.png


Cette scène contient une seule texture mappée objet. Toute la région qui est dessinée en noir sont pixels avec aucun fragment, car aucun objet dans la scène peut affecter leur couleur finale.


Ce que le fragment shader ci-dessus ne se fixe aucune pixel qui a une partie d'un ou plusieurs objets virtuels «derrière» (façon de parler) au bleu. C'est probablement un peu confus, mais il deviendra clair quand nous écrivons notre première application OpenGL ES dans le chapitre suivant. Pour l'instant, les points à retenir à propos fragment shaders sont les suivantes:
  • Fragment shaders exécuté une fois pour chaque fragment, ce qui signifie une fois pour chaque pixel dans lequel quelque chose peut potentiellement être tirées.
  • Fragment shaders doit définir gl_FragColor pour indiquer la couleur de ce pixel le fragment devrait être attirée.

Envoi de données à l'Shaders

Shaders n'ont pas accès à la mémoire principale de votre application. Toutes les données que d'un shader doit faire son travail doit être spécifiquement envoyés sur le GPU de votre code d'application. Envoi cette surcharge de données encourt et peut être un goulot d'étranglement dans le pipeline de rendu. Afin de maintenir les performances de rendu en place, il est important d'envoyer uniquement les données que votre shaders ont besoin. Il existe deux types de données que vous pouvez envoyer à partir de votre code d'application à votre shaders: attributes and uniforms.
Attributes
An attribute est une donnée pour laquelle vous avez une valeur distincte pour chaque sommet étant soumis. Si, par exemple, vous présentez une scène avec un millier de sommets, tous les attributs que vous passez doit contenir un millier de valeurs. Si vous avez un attribut de couleurs, vous devez passer de mille couleurs. Si vous avez un attribut de vecteurs, vous devez passer dans un millier de vecteurs. Vous aurez presque toujours au moins un attribut, contenant les coordonnées cartésiennes de chaque sommet pour être étirés ou, du moins, la position de départ de chaque sommet avant qu'il ne soit transformé par le processeur de vertex. Sans ces données, il n'ya vraiment aucun moyen de faire quelque chose dans votre vertex shader. Vous ne pouvez soumettre des données en virgule flottante dans un attribut, et non pas des données entières, si vous pouvez fournir plusieurs valeurs à virgule flottante à chaque sommet dans un seul attribut. Une couleur, par exemple, contient quatre nombres à virgule flottante, afin de fournir des données pour les attributs de couleur, vous devez fournir un tableau contenant quatre floats multiplié par le nombre de sommets d'être soumis. Cet attribut sera de même dans le shader vient comme un seul vec4.

Chaque fois que votre vertex shader s'exécute, le pipeline sera lui fournir juste la valeur qui correspond au sommet que le shader est exécuté pour. Donc, dans votre code d'application, les attributs sont représentés par un tableau avec une ou plusieurs valeurs pour chaque sommet, mais dans votre vertex shader, vous devez traiter qu'avec un seul bloc de données de ce tableau présenté, qui contient les valeurs qui correspondent aux l'actuel sommet. Nous allons voir comment envoyer les attributs de votre application au shader un peu plus tard dans le chapitre, mais voici comment vous travaillez avec un attribut insode votre vertex shader:

attribute vec4 position;  void main() {     gl_Position = position; } 

C'est assez simple: vous déclarez l'attribut dans la partie supérieure du shader, et c'est à peu près tout ce que vous avez à faire dans le shader de côté. Le pipeline OpenGL ES prend soin de remettre votre shader l'élément de données à chaque fois. Cela signifie que vous pouvez traiter l'attribut (position, Dans ce cas) comme une variable d'entrée, presque comme un argument à une fonction. Dans cet exemple, nous prenons la valeur de l'attribut position pour ce sommet et en l'assignant comme-est de l'spéciaux gl_Position variable de sortie. Dans ce cas, notre position finale pour chaque sommet est la position de départ qui nous a été fournie par notre code de l'application. Nous allons voir comment envoyer des attributs de notre code de l'application un peu plus tard dans ce chapitre -. Il ya une autre information nous avons besoin d'aller plus avant il sera logique
Uniforms
Uniforms sont le deuxième type de données que vous pouvez passer de votre code d'application à votre shaders. Les uniformes sont disponibles pour les shaders de vertex et fragment de deux - contrairement attributs, qui sont seulement disponibles dans le vertex shader. La valeur d'un uniforme ne peut pas être changé par les shaders, et aura la même valeur à chaque fois un shader fonctionne pour un trajet donné à travers le pipeline. Uniformes peut être à peu près n'importe quel type de données que vous voulez transmettre à utiliser dans votre shader.

Nous allons voir comment passer des uniformes à partir de votre code d'application un peu plus tard, mais dans votre shader, en travaillant avec un uniforme est comme travailler avec un attribut. Vous déclarez dans le haut et ensuite le traiter comme une valeur d'entrée dans votre code, comme ceci:

attribute vec4 position; uniform float translate;  void main() {     gl_Position = position;     gl_Position.y += translate; } 

Dans cet exemple, nous passons une valeur en virgule flottante appelée translate, Puis l'utiliser pour modifier les gl_Position variable de sortie, en déplaçant le sommet selon l'axe Y sur la valeur de l' translate uniforme. NB: Ce n'est pas ainsi que vous le feriez normalement déplacer un objet dans votre shader. Ceci est juste un exemple simple pour illustrer comment les uniformes de travail.

Varyings

Depuis les attributs sont disponibles uniquement dans le fragment shader et la valeur de l'uniforme ne peut pas être changé, comment le fragment shader savoir ce que les valeurs à utiliser lors de l'élaboration d'un pixel donné? Disons, par exemple, que nous avons un attribut contenant par vertex couleurs. Afin d'être en mesure de déterminer la couleur finale du pixel dans notre fragment shader, nous aurions besoin de l'accès à cette pièce particulière de l'information par vertex, ne ferions-nous?

Pourquoi, oui, nous serions. Et c'est là que quelque chose appelé un varying entre en scène. Varyings sont variables spéciales qui peuvent être transmis par le vertex shader pour le fragment shader, mais il est plus cool que ça en al'air. Il n'ya aucune relation entre les sommets et mis fragments. Alors, comment peut-on la valeur du vertex shader être utilisé plus tard dans le fragment shader? Comment le comprendre quelle valeur vertex à utiliser? Qu'est-ce qui se passe avec varyings est que la valeur définie dans le vertex shader est automatiquement interpolés pour une utilisation dans le fragment shader basé sur la distance relative des pixels du fragment de partir des sommets qui l'affectent. Regardons un exemple simple. Disons que nous sommes à tracer une ligne:

fragment.png
Un ensemble variant dans le vertex shader pour V1 et V2 auraient une valeur mi-chemin entre ces deux valeurs lorsque le fragment shader fonctionne pour le fragment F. Si la variable color a été mis au rouge dans le vertex shader pour V1 et au bleu dans le vertex shader pour V2, lorsque le fragment shader pour le fragment correspondant au pixel à F exécute et lit que variées, il contiendra ni bleu ni rouge. Au lieu de cela, il aura une couleur pourpre, à mi chemin entre le rouge et bleu parce que ce fragment est à peu près à mi-chemin entre ces deux sommets. Le pipeline détermine automatiquement quels sommets affectent le dessin d'un fragment de donnée et interpole automatiquement les valeurs fixées pour les varyings dans les vertex shaders basés sur les distances relatives des fragments de ceux sommets.

triangle.png


Varier l'interpolation n'est pas limité à l'interpolation des valeurs de deux sommets, que ce soit. Le gazoduc va comprendre tous les sommets qui influencent le fragment et de calculer la valeur correcte. Voici un exemple simple avec trois sommets ayant chacun une couleur différente.




Les variables sont faciles à utiliser: il vous suffit de les déclarer dans les deux shaders. Puis toute valeur que vous définissez dans le vertex shader sera disponible, sous forme interpolée, dans le fragment shader. Voici un exemple très simple d'un vertex shader qui attribue une valeur à partir d'une couleur par vertex à une variable:

attribute vec4 position; attribute vec4 color;  varying vec4 fragmentColor;  void main() {     gl_Position = position;     fragmentColor = color; } 

Dans cet exemple, color est la couleur de ce sommet qui a été transmis à partir de notre code d'application. Nous avons déclaré un varying called fragmentColor de nous laisser passer une valeur de couleur pour le fragment shader. Nous avons déclaré une vec4 parce que les couleurs sont composées de quatre valeurs composant. En plus d'établir gl_Position basé sur la valeur la position du vertex qui a été adoptée dans le shader en utilisant les position attribut, nous avons également affecter la valeur de la color par vertex attribuer à la variable appelée fragmentColor. Cette valeur sera ensuite disponible dans le fragment shader sous forme interpolée.

Screen shot 2010-11-01 at 8.00.15 PM.png


Dans le shader ci-dessus, si l'on a tracé une ligne et avait un attribut qui définit la couleur au premier point que le rouge et la couleur au second point que le bleu, c'est ce qui se dessiné.


Voici ce qu'un fragment shader simple en utilisant la même que la variation pourrait ressembler:

varying lowp vec4 fragmentColor;  void main() {     gl_FragColor = fragmentColor; } 

La déclaration de la variable dans le fragment shader a le même nom (fragmentColor) Comme il l'a fait dans le vertex shader. Ceci est important, si les noms ne correspondent pas, OpenGL ES ne réalisera pas que c'est la même variable. Il a également pour être le même type de données. Dans ce cas, il est vec4, Tout comme il a été dans le vertex shader. Notez, cependant, qu'il ya un mot clé supplémentaire, lowp. C'est un mot-clé GLSL utilisé pour spécifier le precision ou, en d'autres termes, le nombre d'octets utilisés pour représenter un nombre. Les octets de plus utilisé pour représenter un nombre, le moins de problèmes que vous aurez avec l'arrondi qui se passe nécessairement par des calculs en virgule flottante. Selon le degré de précision dont vous avez besoin, vous pouvez spécifier lowp, mediumpOu highp pour indiquer combien d'octets va stocker la valeur en virgule flottante alors qu'il est utilisé dans les shaders. Le nombre réel d'octets utilisés pour représenter une variable est décidé par OpenGL ES, mais le mot clé de précision vous permet de donner une indication sur la précision combien vous pensez que cela besoins variables dans cette situation.

GLSL permet à l'utilisateur de modificateurs de précision tout moment une variable est déclarée, mais c'est le seul endroit où cela est nécessaire. Si vous n'avez pas l'inclure lors de la déclaration varyings dans votre fragment shader, votre shader échouera à compiler. Dans d'autres endroits, le modificateur de précision est facultative et la spécification GLSL énonce un ensemble de règles qui seront utilisées pour déterminer la précision quand aucun modificateur explicite est fourni.

The lowp mot-clé qui va donner le meilleur rendement, mais le moins de précision lors de l'interpolation. Il est souvent le meilleur choix pour des choses comme les couleurs, où les petites erreurs d'arrondi ne sera pas vraiment d'importance. En cas de doute, commencer par lowp. Vous pouvez toujours augmenter la précision de mediump or highp si le manque de précision entraîne des problèmes dans votre application.

Tous nous le faisons avec la valeur de fragmentColor, Qui est la version de interpolée les valeurs de couleur définie dans le vertex shader, est-il assigner à gl_FragColor de sorte que le pixel est dessiné dans la couleur interpolée. Cela crée un gradient entre les sommets si ces sommets ne sont pas la même couleur.

Avant de regarder comment passer des attributs et des uniformes pour les shaders à partir de notre code d'application, nous devons d'abord parler de la façon shaders obtenir chargés et compilés, car la façon dont nous transmettons les données dans s'appuie sur ce mécanisme. Regardons maintenant, puis nous allons revenir à des attributs et des uniformes de l'autre côté de la canalisation.

OpenGL ES Programmes

Shaders toujours travailler par paires dans OpenGL ES. À un moment donné, il peut y avoir qu'un seul shader de sommet actif et un fragment shader actif, et quand vous dites OpenGL ES de dessiner quelque chose, un shader de vertex et fragment actif doit déjà être en place. Même si une seule paire shader peut être actif à un instant donné, vous pouvez avoir paires shader différent pour dessiner des objets différents. Cela vous permet, par exemple, appliquer un éclairage différent ou des effets différents à des objets dans la même scène.

OpenGL ES est un concept appelé program qui combine un vertex shader et fragment shader avec leurs attributs dans un objet OpenGL ES unique. Vous pouvez créer autant de ces programmes que vous voulez, mais un seul d'entre eux peut être actif à un moment précis. Si vous faites un programme actif, le programme qui a été précédemment actif devient inactif. Généralement, les programmes sont créés et les shaders chargés et compilés lorsque votre application démarre, ou à un autre moment avant de réellement être le dessin, comme lors du chargement d'un niveau dans un match. Vous ne voulez pas attendre jusqu'à ce que vous avez besoin du shader de charger et de le compiler, car cela peut être une opération coûteuse qui causerait un hoquet notable dans le processus de dessin.

Chargement des programmes et de les préparer à utiliser est un peu un processus impliqué. Voici le flux de base:

  1. Créer et compiler les shaders. Les étapes suivantes doivent être effectuées deux fois par --- une fois pour le vertex shader, et encore pour le fragment shader:

    1. Chargez le code source du shader en mémoire.
    2. Call glCreateShader() pour créer un objet Shader vide, sauver la valeur retournée pour faire référence à ce shader dans les futurs appels.
    3. Use glShaderSource() de passer l'objet Shader nouvellement créé son code source.
    4. Call glCompileShader() pour compiler le shader.
    5. Use glGetShaderiv() pour vérifier l'état de compiler et faire en sorte que le shader compilé correctement.
  2. Call glCreateProgram() de créer un programme vide et sauver la valeur retournée de sorte que vous pouvez utiliser le programme dans les futurs appels.
  3. Fixez les deux shaders pour le programme en utilisant glAttachShader().
  4. Supprimer les shaders à l'aide glDeleteShader(). Le programme aura fait sa propre copie des shaders, afin de supprimer entre eux n'a pas empêcher le programme de travail.
  5. Liez chacun des attributs du vertex shader pour le programme en utilisant glBindAttribLocation().
  6. Lier le programme à l'aide glLinkProgram().
  7. Lorsque vous voulez utiliser ce programme pour la première fois, ou si vous voulez changer le programme actif à ce programme, appelez glUseProgram().
  8. Lorsque vous avez terminé avec un programme, de s'en débarrasser à l'aide glDeleteProgram().

Ce qui suit est un exemple d'un code assez typique OpenGL ES chargement du programme pour iOS 4. Ne pas trop se soucier de ce que cela fait, il suffit de numériser sur et et secouer un peu la tête:

GLuint          program;         GLuint          vertShader;          GLuint          fragShader;          GLint           status; const   GLchar          *source;  NSString *vertShaderPathname = [[NSBundle mainBundle] pathForResource:@"shader"                                                                 ofType:@"vsh"]; source = (GLchar *)[[NSString stringWithContentsOfFile:vertShaderPathname                                                encoding:NSUTF8StringEncoding                                                   error:nil] UTF8String]; if (!source) {     // Deal with error }  vertShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(*vertShader, 1, &source, NULL); glCompileShader(*vertShader);  glGetShaderiv(*vertShader, GL_COMPILE_STATUS, &status); if (status == 0) {     glDeleteShader(*vertShader);     // Deal with error }  NSString *fragShaderPathname = [[NSBundle mainBundle] pathForResource:@"shader"                                                                 ofType:@"fsh"]; source = (GLchar *)[[NSString stringWithContentsOfFile:fragShaderPathname                                                encoding:NSUTF8StringEncoding                                                   error:nil] UTF8String]; if (!source) {     // Error checking }  fragShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(*fragShader, 1, &source, NULL); glCompileShader(*fragShader);  glGetShaderiv(*fragShader, GL_COMPILE_STATUS, &status); if (status == 0) {     glDeleteShader(*fragShader);     // Error checking! }  glAttachShader(program, vertShader); glAttachShader(program, fragShader); glBindAttribLocation(program, 1, "position");  glLinkProgram(program);  glGetProgramiv(program, GL_LINK_STATUS, &status); if (status == 0)     return NO;  if (vertShader)     glDeleteShader(vertShader); if (fragShader)     glDeleteShader(fragShader);  glUseProgram(program);
C'est assez moche, n'est-ce pas? Ce n'est pas beaucoup de plaisir à écrire, non plus. Heureusement, nous pouvons simplifier le processus un peu en créant notre propre classe wrapper Objective-C pour représenter les programmes OpenGL ES. Au lieu de marcher à travers le code ci-dessus et en l'examinant, nous allons ensemble cette même fonctionnalité en une forme plus réutilisables et en discuter, à la place. Cela tue deux oiseaux avec une pierre: elle nous permet de parcourir et de comprendre les processus impliqués dans la création de programmes en OpenGL ES, et rend notre vie plus facile sur la route en nous évite d'avoir à écrire du code méchants comme ça à chaque fois que nous avons besoin pour créer un programme.
La rédaction du GLProgram class
Ouvrez Xcode ou un éditeur de texte et de créer deux fichiers texte vides. Appelez l'un d'eux GLProgram.h et l'autre GLProgram.m. Nous allons utiliser cette classe dans chacun des projets que nous créons dans ce livre, alors assurez-vous de sauver les deux fichiers quelque part, vous pouvez les trouver facilement. Ou, si vous préférez, de copier ma version à partir du dossier de code fourni avec le livre.

Mettez le code suivant dans GLProgram.h:

#import <Foundation/Foundation.h> #import <OpenGLES/ES2/gl.h> #import <OpenGLES/ES2/glext.h>  @interface GLProgram : NSObject  {  } - (id)initWithVertexShaderFilename:(NSString *)vShaderFilename  fragmentShaderFilename:(NSString *)fShaderFilename; - (void)addAttribute:(NSString *)attributeName; - (GLuint)attributeIndex:(NSString *)attributeName; - (GLuint)uniformIndex:(NSString *)uniformName; - (BOOL)link; - (void)use; - (NSString *)vertexShaderLog; - (NSString *)fragmentShaderLog; - (NSString *)programLog; @end 

Jetez un oeil au fichier d'en-tête que nous venons de créer, remarquez que nous n'avons pas créé de propriétés, et nous n'avons pas toutes les variables d'instance dans notre tête. Nous n'avons pas exposés ici quelque chose car il ne doit pas être une nécessité pour d'autres classes d'avoir un accès direct à n'importe lequel de nos variables d'instance. Tout le programme a besoin de faire doivent être manipulés en utilisant des méthodes différentes sur notre GLProgram object.


Nouveau Objective-C Caractéristiques

Parce que ce livre se concentre sur iOS 4, je suis sur un grand nombre de nouvelles fonctionnalités en Objective-C 2.0 et 2.1. Une instance est en GLProgram: J'ai utilisé la capacité de nouvelles Objective-C de déclarer des variables d'instance dans une extension de classe. Cela me permet d'avoir des variables d'instance privées qui ne sont pas annoncés à d'autres classes parce qu'ils ne sont pas contenues dans le fichier d'en-tête de ma classe ». Cette fonctionnalité n'est pas disponible sur l'iOS avant le SDK 4.0, cependant, si vous essayez d'utiliser certains des échantillons de code de ce livre dans les anciennes versions du SDK, vous pouvez obtenir problèmes de compilation. Si vous rencontrez ce problème, copiez les déclarations variable d'instance de l'extension de classe dans le fichier d'en-tête de la classe.

La première méthode dans notre classe est notre méthode d'initialisation. Il prend le nom du fichier contenant le code source de vertex shader et le nom du fichier qui contient le code source fragment shaders comme arguments. Cette méthode charge la source et compile les shaders dans le cadre de l'initialisation de l'objet.

Après cela, nous avons la méthode qui sera utilisée pour ajouter des attributs à notre programme, suivie par deux méthodes qui peuvent être utilisés pour récupérer les valeurs d'index pour un attribut donné, ou uniforme. Ces valeurs de l'indice sont utilisés pour fournir des données pour les shaders et peuvent être récupérés à tout moment après le programme est lié. Tous les attributs du programme doit être ajouté au programme avant de lier.

La méthode suivante, nous déclarons, link, Est similaire à la liaison qui se passe après que vous compiler le code source de votre application. Xcode poignées compiler et lier en une seule étape lorsque vous créez votre application, mais avec les shaders, c'est une étape nécessaire distincts qui relie tous les différents composants et les prépare à utiliser. Nous ne pouvions pas bon lien après nous avons compilé les shaders OpenGL, car a besoin de connaître les attributs du programme avant qu'il ne puisse lier correctement.

The use méthode est appelée lorsque nous voulons attirer l'utilisation des shaders de ce programme. Vous pouvez appeler cette méthode à plusieurs reprises, vous permettant de basculer entre les shaders à l'exécution.

Les trois dernières méthodes sont principalement à des fins de débogage. Depuis les shaders sont compilés à l'exécution, et non pas du temps de construction, une erreur de syntaxe ou autre problème dans un shader ne causera pas de construire notre application à l'échec dans Xcode, mais il fera le shader compiler et / ou le lien du programme à l'échec à l'exécution. Si les link retour de la méthode NOCes trois méthodes sont comment nous pouvons trouver ce qui n'allait pas afin que nous puissions y remédier.

Assurez-vous que vous enregistrez GLProgram.h, Puis passer à l'autre fichier texte que vous avez nommé GLProgram.m et mettre le code suivant dans celle-ci (ou vous pouvez simplement copier la mine du dossier du livre de code source):

#import "GLProgram.h" #pragma mark Function Pointer Definitions typedef void (*GLInfoFunction)(GLuint program,      GLenum pname,      GLint* params); typedef void (*GLLogFunction) (GLuint program,      GLsizei bufsize,      GLsizei* length,      GLchar* infolog); #pragma mark - #pragma mark Private Extension Method Declaration @interface GLProgram() {     NSMutableArray  *attributes;     NSMutableArray  *uniforms;     GLuint          program,         vertShader,          fragShader; } - (BOOL)compileShader:(GLuint *)shader      type:(GLenum)type      file:(NSString *)file; - (NSString *)logForOpenGLObject:(GLuint)object      infoCallback:(GLInfoFunction)infoFunc      logFunc:(GLLogFunction)logFunc; @end #pragma mark -  @implementation GLProgram - (id)initWithVertexShaderFilename:(NSString *)vShaderFilename      fragmentShaderFilename:(NSString *)fShaderFilename {     if (self = [super init])     {         attributes = [[NSMutableArray alloc] init];         uniforms = [[NSMutableArray alloc] init];         NSString *vertShaderPathname, *fragShaderPathname;         program = glCreateProgram();          vertShaderPathname = [[NSBundle mainBundle]              pathForResource:vShaderFilename              ofType:@"vsh"];         if (![self compileShader:&vertShader              type:GL_VERTEX_SHADER              file:vertShaderPathname])             NSLog(@"Failed to compile vertex shader");  // Create and compile fragment shader         fragShaderPathname = [[NSBundle mainBundle]              pathForResource:fShaderFilename              ofType:@"fsh"];         if (![self compileShader:&fragShader              type:GL_FRAGMENT_SHADER              file:fragShaderPathname])             NSLog(@"Failed to compile fragment shader");          glAttachShader(program, vertShader);         glAttachShader(program, fragShader);     }      return self; } - (BOOL)compileShader:(GLuint *)shader      type:(GLenum)type      file:(NSString *)file {     GLint status;     const GLchar *source;      source =          (GLchar *)[[NSString stringWithContentsOfFile:file          encoding:NSUTF8StringEncoding          error:nil] UTF8String];     if (!source)     {         NSLog(@"Failed to load vertex shader");         return NO;     }      *shader = glCreateShader(type);     glShaderSource(*shader, 1, &source, NULL);     glCompileShader(*shader);      glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);     return status == GL_TRUE; } #pragma mark - - (void)addAttribute:(NSString *)attributeName {     if (![attributes containsObject:attributeName])     {         [attributes addObject:attributeName];         glBindAttribLocation(program,              [attributes indexOfObject:attributeName],              [attributeName UTF8String]);     } } - (GLuint)attributeIndex:(NSString *)attributeName {     return [attributes indexOfObject:attributeName]; } - (GLuint)uniformIndex:(NSString *)uniformName {     return glGetUniformLocation(program, [uniformName UTF8String]); } #pragma mark - - (BOOL)link {     GLint status;      glLinkProgram(program);     glValidateProgram(program);      glGetProgramiv(program, GL_LINK_STATUS, &status);     if (status == GL_FALSE)         return NO;      if (vertShader)         glDeleteShader(vertShader);     if (fragShader)         glDeleteShader(fragShader);      return YES; } - (void)use {     glUseProgram(program); } #pragma mark - - (NSString *)logForOpenGLObject:(GLuint)object      infoCallback:(GLInfoFunction)infoFunc      logFunc:(GLLogFunction)logFunc {     GLint logLength = 0, charsWritten = 0;      infoFunc(object, GL_INFO_LOG_LENGTH, &logLength);         if (logLength < 1)         return nil;      char *logBytes = malloc(logLength);     logFunc(object, logLength, &charsWritten, logBytes);     NSString *log = [[[NSString alloc] initWithBytes:logBytes          length:logLength          encoding:NSUTF8StringEncoding]          autorelease];     free(logBytes);     return log; } - (NSString *)vertexShaderLog {     return [self logForOpenGLObject:vertShader          infoCallback:(GLInfoFunction)&glGetProgramiv          logFunc:(GLLogFunction)&glGetProgramInfoLog];  } - (NSString *)fragmentShaderLog {     return [self logForOpenGLObject:fragShader          infoCallback:(GLInfoFunction)&glGetProgramiv          logFunc:(GLLogFunction)&glGetProgramInfoLog]; } - (NSString *)programLog {     return [self logForOpenGLObject:program          infoCallback:(GLInfoFunction)&glGetProgramiv          logFunc:(GLLogFunction)&glGetProgramInfoLog]; } #pragma mark - - (void)dealloc {     [attributes release];     [uniforms release];      if (vertShader)         glDeleteShader(vertShader);      if (fragShader)         glDeleteShader(fragShader);      if (program)         glDeleteProgram(program);      [super dealloc]; } @end

Profitons de cette pièce par pièce et assurez-vous que nous sommes clairs sur ce qu'il fait. La première section pourrait sembler un peu déroutant. Nous avons défini deux types de données pour représenter les pointeurs de fonction:

typedef void (*GLInfoFunction)(GLuint program,      GLenum pname,      GLint* params); typedef void (*GLLogFunction) (GLuint program,      GLsizei bufsize,      GLsizei* length,      GLchar* infolog);

Pendant l'écriture du code pour les trois méthodes journal, il est devenu clair que les trois méthodes étaient presque identiques. Les deux journaux shaders étaient exactement les mêmes sauf pour la valeur passée dans les deux fonctions OpenGL ES. Le fichier journal du programme avait une logique presque identique, sauf qu'il a utilisé deux différents appels d'API OpenGL ES pour récupérer les données du journal. Cependant, ces fonctions ont exactement les mêmes arguments dans les deux cas. Cela nous permet d'écrire une méthode générique pour gérer les trois types de journaux en acceptant les pointeurs de fonction comme paramètre, le raccourcissement du code, et de le rendre plus facile à maintenir car la logique --- log n'aurait pas à être répété. Ces définitions de type de rendre le code avec des pointeurs de fonctions faciles à lire.

Ensuite, nous utilisons une extension d'Objective-C pour déclarer les variables d'instance et de deux méthodes privées de deux méthodes. La variable de première instance que nous avons est un tableau modifiable qui sera utilisé pour suivre les attributs du programme. Il n'y a aucune raison de garder une trace de varyings ou des uniformes. Le varyings sont strictement entre les deux shaders, et de déclarer une variable dans les deux shaders est tout ce qu'il faut pour le créer. Nous avons également n'avez pas besoin de garder une trace des uniformes parce OpenGL ES assignera chacun des uniformes une valeur d'index quand il relie le programme. Avec des attributs, cependant, nous devons arriver à un numéro d'index pour chacun d'eux et de dire ce que OpenGL ES numéro d'index que nous utilisons pour l'attribut quand on les lier. OpenGL ES ne pas céder indices attribut pour nous pour les attributs. S'en tenir les attributs dans un tableau et en utilisant les valeurs de l'indice du tableau est le moyen le plus facile à manipuler que la tâche en Objective-C, c'est ce que nous faisons.

Après le tableau, nous avons trois GLuints. Ce sont pour garder la trace des nombres que l'OpenGL ES assigne pour identifier notre programme et de ses deux shaders.

Puis nous avons deux méthodes privées, qui sont des méthodes qui seront utilisées au sein de cette classe, mais que le code en dehors de cette classe ne devrait jamais besoin d'accéder. L'une est une méthode qui compile un shader. Depuis le processus de compilation d'un shader de fragment et un vertex shader est exactement la même, nous créons une méthode pour faire les deux. La deuxième méthode est la méthode log générique mentionné précédemment qui est utilisé par les trois méthodes de log public.

#pragma mark - #pragma mark Private Extension Method Declaration @interface GLProgram() {     NSMutableArray  *attributes;     NSMutableArray  *uniforms;     GLuint          program,         vertShader,          fragShader; } - (BOOL)compileShader:(GLuint *)shader      type:(GLenum)type      file:(NSString *)file; - (NSString *)logForOpenGLObject:(GLuint)object      infoCallback:(GLInfoFunction)infoFunc      logFunc:(GLLogFunction)logFunc; @end

Après cela, nous avons une init méthode. Cette méthode prend le nom des deux shaders (sans l'extension), les charges et tente de compiler tous les deux, puis crée un programme pour les retenir. Une fois qu'il a un programme, il attache les deux shaders pour le programme et retourne l'objet initialisé. Elle crée également le tableau modifiable qui sera utilisé pour stocker les informations d'attributs. Si les shaders ne pas compiler, il renvoie toujours un objet valide. Si nous devions libérer l'objet et le retour nil, Nous n'aurions aucun moyen pour arriver à des données du journal qui me dit ce qui n'allait pas. En retournant un objet valide lorsque la compilation shaders échoue, le link étape va échouer et retourner NO, Qui sera l'indication du code appelant que quelque chose s'est mal passé et les journaux doivent être vérifiées.

- (id)initWithVertexShaderFilename:(NSString *)vShaderFilename      fragmentShaderFilename:(NSString *)fShaderFilename {     if (self = [super init])     {         attributes = [[NSMutableArray alloc] init];         uniforms = [[NSMutableArray alloc] init];         NSString *vertShaderPathname, *fragShaderPathname;         program = glCreateProgram();          vertShaderPathname = [[NSBundle mainBundle]              pathForResource:vShaderFilename              ofType:@"vsh"];         if (![self compileShader:&vertShader              type:GL_VERTEX_SHADER              file:vertShaderPathname])             NSLog(@"Failed to compile vertex shader");          // Create and compile fragment shader         fragShaderPathname = [[NSBundle mainBundle]              pathForResource:fShaderFilename              ofType:@"fsh"];         if (![self compileShader:&fragShader              type:GL_FRAGMENT_SHADER              file:fragShaderPathname])             NSLog(@"Failed to compile fragment shader");          glAttachShader(program, vertShader);         glAttachShader(program, fragShader);     }      return self; }
Pour réellement charger et compiler les shaders, la méthode init appelle la méthode suivante dans le fichier deux fois, pour chaque shader:

- (BOOL)compileShader:(GLuint *)shader      type:(GLenum)type      file:(NSString *)file {     GLint status;     const GLchar *source;      source =          (GLchar *)[[NSString stringWithContentsOfFile:file                                               encoding:NSUTF8StringEncoding                                                  error:nil] UTF8String];     if (!source)     {         NSLog(@"Failed to load vertex shader");         return NO;     }      *shader = glCreateShader(type);     glShaderSource(*shader, 1, &source, NULL);     glCompileShader(*shader);      glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);     return status == GL_TRUE; }
Le fichier contenant la source de shader est chargé de l'application bundle. Si la méthode est incapable de charger le fichier spécifié, il retourne NO. Si elle a pu obtenir source du shader, puis il utilise OpenGL ES fonctions API pour créer un shader, donner le shader nouvellement créé le code source chargé, puis le compiler. Après la compilation, le statut de compilation est vérifiée et une valeur retournée selon si le shader est compilé avec succès.

Une fois qu'une GLProgram Ainsi a été créé et initialisé, la prochaine chose que nous devons faire est de lui dire quels sont les attributs les utilisations vertex shader. La méthode suivante dans le fichier est utilisé à cette fin:

- (void)addAttribute:(NSString *)attributeName {     if (![attributes containsObject:attributeName])     {         [attributes addObject:attributeName];         glBindAttribLocation(program,              [attributes indexOfObject:attributeName],              [attributeName UTF8String]);     } }

Cette méthode vérifie que l'attribut n'a pas déjà été ajouté à la attributes tableau avant de l'ajouter, car les attributs doivent avoir des noms uniques. Il appelle également glBindAttribLocation() de laisser OpenGL ES savoir sur l'attribut. Rappelez-vous: OpenGL ES a besoin de connaître tous les attributs avant de lier le programme, et c'est ainsi que nous le faisons. Les attributs sont identifiés par leurs numéros d'index, et nous indiquer le numéro d'index de mon tableau quand nous appelons glBindAttribLocation(). Cela garantit que chaque attribut a une valeur d'index unique. L'approche traditionnelle pour les attributs est de créer une enum pour chacun des attributs à utiliser, mais notre approche rend le code un peu plus lisible et la fonctionnalité du programme plus autonome.

Uniformes n'ont pas besoin d'être gardé la trace de ou ajouté avant de lier. OpenGL va attribuer à chaque uniforme, un indice sans l'entrée. Lorsque les shaders sont compilés, OpenGL ES fera découvrir les uniformes. Quand nous lier le programme, OpenGL ES assignera chacun des uniformes utilisés dans les shaders une valeur d'index.


Attributs et indices uniforme



Pourquoi les indices d'attributs doivent être spécifiés avant de lier, mais les indices uniformes sont assignés par OpenGL ES sans apport? Je ne sais pas. Autant que j'ai pu découvrir, la raison de la différence n'est pas spécifiquement documenté.

J'ai une théorie, toutefois, que cela peut être dû à la façon dont les attributs et les uniformes sont stockés. Tous les attributs de prendre au moins l'espace d'un vec4. Cela signifie que, si vous avez un attribut qui contient une valeur unique point flottant pour chaque sommet, il va toujours prendre la même quantité d'inscrire l'espace sur le GPU comme un attribut qui contient quatre valeurs à virgule flottante pour chaque vertex.


Uniformes, d'autre part, sont emballées de façon plus efficace dans l'espace registre disponible sur le GPU. Si nous avons quatre float uniformes, par exemple, ils seront rassemblés en un seul registre. Pour ce faire, OpenGL ES peuvent avoir à réorganiser les uniformes de rendre l'utilisation plus efficace de l'espace disponible.

Il semble probable que, puisque les attributs ne sera pas emballé et ne seront donc pas être réorganisées durant la phase de construction; par conséquent, OpenGL ES peut nous laisser choisir nos valeurs d'index pour les attributs. Toutefois, OpenGL ES prend la responsabilité d'assigner les valeurs pour les uniformes de sorte qu'il peut faire le meilleur usage de l'espace de registre à la disposition.

Vous pouvez lire plus sur la façon GLSL fonctionne en lisant les spécifications GLSL pour OpenGL ES (qui est différent de la spécification OpenGL GLSL pour régulières) ici: http://www.khronos.org/files/opengles_shading_language.pdf. En fait, une fois que vous en avez fini avec le livre et ont un peu de réconfort avec la façon générale dont OpenGL ES fonctionne, je recommande fortement la lecture de la spécification. Il peut être un peu sec, mais il est plein d'informations que vous devez savoir si vous faites aucune programmation sérieuse avec OpenGL ES.

Des informations spécifiques sur l'emballage uniforme peut être trouvée dans l'Annexe A, partie 7.

Les deux méthodes suivantes il suffit de retourner le nombre d'index pour un attribut donné, ou uniforme. Pour l'attribut, il sera de retour l'index du tableau parce que c'est ce que nous dit l'OpenGL ES à utiliser lorsque nous avons appelé glBindAttribute(). Pour les uniformes, nous devons nous demander OpenGL ES pour nous donner la valeur de l'indice qu'il affecté au moment de liaison. Notez que ces deux méthodes impliquent faisant cordes compare, si possible, ils devraient être appelés qu'une seule fois. La classe contrôleur qui crée l'instance de GLProgram devrait probablement garder une trace des valeurs renvoyées par attributeIndex: and uniformIndex:. Recherches sont coûteuses cordes assez leur faire quelques centaines de fois par seconde pourrait avoir un impact notable sur le dessin de performance.

- (GLuint)attributeIndex:(NSString *)attributeName {     return [attributes indexOfObject:attributeName]; } - (GLuint)uniformIndex:(NSString *)uniformName {     return glGetUniformLocation(program, [uniformName UTF8String]); }

La prochaine étape est la méthode qui est appelée à relier le programme après les attributs ont été ajoutés. Il fait le lien et valide le programme, récupère alors l'état du lien. Si le lien a échoué, nous avons immédiatement le retour NO. Si l'opération s'est passé avec succès un lien, nous supprimons les deux shaders et puis revenir YES pour indiquer la réussite. La raison pour laquelle nous ne supprimez pas les shaders quand le lien ne l'est notre objet sera encore l'accès aux journaux shader, et nous pouvons débogage si quelque chose va mal. Une fois un shader est supprimé, son journal sont allés aussi.

- (BOOL)link {     GLint status;      glLinkProgram(program);     glValidateProgram(program);      glGetProgramiv(program, GL_LINK_STATUS, &status);     if (status == GL_FALSE)         return NO;      if (vertShader)         glDeleteShader(vertShader);     if (fragShader)         glDeleteShader(fragShader);      return YES; }

La méthode pour faire de ce programme un actif est appelé use, Et il ne fait rien de plus que d'appeler une méthode simple OpenGL ES, en passant program:

- (void)use {     glUseProgram(program); }

Les quatre prochaines méthodes sont les méthodes les journaux. Comme je l'ai mentionné précédemment, nous avons une méthode privée qui gère la fonctionnalité, et que cette méthode est appelée par la méthode des trois publics. La façon dont vous avez des logs d'OpenGL ES est une école de petite vieille. Non, c'est une école ancien lot. C'est pratiquement médiévale. Nous devons d'abord demander à OpenGL ES pour nous dire combien de temps le journal spécifique qui nous intéresse est donc allouer un buffer de considérer que beaucoup de données, puis récupérez le long dans ce tampon. Dans notre cas, nous avons ensuite transformer ces personnages dans une NSString and free() la mémoire tampon avant de retourner le NSString par exemple avec les données du journal:

- (NSString *)logForOpenGLObject:(GLuint)object      infoCallback:(GLInfoFunction)infoFunc      logFunc:(GLLogFunction)logFunc {     GLint logLength = 0, charsWritten = 0;      infoFunc(object, GL_INFO_LOG_LENGTH, &logLength);         if (logLength < 1)         return nil;      char *logBytes = malloc(logLength);     logFunc(object, logLength, &charsWritten, logBytes);     NSString *log = [[[NSString alloc] initWithBytes:logBytes          length:logLength          encoding:NSUTF8StringEncoding]          autorelease];     free(logBytes);     return log; }

Les trois prochaines méthodes sont les méthodes les journaux grand public, et ils sont tous simplement appeler la méthode privée ci-dessus:

- (NSString *)vertexShaderLog {     return [self logForOpenGLObject:vertShader          infoCallback:(GLInfoFunction)&glGetProgramiv          logFunc:(GLLogFunction)&glGetProgramInfoLog];  } - (NSString *)fragmentShaderLog {     return [self logForOpenGLObject:fragShader          infoCallback:(GLInfoFunction)&glGetProgramiv          logFunc:(GLLogFunction)&glGetProgramInfoLog]; } - (NSString *)programLog {     return [self logForOpenGLObject:program          infoCallback:(GLInfoFunction)&glGetProgramiv          logFunc:(GLLogFunction)&glGetProgramInfoLog]; }

Enfin, nous avons nos dealloc méthode, qui libère le tableau modifiable, puis vérifie les deux shaders et le programme, et si aucun d'entre eux sont non-nulle, cela signifie qu'il ya un objet OpenGL ES qui doit être supprimé, afin que nous les supprimer.

- (void)dealloc {     [attributes release];     [uniforms release];      if (vertShader)         glDeleteShader(vertShader);      if (fragShader)         glDeleteShader(fragShader);      if (program)         glDeleteProgram(program);      [super dealloc]; }

Vous avez tout compris? Bon. Maintenant vous pouvez oublier l'essentiel. Vous devez vous souvenir de ce que les programmes, les shaders, les attributs et les uniformes sont et comment ils se rapportent les uns aux autres, mais vous pouvez oublier les moindres détails de la création de programmes. Certains type d'explication ou d'encouragement ici? Pourquoi était-il important de passer par tout cela? A partir de maintenant, il suffit d'utiliser GLProgram pour charger vos shaders. Prenons un coup d'œil maintenant à la façon d'utiliser GLProgram.

Using GLProgram

Utilisation de la GLProgram objet que nous venons de créer est relativement facile. Vous devez d'abord allouer et initialiser une instance en fournissant les noms des deux fichiers contenant le code source du shader ", laissant hors de la .vsh and .fsh extensions.

GLProgram *theProgram = [[GLProgram alloc] initWithVertexShaderFilename:@"Shader" fragmentShaderFilename:@"Shader"]; 

Ensuite, ajouter les attributs utilisés dans votre shader au programme. Si vous aviez deux attributs, l'un avec la position de chaque sommet a appelé position, Et une avec une couleur pour chaque sommet a appelé color, Votre code d'ajouter des attributs ressemblerait à ceci:

[program addAttribute:@"position"]; [program addAttribute:@"color"]; 

Après avoir ajouté les attributs, les liens du programme. Si le programme est incapable de lien, il sera de retour NO et vous devriez vider le logs sur la console de sorte que vous pouvez déboguer le problème. Une fois que vous avez jeté les journaux, c'est une bonne idée de publier le programme, afin de libérer la mémoire qu'ils utilisent, et si vous n'essayez pas d'utiliser un programme non valides:

if (![program link]) { NSLog(@"Link failed"); NSString *progLog = [program programLog]; NSLog(@"Program Log: %@", progLog);  NSString *fragLog = [program fragmentShaderLog]; NSLog(@"Frag Log: %@", fragLog); NSString *vertLog = [program vertexShaderLog]; NSLog(@"Vert Log: %@", vertLog); [program release]; program = nil; } 

Si le processus de liaison a été un succès et les retours YES, Récupérer les uniformes et les indices d'attribut et puis appeler use pour commencer à utiliser les shaders dans ce programme pour dessiner des objets:

GLuint positionAttribute = [program attributeIndex:@"position"]; GLuint colorAttribute = [program attributeIndex:@"color"]; GLuint translateUniform = [program uniformIndex:@"translate"]; [program use]; 

Une fois que vous avez appelé use, Vous êtes prêt à commencer à soumettre des données uniformes et d'attributs.
Envoi des attributs et des uniformes pour les Shaders
Nous sommes presque prêts à essayer le pipeline OpenGL ES programmable par écrit notre première application complète OpenGL ES, mais avant nous faisons cela, nous avons besoin de parler de comment nous expédier les attributs et les uniformes plus au shader. C'est en fait un processus simple, bien qu'il puisse paraître intimidant la première fois qu'on voit dans le code. Le processus pour les attributs et les uniformes sont un peu différentes, donc nous allons les regarder individuellement.
Uniformes Présentation
Regardons premiers uniformes parce qu'ils sont un peu plus facile à Grok. Après nous relions à notre programme, nous récupérons la valeur de l'indice pour chacune de nos uniformes et enregistrer cette valeur d'index afin que nous puissions l'utiliser pour présenter les données de cet uniforme d'OpenGL ES:

GLuint translateUniform = [program uniformIndex:@"translate"]; 

Une fois que nous avons la valeur de l'indice (translateUniform dans la ligne de code précédente), nous avons juste à utiliser une fonction appelée glUniform() de soumettre les données pour que l'uniforme à nos shaders. glUniform() est un de ces «alphabet soup" des fonctions qui prend de nombreuses formes différentes. Parce que les shaders fonctionner sur le GPU, et les offres GPU principalement avec nombres à virgule flottante, l'ensemble des glUniform() variantes prendre une ou plusieurs GLfloats ou d'un ou plusieurs pointeurs GLfloats. ^ 3 Pour envoyer une valeur unique point flottant comme un uniforme, par exemple, nous avions sélectionner soit glUniform1f() or glUniform1fv(), Selon que nous avions besoin d'envoyer un GLfloat ou un pointeur vers une GLfloat. Si nous voulions envoyer un seul sommet, qui serait représenté par un vec3 dans le shader, nous avions le choix entre glUniform3f() or glUniform3fv().

Peu importe lequel des glUniform() variantes que nous choisissons, le premier argument, nous passons doit être l'indice de l'uniforme nous soumettre des données pour, qui est la valeur que nous précédemment récupérées à partir uniformIndex:. Lors de l'utilisation des variantes non-pointeur de la glUniform() (Celles avec un nom se terminant en f), L'indice uniforme est suivie par les données soumises dans le bon ordre. Si nous avons besoin de soumettre un emplacement vertex, par exemple, nous présentions la valeur pour X comme second argument, la valeur de Y que le troisième argument, et la valeur de Z comme le quatrième argument. Alors, pour passer d'un seul pointeur non valeur à l'aide glUniform(), Nous ferions ceci:

glUniform1f(translateUniform, 25.3f); 

Pour passer d'un sommet avec trois valeurs d'un uniforme, nous ferions quelque chose comme ceci:

glUniform3f(vectorUniform, 2.3f, -1.34f, 0.34f); 

Lorsque vous utilisez le glUniform() variantes qui se terminent par v, Nous suivons l'indice de l'uniforme avec la taille des données étant passée puis un pointeur sur les données réelles, comme ceci:

GLfloat vertex[3]; vertex[0] = 2.3f; vertex[1] = -1.34f; vertex[2] = 0.34f; glUniform3fv(vectorUniform, 1, vertex); 

En effet, ce code est exactement identique à l'exemple précédent, mais nous utilisons l'une des variantes du pointeur glUniform() de passer tous les trois valeurs de la matrice qui composent le vec3 uniforme. La chasse aux sorcières ici est la size argument. Vous pourriez penser que vous devez passer 3 dans l'exemple ci-dessus parce que le vertex tableau a une longueur de trois, mais parce que nous utilisons glUniform3fv(), Nous spécifions 1 car cette méthode suppose que chaque élément de données est déjà trois GLfloats longtemps. Si elles étaient plus courtes, vous seriez à l'aide glUniform2fv() or glUniform1fv().
Attributs Présentation
Envoi des données d'attribut pour le shader est seulement légèrement plus impliqués que les uniformes soumettre. D'abord, vous devez récupérer la valeur d'index de l'attribut, comme ceci:

GLuint positionAttribute = [program attributeIndex:@"position"]; 

Tout comme avec les indices uniforme, vous aurez envie de faire cela qu'une seule fois, si possible, et certainement pas à chaque fois que vous tracez, car il invoque une opération de comparaison de chaînes comme elle boucles à travers la attributes tableau, qui est coûteux en calcul. Une fois que vous avez l'index, vous devez soumettre les données à l'aide de la famille OpenGL ES Fonction appelée glVertexAttrib(). Comme glUniform(), glVertexAttrib() est une fonction alphabet soupe avec de nombreuses versions différentes. Cependant, puisque vous êtes presque toujours aller à l'envoi d'un large éventail de données pour le shader lorsque vous travaillez avec des attributs, en pratique, vous devrez presque toujours utiliser la même fonction: glVertexAttribPointer(), Qui vous permet de soumettre un bloc de longueur variable de données à l'attribut dans votre shader. Voici ce qu'est un appel typique à glVertexAttribPointer() pourrait ressembler:

glVertexAttribPointer(positionAttribute, 3, GL_FLOAT, 0, 0, vertices); 

Le premier paramètre (positionAttribute dans la ligne de code précédente) est l'indice qui correspond à l'attribut nous fournir des données pour, celle que nous venons récupérées à partir attributeIndex:. Le second paramètre (3 ci-dessus) raconte OpenGL ES combien d'éléments de données sont présents pour chaque sommet ou, en d'autres termes, comment grand une bonne partie des données soumises doivent obtenir envoyé à chaque exécution du vertex shader. Donc, si vous soumettez des données de position vertex (x, y, z), chaque exécution du vertex shader a besoin de trois éléments de données, et cet argument doit être réglé 3. Si vous envoyez une couleur (r, g, b, a) pour chaque sommet, chaque exécution du vertex aurait besoin de quatre éléments et vous passeriez 4. L'argument suivant indique à OpenGL ES quel type de données est soumis. Dans ce cas, chaque sommet est composé de trois GLfloats, alors passer GL_FLOAT pour lui dire que. En théorie, il existe plusieurs valeurs que vous pourriez passer ici, mais en réalité, puisque les attributs doivent être constitués d'une ou plusieurs variables à virgule flottante, vous aurez toujours passer GL_FLOAT.

Le quatrième argument de glVertexAtttribPointer() peut être ignoré. Il suffit de passer 0 pour cet argument. Cet élément est utilisé uniquement avec GL_FIXED types de données. L' GLfixed datatype vous permet de représenter une valeur en virgule flottante en utilisant des nombres entiers, ce qui permet des calculs plus rapides sur les systèmes qui sont lents à flotter opérations en virgule, soit parce qu'il n'y a aucun GPU, ou parce que le GPU utilise en interne des représentations point fixe de données. Tous les appareils iOS ont un GPU qui utilisent en interne des représentations en virgule flottante et qui sont capables de faire rapidement les mathématiques à virgule flottante, alors nous ne serons jamais utiliser le GLfixed datatype lors de la programmation pour l'iOS, et vous n'avez pas à vous soucier qu'ils soient fixes données de point les valeurs sont normalisées lorsque envoyé au shader.

Nous n'allons pas à regarder le cinquième élément en ce moment. Nous allons juste faire passer 0 pour cet élément, pour le moment. Le cinquième argument est connu comme le stride argument, et il peut être utilisé pour emballer plus d'un type de données dans un seul bloc de données. Nous pourrions, par exemple, emballer nos couleurs et les sommets dans un seul tableau entrelacé et utilisez l'argument foulée de laisser OpenGL ES savoir pour sauter les données de couleur lors du passage des attributs de vertex pour le shader. Nous allons voir comment faire entrelacement de données dans le chapitre sur l'optimisation performance.will Ajouter X-Ref lors chapitre existe

L'argument final glVertexAttribPointer() est le pointeur vers les données réelles nous soumettre pour cet attribut. Cela va être soit un tableau ou un pointeur sur une portion de mémoire créée avec alloc().

Pour chaque attribut, il ya un deuxième appel que nous avons appelé à faire glEnableVertexAttribArray(), En passant l'index de l'attribut que nous sommes favorables. Par défaut, tous les attributs sont désactivés, donc nous devons dire à OpenGL ES spécifiquement pour permettre à un attribut pour qu'il expédier les données de l'attribut dans le shader. Vous pouvez appeler cette fonction qu'une seule fois après que vous liez votre programme et ensuite ne jamais s'en soucier. Cependant, c'est un appel très faible hauteur, et si un attribut été désactivé en quelque sorte d'obtenir, ce serait un problème très difficile à comprendre. En conséquence, il n'est pas rare de faire appel glEnableVertexAttribArray() chaque fois que vous tracez pour s'assurer que les données d'attribut est envoyé au shader.

Une fois vos attributs ont tous été activé et que vous avez soumis vos données d'attribut, vous êtes prêt à raconter OpenGL ES à dessiner.

Tire à sa fin

Le fait de raconter OpenGL ES pour commencer le rendu est lancé par l'un des deux appels de fonction: glDrawArrays() or glDrawElements(). Il ya de nombreuses nuances au dessin, et la meilleure façon d'apprendre ces nuances est d'utiliser les deux fonctions. Dans le prochain chapitre, nous allons construire notre première application, qui va utiliser glDrawArrays(). Quelques chapitres plus tard, nous allons en apprendre davantage sur glDrawElements() et les raisons pour avoir deux fonctions différentes de dessin, en premier lieu.

Dans ce chapitre, nous avons pris un coup d'oeil au pipeline OpenGL ES 2.0 de l 'programmables. Vous avez appris que les vertex shaders exécuté une fois pour chaque vertex et fragment shaders qui exécuter une fois pour chaque fragment, ce qui correspond à une fois pour chaque pixel qui sera tiré. Vous avez également vu ce simple fragment et vertex shaders ressembler, et appris à les compiler et lier un shader paire dans un programme OpenGL ES unique. Vous avez même vu comment passer des données à partir de votre code d'application pour les shaders en utilisant des uniformes et des attributs et comment passer des données entre le vertex shader et le fragment shader utilisant varyings.

Cela a été un long chapitre, mais maintenant nous sommes prêts à mettre le pipeline programmable à utiliser. Prenez une profonde respiration, vous tape dans le dos pour traverser tout cela ennuyeux, mais importante, jusqu'à l'avant-stuff.


1 - En fait, l'iPad et l'iPhone 4 d'Apple utilisent A4 "système sur une puce», qui a un CPU et GPU intégré sur une seule puce au lieu de CPU GPU et les puces séparées, mais qui n'a pas vraiment affecter la façon dont vous programmez pour elle, ne modifie pas la méthode de base que les deux processeurs d'interagir les uns avec les autres.

2 - La puce à tous les appareils iOS est capable de faire vite à virgule flottante en utilisant quelque chose qui s'appelle processeurs vectoriels. Votre code ne signifie pas automatiquement profiter des processeurs vectoriels, cependant, si cette affirmation est vraie, généralement parlant. Nous allons voir comment exploiter les processeurs vectoriels dans le code de l'application qui s'exécute sur le processeur dans le chapitre sur l'optimisation.

3 - Il ya aussi une fonction similaire appelé glUniformMatrix() que nous allons examiner quand nous discuterons des transformations de matrice dans un prochain chapitre. Nous ne voulons pas prendre de l'avance de nous-mêmes, mais les matrices sont vraiment rien de plus que deux dimensions des tableaux de GLfloats.

Aucun commentaire: