Comment utiliser OpenGL ES dans les applications Android

Aujourd'hui, presque tous les téléphones Android disponibles sur le marché disposent d'une unité de traitement graphique, ou GPU. Comme son nom l'indique, il s'agit d'une unité matérielle dédiée à la gestion des calculs généralement liés aux graphiques 3D. En tant que développeur d'applications, vous pouvez utiliser le GPU pour créer des graphiques et des animations complexes fonctionnant à des cadences très élevées..

Il existe actuellement deux API différentes que vous pouvez utiliser pour interagir avec le GPU d'un périphérique Android: Vulkan et OpenGL ES. Tandis que Vulkan est disponible uniquement sur les appareils fonctionnant sous Android 7.0 ou version ultérieure, OpenGL ES est pris en charge par toutes les versions d'Android..

Dans ce didacticiel, je vais vous aider à utiliser OpenGL ES 2.0 dans des applications Android..

Conditions préalables

Pour pouvoir suivre ce tutoriel, vous aurez besoin de:

  • la dernière version d'Android Studio
  • un périphérique Android prenant en charge OpenGL ES 2.0 ou une version ultérieure
  • une version récente de Blender, ou de tout autre logiciel de modélisation 3D

1. Qu'est-ce qu'OpenGL ES??

OpenGL, abréviation de Open Graphics Library, est une API indépendante de la plate-forme qui vous permet de créer des graphiques 3D à accélération matérielle. OpenGL ES, abréviation d'OpenGL pour systèmes intégrés, est un sous-ensemble de l'API..

OpenGL ES est une API de très bas niveau. En d'autres termes, il ne propose aucune méthode permettant de créer ou de manipuler rapidement des objets 3D. Tout en travaillant avec ce dernier, vous devez gérer manuellement des tâches telles que la création des sommets et faces individuels d'objets 3D, le calcul de diverses transformations 3D et la création de différents types de shaders.

Il est également intéressant de noter que le SDK Android et le NDK permettent d'écrire du code lié à OpenGL ES en Java et en C.

2. Configuration du projet

Les API OpenGL ES faisant partie du cadre Android, vous n'avez pas besoin d'ajouter de dépendances à votre projet pour pouvoir les utiliser. Dans ce didacticiel, nous utiliserons toutefois la bibliothèque Apache Commons IO pour lire le contenu de quelques fichiers texte. Par conséquent, ajoutez-le en tant que compiler la dépendance dans le module de votre application build.gradle fichier:

compiler 'commons-io: commons-io: 2.5'

En outre, pour empêcher les utilisateurs de Google Play ne disposant pas de périphériques prenant en charge la version d'OpenGL ES dont vous avez besoin d'installer votre application, ajoutez les éléments suivants: marquez le fichier manifeste de votre projet:

3. Créer une toile

Le framework Android propose deux widgets pouvant servir de canevas pour vos graphiques 3D: GLSurfaceView et TextureView. La plupart des développeurs préfèrent utiliser GLSurfaceView, et choisir TextureView seulement lorsqu'ils ont l'intention de superposer leurs graphiques 3D sur un autre Vue widget. Pour l'application que nous allons créer dans ce tutoriel, GLSurfaceView suffira.

Ajout d'un GLSurfaceView le widget dans votre fichier de mise en page n'est pas différent de l'ajout d'un autre widget.

Notez que nous avons rendu la largeur de notre widget égale à sa hauteur. Cela est important car le système de coordonnées OpenGL ES est un carré. Si vous devez utiliser un canevas rectangulaire, n'oubliez pas d'inclure son rapport de format lors du calcul de votre matrice de projection. Vous apprendrez ce qu'est une matrice de projection dans une étape ultérieure.

Initialiser un GLSurfaceView widget à l'intérieur d'un Activité la classe est aussi simple que d'appeler le findViewById () méthode et en lui passant son identifiant.

mySurfaceView = (GLSurfaceView) findViewById (R.id.my_surface_view);

