OpenGL est une puissante API multiplateforme qui permet un accès très proche au matériel du système dans divers environnements de programmation.
Alors, pourquoi devriez-vous l'utiliser?
Il fournit un traitement de très bas niveau pour les graphiques en 2D et 3D. En général, cela évitera tout clunk que nous avons à cause des langages de programmation interprétés ou de haut niveau. Plus important encore, il fournit également un accès au niveau matériel à une fonctionnalité clé: le GPU.
Le GPU peut considérablement accélérer de nombreuses applications, mais il a un rôle très spécifique dans un ordinateur. Les cœurs GPU sont en fait plus lents que les cœurs CPU. Si nous devions exécuter un programme qui est notamment série sans activité simultanée, alors il sera presque toujours être plus lent sur un cœur GPU qu'un cœur CPU. La principale différence est que le GPU prend en charge le traitement parallèle massif. Nous pouvons créer de petits programmes appelés shaders qui fonctionneront efficacement sur des centaines de cœurs à la fois. Cela signifie que nous pouvons prendre des tâches qui sont par ailleurs incroyablement répétitives et les exécuter simultanément.
Dans cet article, nous allons créer une application Android simple qui utilise OpenGL pour rendre son contenu à l'écran. Avant de commencer, il est important que vous soyez déjà familiarisé avec le connaissance de l'écriture d'applications Android et la syntaxe d'un langage de programmation de type C. L'ensemble du code source de ce didacticiel est disponible sur GitHub .
Pour démontrer la puissance d'OpenGL, nous allons écrire une application relativement basique pour un appareil Android. Désormais, OpenGL sur Android est distribué sous un sous-ensemble appelé OpenGL for Embedded Systems (OpenGL ES). Nous pouvons essentiellement considérer cela comme une version allégée d'OpenGL, bien que les fonctionnalités de base nécessaires soient toujours disponibles.
Au lieu d'écrire un 'Hello World' de base, nous allons écrire une application d'une simplicité trompeuse: un générateur de jeux Mandelbrot. La Ensemble de Mandelbrot est basé dans le domaine de nombres complexes . L'analyse complexe est un domaine magnifiquement vaste, nous allons donc nous concentrer sur le résultat visuel plus que sur les mathématiques réelles.
Avec OpenGL, construire un groupe électrogène Mandelbrot est plus facile que vous ne le pensez! Tweet
Lorsque nous créons l'application, nous voulons nous assurer qu'elle n'est distribuée qu'à ceux qui prennent en charge OpenGL. Commencez par déclarer l'utilisation d'OpenGL 2.0 dans le fichier manifeste, entre la déclaration du manifeste et l'application:
MainActivity
À ce stade, la prise en charge d'OpenGL 2.0 est omniprésente. OpenGL 3.0 et 3.1 gagnent en compatibilité, mais l'écriture pour l'un ou l'autre laissera de côté environ 65% des appareils , ne prenez donc la décision que si vous êtes certain que vous aurez besoin de fonctionnalités supplémentaires. Ils peuvent être implémentés en définissant la version sur «0x000300000» et «0x000300001» respectivement.
Lorsque vous créez cette application OpenGL sur Android, vous disposez généralement de trois classes principales utilisées pour dessiner la surface: votre GLSurfaceView
, une extension de GLSurfaceView.Renderer
, et une implémentation d'un MainActivity
. À partir de là, nous créerons divers modèles qui encapsuleront les dessins.
FractalGenerator
, appelé GLSurfaceView
dans cet exemple, va essentiellement instancier votre public class FractalGenerator extends Activity { private GLSurfaceView mGLView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Create and set GLSurfaceView mGLView = new FractalSurfaceView(this); setContentView(mGLView); } //[...] @Override protected void onPause() { super.onPause(); mGLView.onPause(); } @Override protected void onResume() { super.onResume(); mGLView.onResume(); } }
et acheminez tout changement global sur toute la ligne. Voici un exemple qui sera essentiellement votre code standard:
GLSurfaceView
Ce sera également la classe dans laquelle vous voudrez mettre tous les autres modificateurs de niveau d'activité, (tels que plein écran immersif ).
Une classe plus profonde, nous avons une extension de setEGLContextClientVersion(int version)
, qui agira comme notre vue principale. Dans cette classe, nous définissons la version, configurons un moteur de rendu et contrôlons les événements tactiles. Dans notre constructeur, il suffit de définir la version OpenGL avec public FractalSurfaceView(Context context){ super(context); setEGLContextClientVersion(2); mRenderer = new FractalRenderer(); setRenderer(mRenderer); }
et aussi créer et paramétrer notre moteur de rendu:
setRenderMode(int renderMode)
De plus, nous pouvons définir des attributs comme le mode de rendu avec RENDERMODE_WHEN_DIRTY
. Parce que générer un ensemble Mandelbrot peut être très coûteux, nous allons utiliser requestRender()
, qui ne rendra la scène qu'à l'initialisation et lorsque des appels explicites sont faits à GLSurfaceView
. Vous trouverez plus d'options de paramètres dans le onTouchEvent(MotionEvent event)
FEU .
Une fois que nous aurons le constructeur, nous voudrons probablement remplacer au moins une autre méthode: {a, 0, 0, 0, 0, b, 0, 0, 0, 0, c, 0, v_a, v_b, v_c, 1}
, qui peut être utilisé pour une saisie utilisateur tactile générale. Je ne vais pas entrer trop dans les détails ici, car ce n’est pas l’objectif principal de la leçon.
Enfin, nous passons à notre Renderer, qui sera l'endroit où la plupart des travaux d'éclairage ou peut-être des changements de scène se produiront. Tout d'abord, nous devrons examiner un peu le fonctionnement et le fonctionnement des matrices dans le monde graphique.
OpenGL repose fortement sur l'utilisation de matrices. Les matrices sont une manière merveilleusement compacte de représenter des séquences de changements de coordonnées . Normalement, ils nous permettent de faire des rotations, dilatations / contractions et réflexions arbitraires, mais avec un peu de finesse, nous pouvons aussi faire des traductions. Essentiellement, tout cela signifie que vous pouvez facilement effectuer n'importe quel raisonnable changer que vous voulez, y compris déplacer une caméra ou faire grandir un objet. Par multiplier nos matrices par un vecteur représentant notre coordonnée, nous pouvons produire efficacement le nouveau système de coordonnées.
La Matrice La classe fournie par OpenGL donne un certain nombre de méthodes prêtes à l'emploi pour calculer les matrices dont nous aurons besoin, mais comprendre comment elles fonctionnent est une idée intelligente même lorsque vous travaillez avec des transformations simples.
Tout d'abord, nous pouvons expliquer pourquoi nous utiliserons des vecteurs et des matrices à quatre dimensions pour traiter les coordonnées. Cela revient en fait à l'idée de peaufiner notre utilisation des coordonnées pour pouvoir faire des traductions: alors qu'une traduction dans l'espace 3D est impossible en utilisant seulement trois dimensions, l'ajout d'une quatrième dimension permet la possibilité.
Pour illustrer cela, nous pouvons utiliser une matrice générale d'échelle / traduction très basique:
Il est important de noter que les matrices OpenGL sont par colonne, donc cette matrice serait écrite comme onSurfaceCreated(GL10 gl, EGLConfig config)
, ce qui est perpendiculaire à la façon dont elle sera généralement lue. Cela peut être rationalisé en s'assurant que les vecteurs, qui apparaissent dans la multiplication sous forme de colonne, ont le même format que les matrices.
Forts de cette connaissance des matrices, nous pouvons revenir à la conception de notre moteur de rendu. Habituellement, nous créons une matrice dans cette classe qui est formée à partir du produit de trois matrices: modèle, vue et projection. Cela s'appellerait, à juste titre, un MVPMatrix. Vous pouvez en savoir plus sur les spécificités Ici , comme nous allons utiliser un ensemble plus basique de transformations - l'ensemble de Mandelbrot est un modèle plein écran en 2 dimensions, et il ne nécessite pas vraiment l'idée d'une caméra.
Commençons par configurer le cours. Nous devrons mettre en œuvre le méthodes requises pour l'interface de rendu: onSurfaceChanged(GL10 gl, int width, int height)
, onDrawFrame(GL10 gl)
et public class FractalRenderer implements GLSurfaceView.Renderer { //Provide a tag for logging errors private static final String TAG = 'FractalRenderer'; //Create all models private Fractal mFractal; //Transformation matrices private final float[] mMVPMatrix = new float[16]; //Any other private variables needed for transformations @Override public void onSurfaceCreated(GL10 unused, EGLConfig config) { // Set the background frame color GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); //Instantiate all models mFractal = new Fractal(); } @Override public void onDrawFrame(GL10 unused) { //Clear the frame of any color information or depth information GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); //Create a basic scale/translate matrix float[] mMVPMatrix = new float[]{ -1.0f/mZoom, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f/(mZoom*mRatio), 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, -mX, -mY, 0.0f, 1.0f}; //Pass the draw command down the line to all models, giving access to the transformation matrix mFractal.draw(mMVPMatrix); } @Override public void onSurfaceChanged(GL10 unused, int width, int height) { //Create the viewport as being fullscreen GLES20.glViewport(0, 0, width, height); //Change any projection matrices to reflect changes in screen orientation } //Other public access methods for transformations }
. La classe complète finira par ressembler à quelque chose comme ceci:
checkGLError
Il existe également deux méthodes utilitaires utilisées dans le code fourni, loadShaders
et public Fractal() { // initialize vertex byte buffer for shape coordinates ByteBuffer bb = ByteBuffer.allocateDirect( // (# of coordinate values * 4 bytes per float) squareCoords.length * 4); bb.order(ByteOrder.nativeOrder()); vertexBuffer = bb.asFloatBuffer(); vertexBuffer.put(squareCoords); vertexBuffer.position(0); // initialize byte buffer for the draw list ByteBuffer dlb = ByteBuffer.allocateDirect( // (# of coordinate values * 2 bytes per short) drawOrder.length * 2); dlb.order(ByteOrder.nativeOrder()); drawListBuffer = dlb.asShortBuffer(); drawListBuffer.put(drawOrder); drawListBuffer.position(0); // Prepare shaders int vertexShader = FractalRenderer.loadShader( GLES20.GL_VERTEX_SHADER, vertexShaderCode); int fragmentShader = FractalRenderer.loadShader( GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode); // create empty OpenGL Program mProgram = GLES20.glCreateProgram(); // add the vertex shader to program GLES20.glAttachShader(mProgram, vertexShader); // add the fragment shader to program GLES20.glAttachShader(mProgram, fragmentShader); // create OpenGL program executables GLES20.glLinkProgram(mProgram); }
pour aider au débogage et à l'utilisation des shaders.
Dans tout cela, nous continuons à passer la chaîne de commande le long de la ligne pour encapsuler les différentes parties du programme. Nous sommes finalement arrivés au point où nous pouvons écrire ce que notre programme est réellement Est-ce que , au lieu de savoir comment nous pouvons y apporter des modifications théoriques. Pour ce faire, nous devons créer une classe de modèle contenant les informations qui doivent être affichées pour tout objet donné de la scène. Dans les scènes 3D complexes, cela pourrait être un animal ou une bouilloire, mais nous allons faire une fractale comme un exemple 2D beaucoup plus simple.
Dans les classes de modèle, nous écrivons la classe entière - aucune superclasse ne doit être utilisée. Nous n'avons besoin que d'un constructeur et d'une sorte de méthode de dessin qui accepte tous les paramètres.
Cela dit, il y a encore un certain nombre de variables dont nous aurons besoin qui sont essentiellement passe-partout. Jetons un coup d'œil au constructeur exact utilisé dans la classe Fractal:
static float squareCoords[] = { -1.0f, 1.0f, 0.0f, // top left -1.0f, -1.0f, 0.0f, // bottom left 1.0f, -1.0f, 0.0f, // bottom right 1.0f, 1.0f, 0.0f }; // top right private final short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices
Une bouchée, n’est-ce pas? Heureusement, c'est une partie du programme que vous n'aurez pas à changer du tout, enregistrez le nom du modèle. Si vous modifiez les variables de classe de manière appropriée, cela devrait bien fonctionner pour les formes de base.
Pour en discuter certaines parties, examinons quelques déclarations de variables:
squareCoords
Dans (-1,-1)
, nous spécifions toutes les coordonnées du carré. Notez que toutes les coordonnées à l'écran sont représentées sous forme de grille avec (1,1)
en bas à gauche et drawOrder
en haut à droite.
Dans 0
, nous spécifions l'ordre des coordonnées en fonction des triangles qui constitueraient le carré. En particulier pour la cohérence et la vitesse, OpenGL utilise des triangles pour représenter toutes les surfaces. Pour faire un carré, il suffit de couper une diagonale (dans ce cas, 2
à ByteBuffers
) pour donner deux triangles.
Afin d'ajouter ces deux éléments au programme, vous devez d'abord les convertir en un tampon d'octets bruts pour interfacer directement le contenu du tableau avec l'interface OpenGL. Java stocke les tableaux sous forme d’objets contenant des informations supplémentaires qui ne sont pas directement compatibles avec les tableaux C basés sur des pointeurs utilisés par l’implémentation d’OpenGL. Pour y remédier, loadShader(int type, String shaderCode)
sont utilisés, qui stockent l'accès à la mémoire brute du tableau.
Après avoir mis les données pour les sommets et l'ordre de dessin, nous devons créer nos shaders.
Lors de la création d'un modèle, deux shaders doivent être créés: un Vertex Shader et un Fragment (Pixel) Shader. Tous les shaders sont écrits en GL Shading Language (GLSL), qui est un langage basé sur C avec l'ajout d'un certain nombre de fonctions intégrées , modificateurs de variables , primitives , et entrée / sortie par défaut . Sur Android, celles-ci seront transmises en tant que chaînes finales via const
, l'une des deux méthodes de ressources du moteur de rendu. Passons d'abord en revue les différents types de qualificatifs:
uniform
: Toute variable finale peut être déclarée comme constante afin que sa valeur puisse être stockée pour un accès facile. Les nombres tels que π peuvent être déclarés comme constantes s'ils sont fréquemment utilisés dans le shader. Il est probable que le compilateur déclare automatiquement les valeurs non modifiées en tant que constantes, en fonction de l'implémentation.varying
: Les variables uniformes sont celles qui sont déclarées constantes pour tout rendu unique. Ils sont essentiellement utilisés comme arguments statiques pour vos shaders.attribute
: Si une variable est déclarée comme variable et est définie dans un vertex shader, alors elle est interpolée linéairement dans le fragment shader. Ceci est utile pour créer toute sorte de dégradé de couleur et est implicite pour les changements de profondeur.vec2
: Les attributs peuvent être considérés comme des arguments non statiques d'un shader. Ils désignent l'ensemble des entrées qui sont spécifiques aux vertex et n'apparaîtront que dans Vertex Shaders.De plus, nous devrions discuter de deux autres types de primitives qui ont été ajoutés:
vec3
, vec4
, mat2
: Vecteurs à virgule flottante de dimension donnée.mat3
, mat4
, x
: Matrices à virgule flottante de dimension donnée.Les vecteurs sont accessibles par leurs composants y
, z
, w
et r
ou g
, b
, a
, et vec3 a
. Ils peuvent également générer n'importe quel vecteur de taille avec plusieurs indices: for a.xxyz
, vec4
renvoie a a
avec les valeurs correspondantes de mat2 matrix
.
Les matrices et les vecteurs peuvent également être indexés sous forme de tableaux, et les matrices renverront un vecteur avec un seul composant. Cela signifie que pour matrix[0].a
, matrix[0][0]
est valide et renverra vec2 a = vec2(1.0,1.0); vec2 b = a; b.x=2.0;
. Lorsque vous travaillez avec ces derniers, rappelez-vous qu'ils agissent comme des primitifs et non comme des objets. Par exemple, considérez le code suivant:
a=vec2(1.0,1.0)
Cela laisse b=vec2(2.0,1.0)
et b
, ce qui n'est pas ce à quoi on pourrait s'attendre du comportement des objets, où la deuxième ligne donnerait a
un pointeur vers private final String vertexShaderCode = 'attribute vec4 vPosition;' + 'void main() {' + ' gl_Position = vPosition;' + '}';
.
Dans l'ensemble de Mandelbrot, la majorité du code sera dans le shader de fragment, qui est le shader qui s'exécute sur chaque pixel. En principe, les nuanceurs de vertex fonctionnent sur chaque sommet, y compris les attributs qui seront sur une base par sommet, comme les changements de couleur ou de profondeur. Jetons un coup d'œil au shader de vertex extrêmement simple pour une fractale:
gl_Position
En cela, gl_Position
est une variable de sortie définie par OpenGL pour enregistrer les coordonnées d'un sommet. Dans ce cas, on passe dans une position pour chaque sommet sur lequel on pose vPosition
. Dans la plupart des applications, nous multiplierions MVPMatrix
par un fragmentShaderCode
, transformant nos sommets, mais nous voulons que la fractale soit toujours en plein écran. Toutes les transformations seront effectuées avec un système de coordonnées local.
Le Fragment Shader sera l'endroit où la plupart du travail sera effectué pour générer l'ensemble. Nous allons définir precision highp float; uniform mat4 uMVPMatrix; void main() { //Scale point by input transformation matrix vec2 p = (uMVPMatrix * vec4(gl_PointCoord,0,1)).xy; vec2 c = p; //Set default color to HSV value for black vec3 color=vec3(0.0,0.0,0.0); //Max number of iterations will arbitrarily be defined as 100. Finer detail with more computation will be found for larger values. for(int i=0;i4.0){ //The point, c, is not part of the set, so smoothly color it. colorRegulator increases linearly by 1 for every extra step it takes to break free. float colorRegulator = float(i-1)-log(((log(dot(p,p)))/log(2.0)))/log(2.0); //This is a coloring algorithm I found to be appealing. Written in HSV, many functions will work. color = vec3(0.95 + .012*colorRegulator , 1.0, .2+.4*(1.0+sin(.3*colorRegulator))); break; } } //Change color from HSV to RGB. Algorithm from https://gist.github.com/patriciogonzalezvivo/114c1653de9e3da6e1e3 vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); vec3 m = abs(fract(color.xxx + K.xyz) * 6.0 - K.www); gl_FragColor.rgb = color.z * mix(K.xxx, clamp(m - K.xxx, 0.0, 1.0), color.y); gl_FragColor.a=1.0; }
à ce qui suit:
fract
Une grande partie du code est simplement le calcul et l'algorithme du fonctionnement de l'ensemble. Notez l'utilisation de plusieurs fonctions intégrées: abs
, mix
, sin
, clamp
, et dot
, qui fonctionnent toutes sur des vecteurs ou des scalaires et renvoient des vecteurs ou scalaires. De plus, draw
est utilisé qui prend des arguments vectoriels et renvoie un scalaire.
Maintenant que nos shaders sont configurés pour être utilisés, nous avons une dernière étape, qui consiste à implémenter le public void draw(float[] mvpMatrix) { // Add program to OpenGL environment GLES20.glUseProgram(mProgram); // get handle to vertex shader's vPosition member mPositionHandle = GLES20.glGetAttribLocation(mProgram, 'vPosition'); mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, 'uMVPMatrix'); //Pass uniform transformation matrix to shader GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0); //Add attribute array of vertices GLES20.glEnableVertexAttribArray(mPositionHandle); GLES20.glVertexAttribPointer( mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, vertexBuffer); // Draw the square GLES20.glDrawElements( GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer); // Disable vertex array GLES20.glDisableVertexAttribArray(mPositionHandle); FractalRenderer.checkGlError('Test'); }
fonction dans notre modèle:
uniform
La fonction transmet tous les arguments aux shaders, y compris attribute
matrice de transformation et le double
position.
Après avoir assemblé toutes les parties du programme, nous pouvons enfin l'exécuter. À condition qu'une prise en charge tactile appropriée soit gérée, des scènes absolument fascinantes seront peintes:
Si nous zoomons un peu plus, nous commençons à remarquer une panne dans l'image:
Cela n'a absolument rien à voir avec les calculs de l'ensemble derrière lui et tout à voir avec la façon dont les nombres sont stockés et traités dans OpenGL. Bien que le support plus récent pour float
précision a été faite, OpenGL 2.0 ne supporte nativement rien de plus que precision highp float
s. Nous les avons spécifiquement désignés comme les flotteurs les plus précis disponibles avec double
dans notre shader, mais même cela n’est pas suffisant.
Afin de contourner ce problème, le seul moyen serait de imiter float
s en utilisant deux GLSurfaceView
s. Cette méthode se situe en fait dans un ordre de grandeur de la précision réelle d'une méthode implémentée nativement, bien que le coût de la vitesse soit assez élevé. Ceci sera laissé comme exercice au lecteur, si l'on souhaite avoir un niveau de précision plus élevé.
Avec quelques classes de support, OpenGL peut rapidement soutenir le rendu en temps réel de scènes complexes. La création d'une mise en page composée d'un Renderer
, la définition de son