Programação com OpenGL/Android GLUT Wrapper

Nosso empacotador(wrapper): Bastidores

Se você planeja escrever seus próprios aplicativos OpenGL ES 2.0, aqui estão algumas dicas de como nosso empacotador(wrapper) funciona:

Escrevendo código C/C++ para Android

editar

Todos os Aplicativos do Android são escrito em Java, mas você pode chamar códigos em C/C++ usando o JNI (Java Native Interface ), que estão presentes na NDK ( Native Development Kit).

Como você pode fazer isto:

  • Escreva um empacotador em Java e C++:
    • Disponível a partir do Android 1.5
    • O Código C++ pode interagir com um contexto OpenGL ES criado pelo Java
    • Para criar um contexto OpenGL ES 2.0 ( com EGL ) diretamente pelo C++ é necessário o Android 2.3/Gingerbread/API android-9
    • O OpenGL ES 2.0 está disponível a partir do Android 2.0/API android-5
    • Exemplos: na NDK hello-gl2.
  • Contando com um "NativeActivity" incorporado ao empacotador Java, e escrevendo apenas em C++:
    • Disponivel a partir do Android 2.3/Gingerbread/API android-9
    • Use o EGL para criar um contexto OpenGL ES.
    • Exemplos na NDK native-activity ( está em OpenGL ES 1.x, mas você pode atualizar ele sem dificuldade)

Detalhes da Native Activity

editar

O Android 2.3/Gingerbread/API android-9 introduziram o native activities, que permitem escreve um aplicativo sem usar o Java.

Mesmo que o exemplo menciona a API versão 8, colocaremos um 9.

     <uses-sdk android:minSdkVersion="9" />

Tambem, certifique-se que esteja assim seu manifest has:

<application ...
        android:hasCode="true"

Caso contrário seu aplicativo não iniciará. Seu ponto de entrada será a função android_main(em vez da mais comum que é main ou WinMain) Para uma melhor portabilidade, você pode renomear a versão do pré-processador usando -Dmain=android_main[1]

Compilador do sistema

editar

O empacotador é baseado no exemplo native-activity, ele usa o código do 'android_native_app_glue' que ofereçe um processamento de eventos non-blocking.

<!-- Android.mk -->
LOCAL_STATIC_LIBRARIES := android_native_app_glue
...
$(call import-module,android/native_app_glue)

Desde que você não chame diretamente o código glue (seus pontos de entrada são os callbacks usados pelo Android, não o nosso), android_native_app_glue.o ele pode ser retirada pelo compilador, assim vamos chamar seus modelos de pontos de entrada:

    // Certifique que o glue foi retirado.
    app_dummy();

Ele usará o OpenGL ES 2.0 (em vez do exemplo em OpenGL Es 1.X):

<!-- Android.mk -->
LOCAL_LDLIBS    := -llog -landroid -lEGL -lGLESv2

Para usar o GLM, nós precisaremos ativar o C++ STL:

<!-- Application.mk -->
APP_STL := gnustl_static

e mencionar o local aonde foi instalado:

<!-- Android.mk -->
LOCAL_CPPFLAGS  := -I/usr/src/glm

Agora nós vamos declarar nossos arquivo fonte ( tut.cpp ):

<!-- Android.mk -->
LOCAL_SRC_FILES := main.c GL/glew.c tut.cpp

para rodar o compilador do sistema:

  • Compile o Código em C/C++
ndk-build NDK_DEBUG=1 V=1
  • Preparando o compilador Java do sistema (Somente uma vez):
android update project --name wikibooks-opengl --path . --target "android-10"
  • Criando um pacote .apk:
ant debug
  • E instalando:
ant installd
# ou manualmente:
adb install -r bin/wikibooks-opengl.apk
  • Limpando:
ndk-build clean
ant clean

Nós incluiremos todos estes comandos no montador Makefile

Criando um contexto em OpenGL ES com o EGL

editar

Nós precisamo usar EGL para o OpenGL ES versão 2.0 (não a versão 1.x).