De plus, nous devons appeler le setEGLContextClientVersion () méthode pour spécifier explicitement la version d'OpenGL ES que nous allons utiliser pour dessiner à l'intérieur du widget.

mySurfaceView.setEGLContextClientVersion (2);

4. Créer un objet 3D

Bien qu'il soit possible de créer des objets 3D en Java en codant à la main les coordonnées X, Y et Z de tous leurs sommets, cela est très fastidieux. L'utilisation d'outils de modélisation 3D est beaucoup plus simple. Blender est l'un de ces outils. Il est open source, puissant et très facile à apprendre.

Lancez Blender et appuyez sur X supprimer le cube par défaut. Ensuite, appuyez sur Maj-A et sélectionnez Mesh> Torus. Nous avons maintenant un objet 3D assez complexe composé de 576 sommets.

Pour pouvoir utiliser le tore dans notre application Android, nous devons l'exporter sous forme de fichier OBJ Wavefront. Par conséquent, allez à Fichier> Exporter> Wavefront (.obj). Dans l’écran suivant, donnez un nom au fichier OBJ, assurez-vous que le Visages triangulaires et Garder l'ordre du sommet les options sont sélectionnées et appuyez sur le bouton Export OBJ bouton.

Vous pouvez maintenant fermer Blender et déplacer le fichier OBJ vers celui de votre projet Android Studio. les atouts dossier.

5. Analyser le fichier OBJ

Si vous ne l'avez pas déjà remarqué, le fichier OBJ que nous avons créé à l'étape précédente est un fichier texte, qui peut être ouvert à l'aide de n'importe quel éditeur de texte..

Dans le fichier, chaque ligne commençant par un "v" représente un seul sommet. De même, chaque ligne commençant par un "f" représente une seule face triangulaire. Bien que chaque ligne de sommet contienne les coordonnées X, Y et Z d'un sommet, chaque ligne de face contient les indices de trois sommets, qui forment ensemble une face. C'est tout ce que vous devez savoir pour analyser un fichier OBJ.

Avant de commencer, créez une nouvelle classe Java appelée Torus et ajouter deux liste objets, l'un pour les sommets et l'autre pour les faces, en tant que variables membres.

public class Torus liste privée liste des sommets; liste privée facesList; public Torus (contexte de contexte) verticesList = new ArrayList <> (); facesList = new ArrayList <> (); // Plus de code va ici

Le moyen le plus simple de lire toutes les lignes individuelles du fichier OBJ est d’utiliser le Scanner classe et ses nextLine () méthode. En parcourant les lignes et en remplissant les deux listes, vous pouvez utiliser le Chaîne la classe commence avec() méthode pour vérifier si la ligne en cours commence par un "v" ou un "f".

// Ouvrir le fichier OBJ avec un scanner Scanner scanner = new Scanner (context.getAssets (). Open ("torus.obj")); // Parcourt toutes ses lignes while (scanner.hasNextLine ()) String line = scanner.nextLine (); if (line.startsWith ("v")) // Ajouter une ligne de sommet à la liste des sommets verticesList.add (ligne);  else if (line.startsWith ("f")) // Ajouter une ligne de visage à la liste de visages facesList.add (ligne);  // Fermez le scanner scanner.close (); 

6. Créer des objets tampons

Vous ne pouvez pas transmettre directement les listes de sommets et de faces aux méthodes disponibles dans l'API OpenGL ES. Vous devez d'abord les convertir en objets tampons. Pour stocker les données de coordonnées de sommet, nous aurons besoin d'un FloatBuffer objet. Pour les données de visage, qui consistent simplement en indices de sommet, un ShortBuffer objet suffira.

En conséquence, ajoutez les variables de membre suivantes au Torus classe:

sommets privés FloatBufferBuffer; ShortBuffer privé facesBuffer;

Pour initialiser les tampons, il faut d’abord créer un ByteBuffer objet en utilisant le allocateDirect () méthode. Pour le tampon de sommets, attribuez quatre octets à chaque coordonnée, les coordonnées étant des nombres à virgule flottante. Une fois la ByteBuffer objet a été créé, vous pouvez le convertir en un FloatBuffer en appelant son asFloatBuffer () méthode.

