Friday, April 5, 2013

OpenGL 1.0 - Part 2 - Simple Android Games

Introduction
Here we try to do in JNI what we accomplished in java in part 1 of this howto. Then we're going to try to connect our game code to it. By this I mean show some basic functionality, specifically showing motion on the screen.

Package Name
The package name for our project is 'com.example.testapp'. We've noted this before, but at this stage we start writing JNI, so it becomes particularly important.

Organization of JNI
So far we've compiled simple JNI files. We've installed the android NDK package, written our source code files and a makefile, and compiled the code. All our game code has been in two files. One was called 'awesomeguy.h' and one was called 'awesomeguy.c'. Now we're going to add all our OpenGL code to a third file called 'androidgl1.c'. Then we're going to rename our 'h' file to 'androidgl.h'. Strictly speaking we don't need to rename anything, certianly not our header file, but at some point I made the change, and so now I'm going to stick with it for these explanations. Because we have more source code files, our makefile has changed also. In the actual game I am going to try to keep the game code in the 'awesomeguy.c' file, with almost no regard to how large the file gets. The only code that I've separated to a different file is the OpenGL code. For this howto I will change the name of the 'awesomeguy.c' file to 'testapp.c'.

Makefile - Android.mk
Here I have included the new 'Android.mk' file. It includes all the newly renamed files.

LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := testapp
LOCAL_CFLAGS    := -Werror 
LOCAL_SRC_FILES := testapp.c androidgl1.c
LOCAL_LDLIBS    := -llog -lGLESv1_CM

include $(BUILD_SHARED_LIBRARY)


Notice the line that starts 'LOCAL_LDLIBS := -llog -lGLESv1_CM' . This line tells the NDK tools to add the OpenGL ES version 1 libraries to the project.

Includes
Here I have included the new list of 'include' statements.

#include < jni.h >
#include < android/log.h >
#include < stdio.h >
#include < stdlib.h >

#include < GLES/gl.h >
#include < GLES/glext.h >
Notice that the angle brackets in this listing are separated from the library that they enclose by a single space. This space should be removed for your project.

'testapp.c' file
Here is our first listing for the 'testapp.c' file. The package name for this project is 'com.example.testapp'. We are working in eclipse. The native methods in the java classes will be called JNIinit(), JNIdraw(), and JNIresize(int w, int h).

#include "androidgl.h"
// C methods go here
 
JNIEXPORT void 
JNICALL Java_com_example_testapp_Panel_JNIinit(JNIEnv * env, jobject  obj)
{
    
        init();
    
}

JNIEXPORT void 
JNICALL Java_com_example_testapp_Panel_JNIdraw(JNIEnv * env, jobject  obj)
{

        draw();
    
    
}

JNIEXPORT void 
JNICALL Java_com_example_testapp_Panel_JNIresize(JNIEnv * env, jobject  obj, jint w, jint h)
{
        resize(w,h);
    
}


This is basically a listing of the methods we use in our JNI -- Java Native Interface. As this listing shows these JNI methods just call the C methods 'draw()', 'init()', and 'resize(int w, int h)'. We will introduce these methods later in this HOWTO.

'androidgl1.h' file
Since the androidgl1.h file is included we will give a listing for it here.

#ifndef ANDROIDGL_H
#define ANDROIDGL_H


#include < jni.h >
#include < android/log.h >
#include < stdio.h >
#include < stdlib.h >
#include < math.h >
#include < pthread.h >
#include < time.h >


#include < GLES/gl.h >
#include < GLES/glext.h >

// include loge and logi definitions
 
#define  LOG_TAG              "testapp-jni"
#define  LOGI(...)  __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

#define TRUE 1
#define FALSE 0 

#define TEX_WIDTH   256
#define TEX_HEIGHT  256

#define TEX_DIMENSION    256
#define BOOL int

#define MY_SCREEN_FRONT 0
#define MY_SCREEN_BACK  1


//////////////////////////////////////////////////////
// function prototype: room for testapp.c
//////////////////////////////////////////////////////

//////////////////////////////////////////////////////
// function prototype: androidgl1.c
//////////////////////////////////////////////////////

void init() ;

void draw();

void resize(int w, int h);


#endif


