Search code examples
androidopengl-estextures

Android OpenGL ES textured half sphere


I have to develop an equirectangular image viewer, like the one of the Ricoh Theta app.

I'm doing it on Android, with Open GL ES (1.0, but I can change to 2.0 if needed).

For now, I have managed to create the half sphere (based on this answer), with this code:

public class HalfSphere {

    // ---------------------------------------------------------------------------------------------
    // region Attributes

    private final int[] mTextures = new int[1];

    float[][] mVertices;
    int mNbStrips;
    int mNbVerticesPerStrips;

    private final List<FloatBuffer> mVerticesBuffer = new ArrayList<>();
    private final List<ByteBuffer> mIndicesBuffer = new ArrayList<>();
    private final List<FloatBuffer> mTextureBuffer = new ArrayList<>();

    // endregion
    // ---------------------------------------------------------------------------------------------



    // ---------------------------------------------------------------------------------------------
    // region Constructor

    public HalfSphere(int nbStrips, int nbVerticesPerStrips, float radius) {

        // Generate the vertices:
        mNbStrips = nbStrips;
        mNbVerticesPerStrips = nbVerticesPerStrips;
        mVertices = new float[mNbStrips * mNbVerticesPerStrips][3];

        for (int i = 0; i < mNbStrips; i++) {

            for (int j = 0; j < mNbVerticesPerStrips; j++) {

                mVertices[i * mNbVerticesPerStrips + j][0] = (float) (radius * Math.cos(j * 2 * Math.PI / mNbVerticesPerStrips) * Math.cos(i * Math.PI / mNbStrips));
                mVertices[i * mNbVerticesPerStrips + j][1] = (float) (radius * Math.sin(i * Math.PI / mNbStrips));
                mVertices[i * mNbVerticesPerStrips + j][2] = (float) (radius * Math.sin(j * 2 * Math.PI / mNbVerticesPerStrips) * Math.cos(i * Math.PI / mNbStrips));
            }
        }

        // Populate the buffers:
        for(int i = 0; i < mNbStrips - 1; i++) {

            for(int j = 0; j < mNbVerticesPerStrips; j++) {

                byte[] indices = {
                        0, 1, 2,  // first triangle (bottom left - top left - top right)
                        0, 2, 3   // second triangle (bottom left - top right - bottom right)
                };

                float[] p1 = mVertices[i * mNbVerticesPerStrips + j];
                float[] p2 = mVertices[i * mNbVerticesPerStrips + (j + 1) % mNbVerticesPerStrips];
                float[] p3 = mVertices[(i + 1) * mNbVerticesPerStrips + (j + 1) % mNbVerticesPerStrips];
                float[] p4 = mVertices[(i + 1) * mNbVerticesPerStrips + j];

                float[] quad = {
                        p1[0], p1[1], p1[2],
                        p2[0], p2[1], p2[2],
                        p3[0], p3[1], p3[2],
                        p4[0], p4[1], p4[2]
                };

                mVerticesBuffer.add(floatArrayToFloatBuffer(quad));
                mTextureBuffer.add(floatArrayToFloatBuffer(quad));
                mIndicesBuffer.add(byteArrayToByteBuffer(indices));
            }
        }
    }

    // endregion
    // ---------------------------------------------------------------------------------------------



    // ---------------------------------------------------------------------------------------------
    // region Draw

    public void draw(final GL10 gl) {

        // bind the previously generated texture.
        gl.glBindTexture(GL10.GL_TEXTURE_2D, this.mTextures[0]);

        // Point to our buffers.
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

        // Set the face rotation, clockwise in this case.
        gl.glFrontFace(GL10.GL_CW);

        for(int i = 0; i < mVerticesBuffer.size(); i++) {

            gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mVerticesBuffer.get(i));
            gl.glTexCoordPointer(3, GL10.GL_FLOAT, 0, mTextureBuffer.get(i));

            gl.glDrawElements(GL10.GL_TRIANGLE_STRIP, 6, GL10.GL_UNSIGNED_BYTE, mIndicesBuffer.get(i)); // GL_TRIANGLE_STRIP / GL_LINE_LOOP
        }

        // Disable the client state before leaving.
        gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
        gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
    }

    // endregion
    // ---------------------------------------------------------------------------------------------



    // ---------------------------------------------------------------------------------------------
    // region Utils

    public void loadGLTexture(GL10 gl, Bitmap texture) {
        // Generate one texture pointer, and bind it to the texture array.
        gl.glGenTextures(1, this.mTextures, 0);
        gl.glBindTexture(GL10.GL_TEXTURE_2D, this.mTextures[0]);

        // Create nearest filtered texture.
        gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
        gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);

        // Use Android GLUtils to specify a two-dimensional texture image from our bitmap.
        GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, texture, 0);
        texture.recycle();
    }

    public FloatBuffer floatArrayToFloatBuffer(float[] array) {

        ByteBuffer vbb = ByteBuffer.allocateDirect(array.length * 4);
        vbb.order(ByteOrder.nativeOrder());    // use the device hardware's native byte order
        FloatBuffer fb = vbb.asFloatBuffer();  // create a floating point buffer from the ByteBuffer
        fb.put(array);    // add the coordinates to the FloatBuffer
        fb.position(0);      // set the buffer to read the first coordinate

        return fb;
    }

    public ByteBuffer byteArrayToByteBuffer(byte[] array) {

        ByteBuffer vbb = ByteBuffer.allocateDirect(array.length * 4);
        vbb.order(ByteOrder.nativeOrder());    // use the device hardware's native byte order
        vbb.put(array);    // add the coordinates to the FloatBuffer
        vbb.position(0);      // set the buffer to read the first coordinate

        return vbb;
    }

    // endregion
    // ---------------------------------------------------------------------------------------------
}

Of course, the texture is not applied correctly, as I'm using the coordinates of my vertices. Does someone see how to do it correctly? I'll also need to be able to "move" the texture when the user pan.

EDIT: as suggested by codetiger, doing lat/180 and lon/360, and then normalizing to [0..1] worked. Now, I'm trying to add the panning. It works when panning on longitude (horizontally):

longitude panning

But not when panning on latitude (vertically):

enter image description here

I'm simply adding values between 0..1 when the user pans. I tried to use the formula given here with no success. Any idea?

If it helps, that's what I want (obtained with the Ricoh Theta app):

enter image description here


Solution

  • In order to make the sphere a full 360 degree sphere, you can replace the lines below.

    mVertices[i * mNbVerticesPerStrips + j][0] = (float) (radius * Math.cos(j * 2 * Math.PI / mNbVerticesPerStrips) * Math.cos(2 * i * Math.PI / mNbStrips));
    mVertices[i * mNbVerticesPerStrips + j][1] = (float) (radius * Math.sin(2 * i * Math.PI / mNbStrips));
    mVertices[i * mNbVerticesPerStrips + j][2] = (float) (radius * Math.sin(j * 2 * Math.PI / mNbVerticesPerStrips) * Math.cos(2 * i * Math.PI / mNbStrips));
    

    The only change is using 2 * Math.PI / mNbStrips for second angle instead of Math.PI / mNbStrips

    And to rotate the image, you can rotate the sphere by using

    gl.glRotatef(angle, 1.0f, 0.0f, 0.0f);
    

    Update: To get correct Texture Coordinates for the sphere, for standard distortion sphere texture you can use (lat/180, lon/360) and normalise it to get [0..1]. As mentioned here https://stackoverflow.com/a/10395141/409315