OpenGL no Android

O Android fornece suporte para gráficos 2D e 3D de alta performance com a biblioteca Open Graphics Library (OpenGL), especificamente, a API OpenGL ES. O OpenGL é uma API multi-plataforma que especifica uma interface padrão de software para o hardware que processa gráficos 3D. O OpenGL ES é um tipo de especificação do OpenGL projetada para dispositivos móveis. As especificações OpenGL ES 1.0 and 1.1 são suportadas desde o Android 1.0. A partir da versão 2.2 (API Level 8), o framework passa a suporta a especificação OpenGL ES 2.0.

O Básico

O Android suporta o OpenGL através tanto de seu framework quanto do Native Development Kit (NDK). Esse artigo foca nas interfaces do framework. Para mais informações sobre o NDK, leia o artigo Android NDK.

Existem duas classes fundamentais do framework que permitem que você crie e manipule gráficos com a API OpenGL ES:GLSurfaceView e GLSurfaceView.Renderer. Se o seu objetivo for usar o OpenGL em sua aplicação Android, entender como implementar essas classes em um Activity deve ser seu primeiro objetivo.

GLSurfaceView
Essa classe é um View onde você pode desenhar e manipular objetos usando chamadas OpenGL e é similar em função aSurfaceView. Você pode usar essa classe para criar uma instância de GLSurfaceView e adicionar o seu Renderer a ela. Porém, se você quiser capturar eventos de toque na tela, você precisa uasr a classeGLSurfaceView para poder implementar eventos que capturem por esses tipos de eventos, como mostrado no artigo, Responding to Touch Events.
GLSurfaceView.Renderer
Essa interface define os métodos necessários para o desenho de gráficos em um GLSurfaceView. Você precisa fornecer uma implementação dessa interface como uma classe separada e anexa-la a sua instância de GLSurfaceView usando GLSurfaceView.setRenderer().A interface GLSurfaceView.Renderer necessita que você implemente os seguintes métodos:

  • onSurfaceCreated(): O sistema chama esse método uma única vez, quando cria o GLSurfaceView. Use esse método para executar ações que devem acontecer apenas uma vez, como configurar parâmetros de ambiente do OpenGL ou inicializar objetos gráficos.
  • onDrawFrame(): O sitema chama esse método cada vez que GLSurfaceView é redesenhado. Use esse método como ponto principal de desenho (ou re-desenho) de objetos gráficos.
  • onSurfaceChanged(): O sistema chama esse método quando a geometria do GLSurfaceView é alterada, incluindo mudanças no tamanho do GLSurfaceView ou na orientação da tela do dispositivo. Por exemplo, o sistema chama esse método quando o dispositivo muda da orientação retrato para paisagem. Use esse método para responder as mudanças no contêiner do GLSurfaceView.

Pacotes do OpenGL

Uma vez que você estabeleceu um contêiner para o OpenGL usando  o GLSurfaceView e oGLSurfaceView.Renderer, você pode começar fazer chamadas as APIs do OpenGL usando as seguintes classes:

Se você quiser começar de imediato a criar uma aplicação com OpenGL, siga os passos do artigo Displaying Graphics with OpenGL ES.

Declarando os requisitos para o OpenGL


Se a sua aplicação usar recursos do OpenGL que não estejam disponíveis em todos os dispositivos, você precisa incluir esses requisitos em seu arquivo AndroidManifest.xml. A seguir estão as declarações mais comuns para o OpenGL:

  • requisitos de versão para o OpenGL ES – Se a sua aplicação suporta apenas o OpenGL ES 2.0, você precisa declarar esse requisito adicionando a seguinte configuração ao seu arquivo de manifesto:
        <!-- Tell the system this app requires OpenGL ES 2.0. -->
        <uses-feature android:glEsVersion="0x00020000" android:required="true" />

    Adicionando essa declaração faz com que o Google Play restrinja a sua aplicação de ser instalada em dispositivos que não suportem o OpenGL ES 2.0.

  • requisitos de compressão de texturas – Se a sua aplicação usa algum formato de compressão de texturas, você precisa declarar os formatos que a sua aplicação suporta no arquivo de manifesto usando <supports-gl-texture>. Para mais informações sobre os formatos de compressão de textura disponíveis, veja Texture compression support.