There is plenty of room in the 'h' file for more method prototypes. GLuint is a special kind of int defined in the OpenGL library. The other important int in this listing is the 'uint16_t' . This is shorthand for 'unsigned int with 16 bits'. All the pixels in our textures will use this type. Later on we will use screen_0 and screen_1 as our textures, so these arrays need to be defined in terms of 'uint16_t' variables.

At the very bottom of our listing you can see the part that's interesting to us right now. It's the 'init()', 'draw()', and 'resize(int w, int h)' prototypes.

androidgl1.c - init() Method
Here we have a listing of the C code in the androidgl1.c file for the init() method.

void init(void)
{
    int tex_width = TEX_WIDTH;
    int tex_height = TEX_HEIGHT;

    glShadeModel(GL_SMOOTH);
    glClearDepthf(1.0f);
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
    
}


In some ways this is simpler than java because we don't have to convert our floats to byte buffers.

androidgl1.c - draw() Method
Here is the draw() method.

void draw() {

    int tex_width = TEX_WIDTH;
    int tex_height = TEX_HEIGHT;

    glClearColor((float) 0.0, (float) 0.5, (float) 0.0, (float) 0.5);

//    glTexImage2D(GL_TEXTURE_2D, 0,
//            GL_RGBA,//
//            tex_width, tex_height,
//            0,
//            GL_RGBA,//
//            GL_UNSIGNED_SHORT_4_4_4_4,//
//            getScreenPointer(MY_SCREEN_FRONT));//screen


    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glEnableClientState(GL_VERTEX_ARRAY);

    glFrontFace(GL_CCW);
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);
    glEnable(GL_TEXTURE_2D);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);

    glTexCoordPointer(2, GL_FLOAT, 0, tex_coords);

    glVertexPointer(3, GL_FLOAT, 0, vertices);
    glDrawElements( GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);

    glDisable(GL_CULL_FACE);

    glLoadIdentity();
    glTranslatef(0,0, - 1.25f) ;

    glFinish();
    glFlush();
    
}


The code is much the same as the code we wrote solely in java. There is some reorganization though. Note that the 'glTexImage2D()' method is commented out. We will use this method later when we have something to use as a texture to display on our one surface. We have included the 'glClearColor()' command to show a green background. This is not strictly necessary, as a black background is usually sufficient. Green does contrast the red background we used before.

androidgl1.c - resize(w,h) Method
This is the method where we stretch the texture over the vertices.

void resize(int w, int h) {

    float w_h_ratio = (float) w/ (float) h; // specifically for vertices
    float h_w_ratio =  3.0f/  4.0f; // specifically for texture
    
    // vertices array
    vertices[0] =  - (w_h_ratio / 2.0f) ;
    vertices[1] = 0.5f; 
    vertices[2] = 0.0f;  // 0, Top Left
    
    vertices[3] =  - (w_h_ratio / 2.0f) ;
    vertices[4] = -0.5f; 
    vertices[5] = 0.0f;  // 1, Bottom Left
    
    vertices[6] = (w_h_ratio / 2.0f) ;
    vertices[7] = -0.5f; 
    vertices[8] = 0.0f;  // 2, Bottom Right
    
    vertices[9] = (w_h_ratio / 2.0f) ;
    vertices[10] = 0.5f; 
    vertices[11] = 0.0f;  // 3, Top Right
    
    // texture coordinates array
    tex_coords[0] = 0.0f;
    tex_coords[1] = 0.0f; //1
    
    tex_coords[2] = 0.0f; 
    tex_coords[3] = h_w_ratio; //2
    
    tex_coords[4] = 1.0f; 
    tex_coords[5] = h_w_ratio; //3
    
    tex_coords[6] = 1.0f; 
    tex_coords[7] = 0.0f; //4
    
    
    glViewport(0, 0, w, h);    
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    //gluPerspective( 45.0f, (float) w/ (float) h, 0.1f, 100.0f);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    glGenTextures(1, &texture_id);

    glBindTexture(GL_TEXTURE_2D, texture_id);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

    screen_width = w;
    screen_height = h;
}


This method is longer. We commented out the method 'gluPerspective()'. It turns out there is no 'gluPerspective()' in the OpenGL ES library for Android phones. To get around that we will introduce our own version of 'gluPerspective()'. We will do that later though. For now we want to compile some code that will run and demonstrate that we can use openGL in C.

