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):
But not when panning on latitude (vertically):
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):
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