// Créer un tampon pour les sommets ByteBuffer buffer1 = ByteBuffer.allocateDirect (verticesList.size () * 3 * 4); buffer1.order (ByteOrder.nativeOrder ()); verticesBuffer = buffer1.asFloatBuffer ();

De même, créez un autre ByteBuffer objet pour le tampon de faces. Cette fois, allouez deux octets pour chaque index de sommet, car les index sont court non signé littéraux. Assurez-vous également que vous utilisez le asShortBuffer () méthode pour convertir le ByteBuffer objecter à un ShortBuffer.

// Créer un tampon pour les faces ByteBuffer buffer2 = ByteBuffer.allocateDirect (facesList.size () * 3 * 2); tampon2.order (ByteOrder.nativeOrder ()); facesBuffer = buffer2.asShortBuffer ();

Remplir le tampon des sommets implique de parcourir en boucle le contenu de liste des sommets, extraire les coordonnées X, Y et Z de chaque élément et appeler le mettre() méthode pour mettre des données dans le tampon. Parce que liste des sommets ne contient que des chaînes, nous devons utiliser le parseFloat () convertir les coordonnées des chaînes en flotte valeurs.

for (String vertex: sommetsList) String coords [] = vertex.split (""); // Séparation par espace float x = Float.parseFloat (coords [1]); float y = Float.parseFloat (coords [2]); float z = Float.parseFloat (coords [3]); verticesBuffer.put (x); verticesBuffer.put (y); verticesBuffer.put (z);  verticesBuffer.position (0);

Notez que dans le code ci-dessus, nous avons utilisé le position() méthode pour réinitialiser la position du tampon.

Le remplissage du tampon de faces est légèrement différent. Vous devez utiliser le parseShort () méthode pour convertir chaque index de vertex en valeur courte. De plus, comme les indices commencent par un au lieu de zéro, vous devez vous en soustraire un avant de les placer dans la mémoire tampon..

for (String face: facesList) String vertexIndices [] = face.split (""); vertex court1 = Short.parseShort (vertexIndices [1]); vertex court2 = Short.parseShort (vertexIndices [2]); vertex court3 = Short.parseShort (vertexIndices [3]); facesBuffer.put ((court) (vertex1 - 1)); facesBuffer.put ((court) (vertex2 - 1)); facesBuffer.put ((court) (vertex3 - 1));  facesBuffer.position (0);

7. Créer des shaders

Pour pouvoir restituer notre objet 3D, nous devons lui créer un vertex shader et un fragment shader. Pour le moment, vous pouvez considérer un shader comme un programme très simple écrit dans un langage semblable à C appelé OpenGL Shading Language, ou GLSL en abrégé..

Comme vous l'avez peut-être deviné, un vertex shader est responsable de la gestion des sommets d'un objet 3D. Un fragment shader, également appelé pixel shader, est chargé de colorer les pixels de l'objet 3D..

Étape 1: Créer un Vertex Shader

Créez un nouveau fichier appelé vertex_shader.txt dans votre projet res / raw dossier.

Un vertex shader doit avoir un attribut variable globale à l’intérieur de celle-ci afin de recevoir les données de position de sommet de votre code Java. De plus, ajoutez un uniforme variable globale pour recevoir une matrice de projection de vue à partir du code Java.

À l'intérieur de principale() fonction du vertex shader, vous devez définir la valeur de gl_position, une variable intégrée GLSL qui détermine la position finale du sommet. Pour l'instant, vous pouvez simplement définir sa valeur sur le produit de la uniforme et attribut variables globales.

En conséquence, ajoutez le code suivant au fichier:

attribue la position vec4; matrice mat4 uniforme; void main () gl_Position = matrix * position; 

Étape 2: Créer un shader de fragment

Créez un nouveau fichier appelé fragment_shader.txt dans votre projet res / raw dossier.