Compile
To compile this code we type the following.

$ cd ~/workspace/testapp
$ ndk-build
 


Then you refresh the workspace from within eclipse by pressing the F5 key.

JAVA Native Methods
Here is a partial listing from the Panel.java class. All the code that we wrote before for the java OpenGL test with the red background has been removed. Now there is only the JNI C code manipulating the OpenGL library.

public class Panel implements GLSurfaceView.Renderer {

    Context mContext;
    
    public Panel(Context context) {
        mContext = context;
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        this.JNIdraw();

    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        this.JNIresize(width, height);

    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        this.JNIinit();

    }

    public native void JNIinit();
    public native void JNIdraw();
    public native void JNIresize(int w, int h);
    
    static {
        System.loadLibrary("testapp");
    }
}


This shows the library included in the class.

Screen Shot
Here is a screen shot of the running app on the emulator.



The green section is the background color that we defined using the glClearColor() command. The white section is the object that we've defined in 3D in the 'vertices' array. This object appears as proportional to the window that we create it in. If we resize the GLSurfaceView (the java class 'PanelGLSurfaceView.java') this object will always resize itself to be proportional to the view. Below is the code for the gluPerspective() call.

static void gluPerspective(GLfloat fovy, GLfloat aspect,
               GLfloat zNear, GLfloat zFar)
{
    #define PI 3.1415926535897932f
    GLfloat xmin, xmax, ymin, ymax;

    ymax = zNear * (GLfloat)tan(fovy * PI / 360);
    ymin = -ymax;
    xmin = ymin * aspect;
    xmax = ymax * aspect;

    glFrustumx((GLfixed)(xmin * 65536), (GLfixed)(xmax * 65536),
               (GLfixed)(ymin * 65536), (GLfixed)(ymax * 65536),
               (GLfixed)(zNear * 65536), (GLfixed)(zFar * 65536));
    #undef PI
}


This code is placed in the 'androidgl1.c' file below the include command and above the other previously listed functions. This helps us display the texture as we did in the Java code.

Here is a link for the gluPerspective() command: http://stackoverflow.com/questions/6289970/failed-to-execute-this-c-code-on-android .

Now the screenshot should look like this:



New Texture
Here we're going to start using a new texture. What we've done is resized the old one to 256x256. We've also superimposed some text on the texture. We will use the JNI to display the texture, and we will cut it off at the three-quarter mark. We will be able to see that it is cut off at this point by reading the superimposed text. Here is the new image. The alpha in the image that was part of the texture we used in part 1 of this howto is replaced here with black. We do not try to display red or blue at this time.



Panel.java Constructor
We load the texture into the program in the constructor of the Panel.java class. This constructor has the application context passed to it. The code for the constructor is shown below.

    public Panel(Context context) {
        
        mContext = context;
        
        int [] a = new int [256 * 256];
        BitmapFactory.Options mOptions = new BitmapFactory.Options();
        mOptions.outHeight = 256;
        mOptions.outWidth = 256;
        mOptions.inScaled = false;
        mOptions.inPreferredConfig = Bitmap.Config.ARGB_4444;
        Bitmap mTexture;
        mTexture = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.texture, mOptions);
        mTexture.getPixels(a, 0, 256, 0, 0, 256, 256);
        this.JNIsetTexture(a);
        
    }


testapp.c - JNIsetTexture() Method
Then we change the JNI. We need a method 'JNIsetTexture()'. Before we do that we add the following lines to the top of 'testapp.c' . We could have put these lines in our androidgl.h file. The static declaration allows the c code to access the arrays anywhere from inside the .c file in which they are declared. This means any function in the testapp.c file can access these arrays. If we include these lines in the header file, there will be essentially two arrays for each name, six in all, three of which can be accessed from one file (testapp.c) and three of which can be accessed from the other file (androidgl1.c). We need to pass the address of an array -- whichever one we choose -- to the OpenGL code in the androidgl1.c file. To accomplish this we use the 'getScreenPointer()' method. We define this method in the testapp.c file, and we list it shortly.

static uint16_t screen_0 [TEX_DIMENSION][TEX_DIMENSION];
static uint16_t screen_1 [TEX_DIMENSION][TEX_DIMENSION];
static uint16_t screencounter = 0;