Mapeamento de coordenadas para o desenho de objetos


Um dos problemas básicos em exibir gráficos em dispositivos Android é que as telas deles podem várias em tamanho e forma. O OpenGL assume um quadrado, sistema de coordenadas uniforme e, por padrão, desenha essas coordenadas em sua tela tipicamente não-quadrada como se ela fosse um quadrado perfeito.

coordinates

Figura 1. Sistema de coordenadas padrão do OpenGL (à esquerda) mapeado em uma tela típica do Android (à direita).

A ilustração acima mostra o sistema de coordenadas uniforme para um quadro do OpenGL a esquerda, e como essas coordenadas são de fato mapeadas em uma típica tela de dispositivo em orientação paisagem a direita. Para resolver esse problema, você pode aplicar modos de projeção e visões de câmera para transformar coordenadas de forma que seus objetos gráficos tenham as proporções corretas em cada tela.

Para poder aplicar projeções e visões de câmera, você cria uma matriz de projeção e uma matriz de visão de câmera e aplica essas matrizes ao canal de renderização do OpenGL. A matriz de projeção recalcula as coordenadas de seus gráficos de forma que eles sejam mapeados corretamente nas telas dos dispositivos Android. A matriz de visão de câmera cria uma transformação que renderiza os objetos a em uma posição especifica do olhar.

Projeção e visão da cãmera no OpenGL ES 1.0