Pour que ce didacticiel soit court, nous allons maintenant créer un fragment shader très minimaliste qui assigne simplement la couleur orange à tous les pixels. Pour attribuer une couleur à un pixel, dans le principale() fonction d'un fragment shader, vous pouvez utiliser le gl_FragColor variable intégrée.

float mediump de précision; void main () gl_FragColor = vec4 (1, 0,5, 0, 1,0); 

Dans le code ci-dessus, la première ligne spécifiant la précision des nombres à virgule flottante est importante car un fragment shader n'a pas de précision par défaut pour ceux-ci..

Étape 3: compiler les shaders

Retour dans le Torus classe, vous devez maintenant ajouter du code pour compiler les deux shaders que vous avez créés. Avant de le faire, toutefois, vous devez les convertir des ressources brutes en chaînes. le IOUtils classe, qui fait partie de la bibliothèque Apache Commons IO, a une toString () méthode pour faire exactement cela. Le code suivant vous montre comment l'utiliser:

// Conversion de vertex_shader.txt en chaîne InputStream vertexShaderStream = context.getResources (). OpenRawResource (R.raw.vertex_shader); String vertexShaderCode = IOUtils.toString (vertexShaderStream, Charset.defaultCharset ()); vertexShaderStream.close (); // Convertit fragment_shader.txt en chaîne InputStream fragmentShaderStream = context.getResources (). OpenRawResource (R.raw.fragment_shader); String fragmentShaderCode = IOUtils.toString (fragmentShaderStream, Charset.defaultCharset ()); fragmentShaderStream.close ();

Le code des shaders doit être ajouté aux objets shader OpenGL ES. Pour créer un nouvel objet shader, utilisez la commande glCreateShader () méthode du GLES20 classe. Selon le type d’objet de shader que vous voulez créer, vous pouvez soit transmettre GL_VERTEX_SHADER ou GL_FRAGMENT_SHADER à cela. La méthode retourne un entier servant de référence à l'objet shader. Un objet shader nouvellement créé ne contient aucun code. Pour ajouter le code de shader à l’objet shader, vous devez utiliser le glShaderSource () méthode.

Le code suivant crée des objets shader pour le vertex shader et le fragment shader:

int vertexShader = GLES20.glCreateShader (GLES20.GL_VERTEX_SHADER); GLES20.glShaderSource (vertexShader, vertexShaderCode); int fragmentShader = GLES20.glCreateShader (GLES20.GL_FRAGMENT_SHADER); GLES20.glShaderSource (fragmentShader, fragmentShaderCode);

Nous pouvons maintenant passer les objets shader au glCompileShader () méthode pour compiler le code qu'ils contiennent.

GLES20.glCompileShader (vertexShader); GLES20.glCompileShader (fragmentShader);

8. Créer un programme

Lors du rendu d'un objet 3D, vous n'utilisez pas directement les shaders. Au lieu de cela, vous les attachez à un programme et utilisez le programme. Par conséquent, ajoutez une variable membre à la Torus classe pour stocker une référence à un programme OpenGL ES.

programme int privé;

Pour créer un nouveau programme, utilisez le glCreateProgram () méthode. Pour y attacher les objets vertex et fragment shader, utilisez la commande glAttachShader () méthode.

programme = GLES20.glCreateProgram (); GLES20.glAttachShader (programme, vertexShader); GLES20.glAttachShader (programme, fragmentShader);

À ce stade, vous pouvez lier le programme et commencer à l'utiliser. Pour ce faire, utilisez le glLinkProgram () et glUseProgram () les méthodes.

GLES20.glLinkProgram (programme); GLES20.glUseProgram (programme);

9. Dessinez l'objet 3D

Avec les shaders et les tampons prêts, nous avons tout ce dont nous avons besoin pour dessiner notre tore. Ajouter une nouvelle méthode à la Torus classe appelée dessiner:

public void draw () // le code de dessin va ici

