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 | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]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 noandroid_native_app_glue.c
(ex:void onLowMemory(...)
- O
android_native_app_glue.c
escreve uma messagem através do C comopipe(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 (ouprocess_input
) - vamos voltar para o nivel acima em
android_native_app_glue.c
, Aonde oprocess_cmd
executará um pré evento e um pós evento genérico que intermediaria as chamadas ao nosso aplicativoonAppCmd
. - Voltando ao nosso app, aonde pelo
onAppCmd
(ex:engine_nandle_cmd
) processa os eventos que estão por ultimo!
Recursos ou Assets
[editar | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]- ↑ Esta é a técnica usada pelo SDL para o Windows
WinMain
.