Em primeiro lugar, solicitaremos os contextos disponíveis:

    const EGLint attribs[] = {
            ...
	    EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
            EGL_NONE
    };
    ...
    eglChooseConfig(display, attribs, &config, 1, &numConfigs);

Depois criaremos o contexto:

    static const EGLint ctx_attribs[] = {
      EGL_CONTEXT_CLIENT_VERSION, 2,
      EGL_NONE
    };
    context = eglCreateContext(display, config, EGL_NO_CONTEXT, ctx_attribs);

(Em Java:)

setEGLContextClientVersion(2);
// ou em um Renderizador personalizado:
int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
EGLContext context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);

É uma boa prática, mas não é obrigatória, que você de declare os requirementos OpenGL 2.0 no AndroidManifest.xml:

<uses-feature android:glEsVersion="0x00020000"></uses-feature>
<uses-sdk android:targetSdkVersion="9" android:minSdkVersion="9"></uses-sdk>

Quando o usuário vai para tela inicial(ou recebe uma chamada), seu aplicativo é pausado, Quando ele volta para o aplicativo, ele continua, mas o contexto OpenGL é perdido, Neste caso, você precisa recarregar todos recursos na GPU (VBOs, texturas e etc...), Existe um evento no Android para detectar se seu aplicativo foi "despausado".

Da mesma forma que quando o usuário precisa o botão voltar, o aplicativo é destruído, mas fica na memória e pode ser reiniciado.

Para nosso empacotador(wrapper), nos consideremos que o aplicativo GLUT não foi desenhado para refazer o contexto OpenGL, muito menos refazer todas as variáveis estaticamente atribuídas. consequentemente o aplicativo sairá completamente enquanto o contexto é perdido, semelhante ao botão fechar dos desktops.

Eventos no Android

editar

Mesmo que escrevamos um código nativo, nosso aplicativo será iniciado em um processo Java, usando o android.app.NativeActivity oferecido como ativador. que é o processo responsável por receber os eventos do dispositivo e passar para seu aplicativo.