Dans une étape précédente, dans le vertex shader, nous avons défini un position variable pour recevoir les données de position de sommet du code Java. Il est maintenant temps d’envoyer les données de position de sommet. Pour ce faire, nous devons d’abord maîtriser le position variable dans notre code Java en utilisant le glGetAttribLocation () méthode. De plus, le descripteur doit être activé à l'aide du glEnableVertexAttribArray () méthode.

En conséquence, ajoutez le code suivant dans le répertoire dessiner() méthode:

int position = GLES20.glGetAttribLocation (programme, "position"); GLES20.glEnableVertexAttribArray (position);

Pointer le position gérer notre tampon de sommets, nous devons utiliser le glVertexAttribPointer () méthode. En plus du tampon de sommets lui-même, la méthode attend le nombre de coordonnées par sommet, le type des coordonnées et le décalage d'octet pour chaque sommet. Parce que nous avons trois coordonnées par sommet et que chaque coordonnée est un flotte, le décalage d'octet doit être 3 * 4.

GLES20.glVertexAttribPointer (position, 3, GLES20.GL_FLOAT, false, 3 * 4, verticesBuffer);

Notre vertex shader attend également une matrice de projection. Bien qu'une telle matrice ne soit pas toujours nécessaire, son utilisation vous permet de mieux contrôler le rendu de votre objet 3D..

Une matrice de projection est simplement le produit des matrices de vue et de projection. Une matrice de vues vous permet de spécifier les emplacements de votre caméra et le point sur lequel elle se penche. D'autre part, une matrice de projection vous permet non seulement de mapper le système de coordonnées carrées d'OpenGL ES sur l'écran rectangulaire d'un périphérique Android, mais également de spécifier les plans proche et éloigné du tronc de visualisation..

Pour créer les matrices, vous pouvez simplement créer trois flotte tableaux de taille 16:

float [] projectionMatrix = new float [16]; float [] viewMatrix = new float [16]; float [] productMatrix = new float [16];

Pour initialiser la matrice de projection, vous pouvez utiliser le frustumM () méthode du Matrice classe. Il attend les emplacements des plans de clips gauche, droit, bas, haut, proche et éloigné. Parce que notre toile est déjà un carré, vous pouvez utiliser les valeurs -1 et 1 pour la gauche et la droite, et les plans de clip en bas et en haut. Pour les plans de clips proches et lointains, n'hésitez pas à expérimenter différentes valeurs.

Matrix.frustumM (projectionMatrix, 0, -1, 1, -1, 1, 2, 9);

Pour initialiser la matrice de vue, utilisez le setLookAtM () méthode. Il attend les positions de la caméra et le point sur lequel il se penche. Vous êtes à nouveau libre d'expérimenter différentes valeurs.

Matrix.setLookAtM (viewMatrix, 0, 0, 3, -4, 0, 0, 0, 0, 1, 0);

Enfin, pour calculer la matrice de produits, utilisez le multiplyMM () méthode.

Matrix.multiplyMM (productMatrix, 0, projectionMatrix, 0, viewMatrix, 0);

Pour transmettre la matrice de produit au vertex shader, vous devez obtenir une poignée à sa matrice variable en utilisant le glGetUniformLocation () méthode. Une fois que vous avez la poignée, vous pouvez la diriger vers la matrice de produit à l'aide du bouton glUniformMatrix () méthode.

int matrix = GLES20.glGetUniformLocation (programme, "matrice"); GLES20.glUniformMatrix4fv (matrix, 1, false, productMatrix, 0);

Vous avez sûrement remarqué que nous n’avons toujours pas utilisé le tampon des faces. Cela signifie que nous n'avons toujours pas expliqué à OpenGL ES comment connecter les sommets pour former des triangles, qui serviront de faces à notre objet 3D..

le glDrawElements () Cette méthode vous permet d’utiliser le tampon des faces pour créer des triangles. Comme arguments, il attend le nombre total d’index de sommet, le type de chaque index et le tampon de faces..