static uint16_t tex_array [TEX_DIMENSION * TEX_DIMENSION];


Then we can define the following JNI function. Here we've setup three ways of storing the texture data. We will load all three arrays with the texture data, then we will use the three devices separately. In the end we will try to implement a double buffering scheme to show how that can be done. For the time being we will only use one of the arrays listed above at a time. The screencounter variable will be used in the double buffering code.

JNIEXPORT void
JNICALL Java_com_example_testapp_Panel_JNIsetTexture(JNIEnv * env, jobject  obj, jintArray a_bitmap)
{

  jint *a = (*env)->GetIntArrayElements(env, a_bitmap, 0);

  copyArraysExpand_1D(a, tex_array);
  copyArraysExpand_2D(a, screen_0);
  copyArraysExpand_2D(a, screen_1);
}


testapp.c - copyArraysExpand() Method
s This code goes in the testapp.c file. We will have to add a method signature to the androidgl.h file so that the compiler can find these two methods when the time comes. The complete androidgl.h file will be shown in a later listing.

void copyArraysExpand_2D(jint from[],  uint16_t to[TEX_HEIGHT][TEX_WIDTH]) {

    int ii,jj, k;
    for (ii = 0; ii< TEX_HEIGHT; ii ++ ) {
        for (jj = 0; jj < TEX_WIDTH; jj ++ ) {
            k =( jj * TEX_WIDTH ) + ii;
            to[jj][ii] = (uint16_t) from[k] ;
        }
    }
    return;
}

void copyArraysExpand_1D(jint from[],  uint16_t to[ TEX_WIDTH * TEX_HEIGHT]) {

    int j;
    for (j = 0; j< TEX_WIDTH*TEX_HEIGHT ; j ++ ) {

        to[j] = (uint16_t)  from[j] ;
    }
    return;
}


Then we have the code to actually hold the texture in memory.

This is the 'getScreenPointer()' method in its first incarnation.

uint16_t **  getScreenPointer(int screen_enum) {
    return (uint16_t **) screen_0;
}


It just returns the value of the address of the array that we are interested in. Right now it returns the address of screen_0. To return the address of screen_1 or tex_array, simply do the replacement in this code. All three should work, even though the screen_0 and screen_1 are 2 D arrays and the tex_array is a 1 D array. The method prototype for this method must be added to the listing of prototypes in the androidgl.h file.

glTexImage2D()
This is the new 'draw()' method. The only modification is that 'glTexImage2D()' has been included. This is part of the 'androidgl1.c' file.

void draw() {

    int tex_width = TEX_WIDTH;
    int tex_height = TEX_HEIGHT;

    glClearColor((float) 0.0, (float) 0.5, (float) 0.0, (float) 0.5);

    glTexImage2D(GL_TEXTURE_2D, 0,
            GL_RGBA,//
            tex_width, tex_height,
            0,
            GL_RGBA,//
            GL_UNSIGNED_SHORT_4_4_4_4,//
            getScreenPointer(0));//screen

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glEnableClientState(GL_VERTEX_ARRAY);

    glFrontFace(GL_CCW);
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);
    glEnable(GL_TEXTURE_2D);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);

    glTexCoordPointer(2, GL_FLOAT, 0, tex_coords);

    glVertexPointer(3, GL_FLOAT, 0, vertices);
    glDrawElements( GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);

    glDisable(GL_CULL_FACE);

    glLoadIdentity();
    glTranslatef(0,0, - 1.25f) ;

    glFinish();
    glFlush();
    
}


This method uses 'getScreenPointer()'.

Screen Shot
In this screen shot the texture is used. The dimension of the shown image is not 256x256 but 256x192. This is the dimension of the game screen for this game. The image is stretched, so displaying a window on the phone's screen that has the right ratio of width-to-height is the job of the Java code, but since the GLSurfaceView is a view, setting that size shouldn't be too hard.



Movement
Next we try to move an object on the screen. In this test app we want to redraw everything in C, but we want that redraw to happen when it is called for by java code. We want, on the other hand, for all our images to be displayed on the screen by OpenGL as soon as the phone is able to do so. We have two program threads, one where we are drawing things and another where we are showing them to the user. You could say that we are 'drawing' when we show the images to the user, but for this test app the distinction is splitting hairs. The OpenGL harware does this drawing, so we won't worry about that kind of drawing.