Na API do ES 1.0, você aplica projeções e visões de câmera pela criação de cada matriz e posterior adição delas ao ambiente OpenGL.

  1. Matriz de projeção – Crie uma matriz de projeção usando a geometria da tela de seu dispositivo de forma a poder recalcular as coordenadas do objeto de forma que sejam desenhados nas proporções corretas. O exemplo de código abaixo demonstra como modificar o método onSurfaceChanged() de uma implementação de GLSurfaceView.Renderer para criar uma matriz de projeção baseada no tamanho da tela e aplicar ao ambiente do OpenGL.
      public void onSurfaceChanged(GL10 gl, int width, int height) {
          gl.glViewport(0, 0, width, height);
    
          // make adjustments for screen ratio
          float ratio = (float) width / height;
          gl.glMatrixMode(GL10.GL_PROJECTION);        // set matrix to projection mode
          gl.glLoadIdentity();                        // reset the matrix to its default state
          gl.glFrustumf(-ratio, ratio, -1, 1, 3, 7);  // apply the projection matrix
      }
  2. Matriz de transformação de câmera – Uma vez que você tenha ajustado o sistema de coordenadas usando uma matriz de projeção, você precisa aplicar uma visão de câmera. O exemplo de código a seguir mostra como modificar o método onDrawFrame() de uma implementação de GLSurfaceView.Renderer para aplicar uma visão e usar o utilitárioGLU.gluLookAt() para criar uma transformação que simula uma posição de câmera.
        public void onDrawFrame(GL10 gl) {
            ...
            // Set GL_MODELVIEW transformation mode
            gl.glMatrixMode(GL10.GL_MODELVIEW);
            gl.glLoadIdentity();                      // reset the matrix to its default state
    
            // When using GL_MODELVIEW, you must set the camera view
            GLU.gluLookAt(gl, 0, 0, -5, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
            ...
        }

Projeção e visão da câmera no OpenGL ES 2.0

Na API do ES 2.0, você aplica projeções e visões de câmera primeiramente pela adição de um membro matriz aos shaders dos vértices de seus objetos gráficos. Com esse membro adicionado, você pode gerar e aplicar matrizes de projeções e de visões de câmera a seus objetos.

  1. Adicione a matriz aos shaders dos vértices – Crie uma variável para a matriz de projeção e inclua ela como um multiplicador da posição do shader. No exemplo de código a seguir, o membro  uMVPMatrix permite que você aplique uma projeção e matrizes de câmera nas coordenadas dos objetos que usam esse shader.
        private final String vertexShaderCode =
    
            // This matrix member variable provides a hook to manipulate
            // the coordinates of objects that use this vertex shader
            "uniform mat4 uMVPMatrix;   \n" +
    
            "attribute vec4 vPosition;  \n" +
            "void main(){               \n" +
    
            // the matrix must be included as part of gl_Position
            " gl_Position = uMVPMatrix * vPosition; \n" +
    
            "}  \n";

    Nota: O exemplo acima define um único membro da matriz de transformação no shader do vértice no qual você aplica uma matriz de projeção e matriz de visão de câmera combinadas. Dependendo dos requisitos de sua aplicação, você pode querer definir membros separadas para as duas matrizes de forma que você possa altera-las de forma independente.

  2. Acesse a matriz de sombras – Depois de criar um gancho em seus shaders para aplicar projeções e visões de câmera, você pode então acessar essas variáveis para aplicar as matrizes correspondentes. O código a seguir mostra como modificar o método  onSurfaceCreated() de uma implementação de GLSurfaceView.Renderer para acessar as matrizes definidas nos shaders dos vértices.
        public void onSurfaceCreated(GL10 unused, EGLConfig config) {
            ...
            muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
            ...
        }
  3. Crie matrizes de projeção e visões de câmera – Gere a projeção e visão de câmera para serem aplicadas aos objetos gráficos. O código a seguir mostra como modificar os métodos onSurfaceCreated() eonSurfaceChanged() de uma implementaçaõ de GLSurfaceView.Renderer para criar matrizes de projeção e visões de câmera baseados no formato da tela de um dispositivo.
        public void onSurfaceCreated(GL10 unused, EGLConfig config) {
            ...
            // Create a camera view matrix
            Matrix.setLookAtM(mVMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
        }
    
        public void onSurfaceChanged(GL10 unused, int width, int height) {
            GLES20.glViewport(0, 0, width, height);
    
            float ratio = (float) width / height;
    
            // create a projection matrix from device screen geometry
            Matrix.frustumM(mProjMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
        }
  4. Aplique as matrizes de projeção e visões de câmera – Para aplicar as transformações, multiplique as matrizes e então configure elas no shader do vértice. O exemplo a seguir mostra como modificar o método onDrawFrame() de uma implementação de GLSurfaceView.Renderer para combinar as matrizes de projeção e de visões de câmera criadas no código acima em seguida aplica-las aos objetos gráficos a serem renderizados pelo OpenGL.
        public void onDrawFrame(GL10 unused) {
            ...
            // Combine the projection and camera view matrices
            Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mVMatrix, 0);
    
            // Apply the combined projection and camera view transformations
            GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0);
    
            // Draw objects
            ...
        }

Para um exemplo completo de como aplicar projeções e visões de câmera com o OpenGL ES 2.0, veja  Displaying Graphics with OpenGL ES.

Formas e curvas


No OpenGL, uma forma é uma superfície definida por três ou mais pontos em um espaço tridimensional. Um conjunto de três ou mais pontos tridimensionais (chamados de vértices no OpenGL) possui uma face frontal uma face traseira. Como você sabe qual face é a frontale qual face é a traseira? Boa pergunta. A resposta tem a ver com as curvas, ou a direção na qual você define os pontos de sua forma.

ccw-winding

Figura 2. Ilustração de uma lista de coordenadas que são traduzidas em uma ordem de desenho no sentido anti-horário.

Nesse exemplo, os pontos do triângulo são definidos em uma ordem tal que eles são desenhados na direção anti-horária. A ordem em que essas coordenadas são desenhadas definem a direção da curva para a forma. Por padrão, no OpenGL, a face que é desenhada no sentido anti-horário é a face frontal. O triângulo da figura 2 é definido de forma que você olhe para a parte da frente da forma (como interpretado pelo OpenGL) e o outro lado seja a face traseira.

Por que é importante saber qual a face frontal de uma forma? A resposta tem a ver com um recurso usado com frequência no OpenGL que permite que o processo de renderização ignore (não calculo ou desenhe) a parte traseira de uma forma, economizando tempo, memória e ciclos de processamento:

// enable face culling feature
gl.glEnable(GL10.GL_CULL_FACE);
// specify which faces to not draw
gl.glCullFace(GL10.GL_BACK);

Se você tentar usar o recurso de corte (culling)sem saber quais lados de sua forma são a parte frontal ou traseira, seus gráficos OpenGL irão ser exibidos de forma bem clara, ou talvez nem sejam exibidos. Assim, sempre defina as coordenadas de suas formas em ordem anti-horária.

Nota: É possível configurar o ambiente do OpenGL para tratar a face desenhada em sentido horário como a face frontal, mas fazer isso irá requerer mais código e irá causar confusão entre os desenvolvedores. Dessa forma, evite fazer isso.

Versões do OpenGL e compatibilidade entre dispositivos


As especificações da API do OpenGL ES 1.0 e 1.1 tem sido suportadas desde o Android 1.0. Desde a versão 2.2 (API Level 8), o framework suporta a especificação da API do OpenGL ES 2.0. O OpenGL ES 2.0 é suportado pela maioria dos dispositivos Android e é recomendado para novas aplicações. Para informações sobre o número de dispositivos Android que suportam uma versão especifica do OpenGL, veja OpenGL ES Versions Dashboard.

Suporte a compressão de texturas

Compressão de texturas pode aumentar significativamente a performance de sua aplicação OpenGL pela redução das necessidades de memória e tornando mais eficiente o uso da largura de banda da memória. O framework do Android fornece suporte para o formato de compressão ETC1 como padrão, incluindo uma classe utilitária ETC1Util e uma ferramenta de compressãoetc1tool (localizada no SDK do Android em <sdk>/tools/). Para obter um exemplo de uma aplicação Android que usa compressão de texturas, veja o exemplo CompressedTextureActivity.

O formato ETC é suportado pela maioria dos dispositivos Android, mas não tem garantias de disponibilidade. Para verificar se o formato ETC1 é suportado pelo seu dispositivo, chame o método ETC1Util.isETC1Supported().

Nota: O formato de compressão de textura ETC1 não suporta texturas com um canal alpha. Se sua aplicação requer uma textura com um canal alpha, você deve procurar por outros formatos de compressão disponíveis para os seus dispositivos alvos.

Além do formato ETC1, os dispositivos Android tem suporte a diversos formatos de compressão dependendo dos seus chipsets GPU e de suas implementações do OpenGL. Você deve investigar o suporte a compressão de texturas nos dispositivos que sua aplicação vai rodar para determinar que tipos de compressão sua aplicação deve suportar. Para poder determinar quais formatos são suportados em um dispositivo especifico, você pode perguntar ao dispositivo e visualizar os nomes das extensões do OpenGL, que identificam quais formatos de compressão são suportados pelo dispositivo. Alguns formatos de compressão comuns são:

  • ATITC (ATC) – ATI texture compression (ATITC or ATC) is available on a wide variety of devices and supports fixed rate compression for RGB textures with and without an alpha channel. This format may be represented by several OpenGL extension names, for example:
    • GL_AMD_compressed_ATC_texture
    • GL_ATI_texture_compression_atitc
  • PVRTC – PowerVR texture compression (PVRTC) is available on a wide variety of devices and supports 2-bit and 4-bit per pixel textures with or without an alpha channel. This format is represented by the following OpenGL extension name:
    • GL_IMG_texture_compression_pvrtc
  • S3TC (DXTn/DXTC) – S3 texture compression (S3TC) has several format variations (DXT1 to DXT5) and is less widely available. The format supports RGB textures with 4-bit alpha or 8-bit alpha channels. This format may be represented by several OpenGL extension names, for example:
    • GL_OES_texture_compression_S3TC
    • GL_EXT_texture_compression_s3tc
    • GL_EXT_texture_compression_dxt1
    • GL_EXT_texture_compression_dxt3
    • GL_EXT_texture_compression_dxt5
  • 3DC – 3DC texture compression (3DC) is a less widely available format that supports RGB textures with an an alpha channel. This format is represented by the following OpenGL extension name:
    • GL_AMD_compressed_3DC_texture

Aviso: Esses formatos de compressão de textura não são suportados em todos os dispositivos. O suporte para esses formatos pode variar por fabricante e dispositivo. Para informações sobre como determinar quais formatos  estão disponíveis em um dispositivo especifico, veja a próxima seção.

Note: Uma vez que você decida quais formatos de compressão sua aplicação irá suportar, certifique-se de declara-las em seu arquivo de manifesto usando a tag <supports-gl-texture> . Usando essa declaração permite a filtragem por parte serviços externos como o Google Play, de forma que sua aplicação seja instalada apenas em dispositivos que suportem os formatos que sua aplicação necessita.

Determinando extensões do OpenGL

As implementações do OpenGL variam de um dispositivo Android para outro em termos de extensões da API do OpenGL ES API que são suportadas. Essas extensões incluem compressões de texturas, mas tipicamente também incluem outras extensões do OpenGL.

Para determinar quais formatos de compressão de texturas e outras extensões do OpenGL são suportados em um dispositivo em particular:

  1. Execute o código a seguir em seus dispositivos alvos para determinar quais formatos de compressão de texturas são suportados:
      String extensions = javax.microedition.khronos.opengles.GL10.glGetString(GL10.GL_EXTENSIONS);

    Aviso: Os resultados dessa chamada variam em cada dispositivo. Você pode executar essa chamada em vários dispositivos para determinar quais tipos de compressão são mais comumente suportados.

  2. Veja a saída dessa método para determinar quais extensões do OpenGL são suportadas em um dispositivo.

Escolhendo uma versão da API do OpenGL


Tanto a API do OpenGL ES 1.0 (e as extensões do 1.1) quanto a da versão 2.0 fornecem interfaces gráficas de alta performance para a criação de jogos 3D, visualizações e interfaces com o usuário.A programação de gráficos para a API do OpenGL ES 1.0/1.1 versus o OpenGL ES 2.0 diferem de forma significativa, assim os desenvolvedores devem considerar cuidadosamente os seguintes fatores antes de iniciar o desenvolvimento com qualquer uam dessas APIs:

  • Performance – Em geral, o OpenGL ES 2.0 fornece performances gráficas mais rápidas d do que as APIs do ES 1.0/1.1. Porém, a diferença de performance pode varias dependendo do dispositivo Android em que sua aplicação está rodando, devido a implementações diferentes do OpenGL.
  • Compatibilidade de dispositivos – Desenvolvedores devem considerar os tipos de dispositivos, versões do Android e do OpenGL ES disponíveis para seus consumidores. Para mais informações sobre a compatibilidade entre dispositivos, veja OpenGL Versions and Device Compatibility.
  • Conveniência de código – A API do OpenGL ES 1.0/1.1 fornece algumas funções conveniente que não estão disponíveis na API do ES 2.0. Os desenvolvedores novatos no OpenGL podem achar a codificação com o OpenGL ES 1.0/1.1 mais rápida em conveniente.
  • Controle dos gráficos – A API do OpenGL ES 2.0 API fornece um alto nível de controle através do fornecimento de um canal completamente programável através do uso de shaders. Com mais controle direto dos gráficos, os desenvolvedores podem criar efeitos que seriam mais difíceis de serem gerados com a API do 1.0/1.1.

Enquanto performance, compatibilidade, conveniência, controle e outros fatores podem influenciar a sua decisão,  você deve escolher uma versão baseada no que você achar que fornece uma melhor experiência para os usuários.

Traduzido da documentação oficial – http://developer.android.com/guide/topics/graphics/opengl.html