GLES20.glDrawElements (GLES20.GL_TRIANGLES, facesList.size () * 3, GLES20.GL_UNSIGNED_SHORT, facesBuffer);

Enfin, n'oubliez pas de désactiver le attribut gestionnaire que vous avez activé précédemment pour transmettre les données de sommet au sommet de vertex.

GLES20.glDisableVertexAttribArray (position);

10. Créer un moteur de rendu

Notre GLSurfaceView widget a besoin d'un GLSurfaceView.Renderer objet pour pouvoir rendre des graphiques 3D. Vous pouvez utiliser le setRenderer () y associer un rendu.

mySurfaceView.setRenderer (nouveau GLSurfaceView.Renderer () // Plus de code va ici);

À l'intérieur de onSurfaceCreated () méthode de rendu, vous devez spécifier la fréquence à laquelle le graphique 3D doit être rendu. Pour l'instant, n'effectuons le rendu que lorsque le graphique 3D est modifié. Pour ce faire, passez le RENDERMODE_WHEN_DIRTY constante à la setRenderMode () méthode. De plus, initialisez une nouvelle instance du Torus objet.

@Override public null onSurfaceCreated (GL10 gl10, EGLConfig eglConfig) mySurfaceView.setRenderMode (GLSurfaceView.RENDERMODE_WHEN_DIRTY); tore = new Torus (getApplicationContext ()); 

À l'intérieur de onSurfaceChanged () méthode de rendu, vous pouvez définir la largeur et la hauteur de votre fenêtre en utilisant le glViewport () méthode.

@Override public void onSurfaceChanged (GL10 gl10, largeur int, hauteur int) GLES20.glViewport (0,0, largeur, hauteur); 

À l'intérieur de onDrawFrame () méthode du rendu, ajouter un appel à la dessiner() méthode du Torus classe pour dessiner réellement le tore.

@Override public void onDrawFrame (GL10 gl10) torus.draw (); 

À ce stade, vous pouvez exécuter votre application pour voir le tore orange..

Conclusion

Vous savez maintenant comment utiliser OpenGL ES dans les applications Android. Dans ce didacticiel, vous avez également appris à analyser un fichier OBJ Wavefront et à en extraire les données de sommet et de face. Je vous suggère de générer quelques objets 3D supplémentaires à l'aide de Blender et d'essayer de les rendre dans l'application..

Bien que nous nous sommes concentrés uniquement sur OpenGL ES 2.0, sachez qu'OpenGL ES 3.x est rétro-compatible avec OpenGL ES 2.0. Cela signifie que si vous préférez utiliser OpenGL ES 3.x dans votre application, vous pouvez simplement remplacer le GLES20 classe avec le GLES30 ou GLES31 Des classes.

Pour en savoir plus sur OpenGL ES, vous pouvez vous reporter à ses pages de référence. Et pour en savoir plus sur le développement d'applications Android, consultez certains de nos autres tutoriels ici à Envato Tuts.+!

  • Comment débuter avec le kit de développement natif d'Android

    Avec le lancement d'Android Studio 2.2, le développement d'applications Android contenant du code C ++ est devenu plus facile que jamais. Dans ce tutoriel, je vais vous montrer comment…
    Ashraff Hathibelagal
    Android
  • Choses Android: Entrée / sortie périphérique

    Android Things a la capacité unique de se connecter facilement à des composants électroniques externes avec l'API périphérique et la prise en charge d'appareils intégrés. Dans cet article…
    Paul Trebilcox-Ruiz
    SDK Android
  • Comment sécuriser une application Android

    Dans cet article, nous allons examiner certaines des meilleures pratiques que vous pouvez suivre pour créer une application Android sécurisée. Cela signifie une application qui ne fuit pas…
    Ashraff Hathibelagal
    Android
  • Codage d'une application Android avec Flutter et Dart

    Flutter de Google est un framework de développement d'applications multi-plateformes qui utilise le langage de programmation Dart. Dans ce tutoriel, je vais vous présenter les bases de…
    Ashraff Hathibelagal
    Android