testapp.c - JNIbuildLevel() Method
In the next code snippets we will show how to draw on the screen in C with a method we'll call 'JNIbuildLevel()'. This method takes one parameter, the x position of the object. It then calls another method to draw the object on the array that will end up being our texture.

JNIEXPORT void 
JNICALL Java_com_example_testapp_Panel_JNIbuildLevel(JNIEnv * env, jobject  obj, jint a)
{
 int ii = 0;

 uint16_t  *  screen = (void *) (getScreenPointer(MY_SCREEN_BACK));

 for (ii = 0; ii < TEX_WIDTH*TEX_HEIGHT; ii ++ ) {
  screen [ii] = tex_array[ii];
 }
 drawBox(a);

    ////////////////////////
    incrementScreenCounter();
}


testapp.c - drawBox() Method
Then we need a method called 'drawBox()'. Both these methods are placed in 'testapp.c'.

void drawBox(int x ) {
    uint16_t  *  screen = (void *) (getScreenPointer(MY_SCREEN_BACK));
    int xx = x;
    int yy = 96;
    int ii = 0;
    int jj = 0;
    uint16_t color = 0x00f0;//blue

    for ( ii = 0; ii < 10; ii ++ ) {
        for (jj = 0; jj < 10; jj ++ ) {
            screen[((yy+jj) * TEX_WIDTH ) + xx + ii] = (uint16_t) color;
        }
    }

}


Then we add a method prototype to our header file, and a listing for JNIbuildLevel() in the 'native' section of our Panel.java class.

//////////////////////////////////////////////////////
// function prototype: room for testapp.c
//////////////////////////////////////////////////////

void incrementScreenCounter() ;

uint16_t **  getScreenPointer(int screen_enum) ;

void drawBox(int a);
Here is the relevant snippet from Panel.java:
    public native void JNIinit();
    public native void JNIdraw();
    public native void JNIresize(int w, int h);
    
    public native void JNIsetTexture(int [] a);
    public native void JNIbuildLevel(int a);
    static {
        System.loadLibrary("testapp");
    }


Then we need to call the code from java from another thread.

Panel.java Constructor
Here is the new constructor for the Panel.java class:

    public Panel(Context context) {
        
        mContext = context;
        
        int [] a = new int [256 * 256];
        BitmapFactory.Options mOptions = new BitmapFactory.Options();
        mOptions.outHeight = 256;
        mOptions.outWidth = 256;
        mOptions.inScaled = false;
        
        mOptions.inPreferredConfig = Bitmap.Config.ARGB_4444;
        
        
        Bitmap mTexture;
        mTexture = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.texture, mOptions);

        mTexture.getPixels(a, 0, 256, 0, 0, 256, 256);
        this.JNIsetTexture(a);
        
        new AsyncTask < Void, Void, Void > (){
            @Override
            protected Void doInBackground(Void... params) {
                while( mLoop == true) {
                    for (int ii = 0; ii < 256 - 10 ; ii = ii + 5 ) {
                        gameSpeedRegulator();
                        JNIbuildLevel(ii);
                    }
                }
                return null;
            }
        }.execute();
        
    }


Panel.java - gameSpeedRegulator() Method
And here is a helper method that is used in the constructor. It's also part of Panel.java. This method helps time the calls to 'JNIbuildLevel()'. This way the calls to the JNI function are spaced out and we can be sure that we see all of the drawn blue squares:

    boolean mLoop = true;
    private long framesPerSec = 15;
    private long skipTicks = 1000 / framesPerSec;
    private long ticksElapsed; 
    private long sleepTime = 0;
    private long nextGameTick = 0;
   
    public boolean gameSpeedRegulator() {
        
        boolean mIsNotLate = true;
        
        Date newDate = new Date();
        ticksElapsed = newDate.getTime();
        nextGameTick += skipTicks;
        sleepTime = nextGameTick - ticksElapsed;
        
        if ( (sleepTime >= 0 && mLoop) || framesPerSec < 0 ) {
        
            //Log.e("Panel GameLoop", "---Passing time");
            try {
                if (framesPerSec > 0) Thread.sleep(sleepTime);
            } catch (InterruptedException e) {
                //
            } 
            mIsNotLate = true;
        }
        else {
            //Log.e("Panel GameLoop", "---Running behind");
            newDate = new Date();
            nextGameTick = newDate.getTime();
            mIsNotLate = false;
        }
        return mIsNotLate;
    }
   