Funcionamento:

  • O Android envia um evento para o processo NativeActivity do Java.
  • O Activity do Java, chamara todas as funções de callback(chamadas de fundo) (ex: tal como protected void onLowMemory() em caso de memória baixa)
  • O NativeActivity chamará todas as funções JNI correspondente em seu android_app_NativeActivity(ex: void onLowMemory_native(...)
  • O android_app_NativeActivity.cpp chamará as chamadas NativeCode correspondente no android_native_app_glue.c (ex: void onLowMemory(...)
  • O android_native_app_glue.c escreve uma messagem através do C como pipe(2) (ex: APP_CMD_LOW_MEMORY, e retornára imediatamente para o processo Java não ficar preso (Senão será oferecido ao usuário que ele seja encerrado)
  • Em sua native app, regularmente, checaremos os eventos que estão na fila e as chamadas android_native_app_glue.c process_cmd (ou process_input)
  • vamos voltar para o nivel acima em android_native_app_glue.c, Aonde o process_cmd executará um pré evento e um pós evento genérico que intermediaria as chamadas ao nosso aplicativo onAppCmd.
  • Voltando ao nosso app, aonde pelo onAppCmd (ex: engine_nandle_cmd) processa os eventos que estão por ultimo!

Recursos ou Assets

editar

Os aplicativos do Android normalmente extraem os recursos ( como shaders or meshes ) de uma arquivo .apk ( um tipo de arquivo Zip).

  • Os recursos estão localizados na sub-pasta res/ (ex. res/layout); Existem funções do Android que carregam eles dependendo do seu tipo.
  • Os Assets estão localizados na pasta assets/ e são acessadas por meio de uma estrutura de diretório mais tradicional.

Isto não é comum em aplicativos GLUT, assim vamos fazer os recursos ficarem disponíveis com mais facilidade:

  • Usando um wrapper(montador) com fopen/open.
    • Carregando com LD_PRELOAD, como em zlibc
  • Usando o kernel ptrace hooks
  • Redefinindo o fopen do seu arquivo .cpp
  • Extraindo os arquivos antecipadamente

A implementação de um montador(wrapper) é tedioso, porque seu aplicativo o chamara por meio da JNI. Isto significa que não podemos apenas fazer um execv de outro aplicativo depois de ajustar o LD_PRELOAD, em vez disto precisamos iniciar um processo filho, antes de todos os eventos do Android, e ajustar um IPC para compartilhar a estrutura de dados do android_app e do ALooper, o ptrace também requer um processo filho.

Redefinindo o fopen localmente podemos trabalhar com a função do C fopen, mas não com a do C++ cout

Pré-extraindo todos os ativadores adicionais exigem um espaço em disco para gravar os arquivos, mas é apenas uma solução razoável.

Acessando os recursos

editar

Desenvolvedores podem estruturar os acesso ao recursos mais facilmente pela NDK:

  • A API do Android: você pode chamar uma função do Java por meio da JNI, mas pegar um descritor do arquivo requer o uso de uma função não-oficial e apenas trabalhar com arquivos descompactados; usaremos um operador de Buffer Java ao invés de um tediosa do C/C++
  • libzip: você pode acessar mais facilmente um arquivo .apk com libzip, por isto você precisa integrar a biblioteca ao compilador do sistema.
  • API do NDK: no ultimo Android 2.3/Gingerbread/API android-9,tem uma api NDK para acessar os recursos.

Vamos usar a API do NDK, isto não é transparente para o desenvolvedor(sem a substituição do fopen/cout) mas é razoavelmente mais fácil de usar.

O que é um pouco mais complicado é pegar o AssetManager pelo Java/JNI na sua native activity.

Nota: Usaremos um sintaxe levemente simplificada do C++(não é uma sintaxe C) para o JNI.

Primeiro, nosso native activity trabalha com seu próprio segmento, então precisamos de cuidado ao recuperar o JNI handle em android_main:

    JNIEnv* env = state_param->activity->env;
    JavaVM* vm = state_param->activity->vm;
    vm->AttachCurrentThread(&env, NULL);

Então vamos pegar o estado em nossa chamada instanciada NativeActivity:

    jclass activityClass = env->GetObjectClass(state_param->activity->clazz);

Então nós decidiremos aonde extrair os arquivos. Então usaremos um cache padrão de diretório:

    // Get path to cache dir (/data/data/org.wikibooks.OpenGL/cache)
    jmethodID getCacheDir = env->GetMethodID(activityClass, "getCacheDir", "()Ljava/io/File;");
    jobject file = env->CallObjectMethod(state_param->activity->clazz, getCacheDir);
    jclass fileClass = env->FindClass("java/io/File");
    jmethodID getAbsolutePath = env->GetMethodID(fileClass, "getAbsolutePath", "()Ljava/lang/String;");
    jstring jpath = (jstring)env->CallObjectMethod(file, getAbsolutePath);
    const char* app_dir = env->GetStringUTFChars(jpath, NULL);

    // chdir in the application cache directory
    LOGI("app_dir: %s", app_dir);
    chdir(app_dir);
    env->ReleaseStringUTFChars(jpath, app_dir);

Nos agora vamos pegar o AssetManager NativeActivity:

#include <android/asset_manager.h>
    jobject assetManager = state_param->activity->assetManager;
    AAssetManager* mgr = AAssetManager_fromJava(env, assetManager);

Agora a extração é simples: navegaremos pelos arquivos e copiaremos para o disco um por um:

    AAssetDir* assetDir = AAssetManager_openDir(mgr, "");
    const char* filename = (const char*)NULL;
    while ((filename = AAssetDir_getNextFileName(assetDir)) != NULL) {
	AAsset* asset = AAssetManager_open(mgr, filename, AASSET_MODE_STREAMING);
	char buf[BUFSIZ];
	int nb_read = 0;
	FILE* out = fopen(filename, "w");
	while ((nb_read = AAsset_read(asset, buf, BUFSIZ)) > 0)
	    fwrite(buf, nb_read, 1, out);
	fclose(out);
	AAsset_close(asset);
    }
    AAssetDir_close(assetDir);

Agora, todos os arquivos podem ser acessados usando um fopen/cout do aplicativo.

Esta tecnica é adaptada por nosso tutorial, mas provavelmente não para um grande aplicativo. Neste caso, você pode usar:

  • Requisitar um privilégio de escrita no cartão de memória(SD) para extrair os arquivos nele (é isto que o "port" do SDL no Android faz)
  • Usar o wrapper para acessar os seu arquivos que serão usados pelo AssetManager no Android ( cuidado é um acesso somente-leitura)

Orientações

editar

Pela opção:

        <activity ...
                android:screenOrientation="portrait"

Seu aplicativo pode trabalhar apenas no modo portrait, independente do seu dispositivo ou das formas. Isto não é recomendado mas é muito usado em alguns jogos.

Para manusear as orientção mais eficientemente, você teoricamente precisa checar pelo evento onSurfaceChanged. O manuseador onSurfaceChanged_native no montador(wrapper) android_app_NativeActivity.cpp não parece criar o eventoonNativeWindowResized adequadamente na mudança de orientação, Então vamos monitora-lo regularmente:

/* glutMainLoop */

    int32_t lastWidth = -1;
    int32_t lastHeight = -1;

    // loop waiting for stuff to do.
    while (1) {

        ...

	int32_t newWidth = ANativeWindow_getWidth(engine.app->window);
	int32_t newHeight = ANativeWindow_getHeight(engine.app->window);
	if (newWidth != lastWidth || newHeight != lastHeight) {
	    lastWidth = newWidth;
	    lastHeight = newHeight;
	    onNativeWindowResized(engine.app->activity, engine.app->window);
	    // Process new resize event :)
	    continue;
	}

Agora nos podemos processar o evento:

static void onNativeWindowResized(ANativeActivity* activity, ANativeWindow* window) {
    struct android_app* android_app = (struct android_app*)activity->instance;
    LOGI("onNativeWindowResized");
    // Sent an event to the queue so it gets handled in the app thread
    // after other waiting events, rather than asynchronously in the
    // native_app_glue event thread:
    android_app_write_cmd(android_app, APP_CMD_WINDOW_RESIZED);
}

Nota: é possivel processar pelo evento APP_CMD_CONFIG_CHANGED, mas isto só acontece depois que tela é redimensionada, isto é muito cedo para pegar o novo tamanho da tela.

O Android pode apenas detectar o nova resolução e depois da troca de buffer, então vamos abusar de outra hook(uma chamada própria no lugar da chamada do original sistema) para obter o evento de redimensionamento:

/* android_main */
    state_param->activity->callbacks->onContentRectChanged = onContentRectChanged;

...

static void onContentRectChanged(ANativeActivity* activity, const ARect* rect) {
    LOGI("onContentRectChanged: l=%d,t=%d,r=%d,b=%d", rect->left, rect->top, rect->right, rect->bottom);
    // Make Android realize the screen size changed, needed when the
    // GLUT app refreshes only on event rather than in loop.  Beware
    // that we're not in the GLUT thread here, but in the event one.
    glutPostRedisplay();
}

Eventos de entrada do dispositivo

editar

Nós reusaremos a função engine_handle_input para este exemplo do native-activity.

É importante ter o return 0 enquanto o evento não é diretamente manuseável, para que o sistema Android faça isto, para instanciá-lo nos comummente deixaremos o Android cuidar do botão de retorno(back).

O framework NativeActivity parece não enviar apropriadamente os eventos de repetição: A tecla é pressionada e despressionada exatamente ao mesmo tempo, e a conta de repetição é sempre 0, Consequentemente não é parece possivel processar as setas do Hacker´s Keyboard sem rescrever parte da framework.

Movimentação (touchscreen) e eventos do teclado são manuseados apenas pelo mesmo canal.

Para acompanhar o usuario quando ele sai do teclas para usar as setas do teclado, nos implementaremos um virtual keypad(VPAD), localizado no canto inferior esquerdo, ativando a touchscreen. O esforço foi feito para evitar a mistura de um evento VPAD e um evento de movimentação e vice-versa.

Referencia

editar
  1. Esta é a técnica usada pelo SDL para o Windows WinMain.