next we redefine the getScreenPointer() method.

testapp.c - getScreenPointer() Method
Here is the new getScreenPointer() class.

uint16_t **  getScreenPointer(int screen_enum) {

    //////// BUFFER SCREEN PART I ///////////////////
    int local_index = 0;
    if (screen_enum == MY_SCREEN_FRONT) {
        local_index = screencounter;
    }
    else if (screen_enum == MY_SCREEN_BACK) {
        local_index = (screencounter + 1) &1;
    }
    ///////// BUFFER SCREEN PART II /////////////////
    if (local_index) {
        return (uint16_t **) screen_0;
    }
    else {
        return (uint16_t **) screen_1;
    }
}


This is C code, part of 'testapp.c'. It is our framebuffer. When we want to draw to the screen we call 'getScreenPointer(MY_SCREEN_BACK)'. When we want to pass a pointer as the texture address, we call 'getScreenPointer(MY_SCREEN_FRONT)'. Whenever we come to the point that we want to finish drawing on the screen and move to the next frame we call 'incrementScreenCounter()'. This way we can draw what we want and there's no tearing.

testapp.c - incrementScreenCounter() Method
This is the code for 'incrementScreenCounter()'.

void incrementScreenCounter() {
    screencounter = (screencounter + 1)& 1;

}


This is it. What we've tried to do is show movement on the screen with a small blue box that moves across. There is no screen tearing because of the frame buffering.

Here is a screen shot.



Most people won't find this outcome particularly interesting. Still there's no tearing.

Problems and Special Considerations
On the emulator this works as shown. On the device that I own the texture is repeated on the screen twice in the 'left-to-right' direction. The texture stretches as it should in the 'top-to-bottom' direction. This should not effect someone who is trying to make a game where all the backgrounds are drawn on the 'screen' variable by code. To stop the 'left-to-right' repeating set the Java line with the line 'Bitmap.Config.ARGB_4444' to 'Bitmap.Config.ARGB_8888' as noted below.

Note, my texture only uses the colors green, black, and white. If red or blue are added to the initial 'texture.png' file the outcome is unimpressive. Those colors don't show up properly from the original ping. If you want to add objects with those colors later in JNI code, this works fine, but not from the original ping.

The problem is rooted in the fact that the android phone uses a native bitmap configuration of 'ARGB_8888'. This means the bitmap word is 32 bits long and Alpha, Red, Green, and Blue data each use up 8 bits. It also means that they are ordered Alpha first, then Red, Green, and finally Blue. The OpenGL context in our example uses 'RGBA_4444'. Obviously there's a difference. You can specify, as we did in our Java, that the system use 'ARGB_4444' when it decodes the bitmap, but there is no option for 'RGBA_4444' as the OpenGL uses, and though there are other options for both OpenGL ES and Android, there's not one set of them that match. You could use C code to shift around the bits in the 'screen' array in the JNI, but this is pretty messy.

For our purposes simply constructing our own content is enough. As I have mentioned the blue square shows up in the right color because it follows the 'RGBA_4444' arrangement of the OpenGL ES code.

Still, as I want my HowTo to be iron clad, I will work on this and possibly edit this document later to show how to preserve the 'left-to-right' texture as having only one repitition and have the colors correct.




What follows is the final screenshot. In this screenshot the 'Bitmap.Config.ARGB_4444' is removed or replaced with 'Bitmap.Config.ARGB_8888'. The colors are incorrect but the size is fine. Again, if you were writing a game where the background were painted entirely by code, this would not be a problem. If you look you can see the blue box is correctly displayed.



Here is the line in the Panel.java constructor that needs to be changed or removed.

        BitmapFactory.Options mOptions = new BitmapFactory.Options();
        mOptions.outHeight = 256;
        mOptions.outWidth = 256;
        mOptions.inScaled = false;
        
        mOptions.inPreferredConfig = Bitmap.Config.ARGB_4444;
        // change or remove this line ^^^


Finally there may be a way to load textures from Java directly, but I have not explored this.

No comments:

Post a Comment