package org.deviceconnect.android.deviceplugin.theta.core;
import android.graphics.Bitmap;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.GLUtils;
import android.opengl.Matrix;
import org.deviceconnect.android.deviceplugin.theta.opengl.model.UVSphere;
import org.deviceconnect.android.deviceplugin.theta.utils.Quaternion;
import org.deviceconnect.android.deviceplugin.theta.utils.Vector3D;
import java.io.ByteArrayOutputStream;
import java.nio.IntBuffer;
import java.util.logging.Logger;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class SphericalViewRenderer implements GLSurfaceView.Renderer {
/**
* Logger.
*/
private final Logger mLogger = Logger.getLogger("theta.dplugin");
/**
* Distance of left and right eye: {@value} cm.
*/
private static final float DISTANCE_EYES = 10.0f / 100.0f;
/**
* Radius of sphere for photo.
*/
private static final float DEFAULT_TEXTURE_SHELL_RADIUS = 1.0f;
/**
* Number of sphere polygon partitions for photo, which must be an even number.
*/
private static final int SHELL_DIVIDES = 40;
private static final String VSHADER_SRC =
"attribute vec4 aPosition;\n" +
"attribute vec2 aUV;\n" +
"uniform mat4 uProjection;\n" +
"uniform mat4 uView;\n" +
"uniform mat4 uModel;\n" +
"varying vec2 vUV;\n" +
"void main() {\n" +
" gl_Position = uProjection * uView * uModel * aPosition;\n" +
" vUV = aUV;\n" +
"}\n";
private static final String FSHADER_SRC =
"precision mediump float;\n" +
"varying vec2 vUV;\n" +
"uniform sampler2D uTex;\n" +
"void main() {\n" +
" gl_FragColor = texture2D(uTex, vUV);\n" +
"}\n";
public static final float Z_NEAR = 0.1f;
public static final float Z_FAR = 1000.0f;
protected int mScreenWidth;
protected int mScreenHeight;
protected boolean mIsStereo;
protected StereoImageType mStereoType = StereoImageType.HALF;
private Camera mCamera = new Camera();
private boolean mFlipVertical;
private UVSphere mShell;
protected Bitmap mTexture;
protected boolean mTextureUpdate = false;
private int[] mTextures = new int[1];
private int mPositionHandle;
private int mProjectionMatrixHandle;
private int mViewMatrixHandle;
private int mUVHandle;
private int mTexHandle;
private int mModelMatrixHandle;
private final float[] mProjectionMatrix = new float[16];
private final float[] mViewMatrix = new float[16];
private final float[] mModelMatrix = new float[16];
private final Object mLockObj = new Object();
private boolean mIsWaitingSnapshot;
private byte[] mSnapshot;
private boolean mIsScreenSizeMutable;
private boolean mIsDestroyTextureOnUpdate;
private SurfaceListener mSurfaceListener;
/**
* Constructor.
*/
public SphericalViewRenderer() {
mShell = new UVSphere(DEFAULT_TEXTURE_SHELL_RADIUS, SHELL_DIVIDES);
}
public void setDestroyTextureOnUpdate(boolean flag) {
mIsDestroyTextureOnUpdate = flag;
}
public void setSurfaceListener(final SurfaceListener listener) {
mSurfaceListener = listener;
}
public void setFlipVertical(final boolean isFlip) {
mFlipVertical = isFlip;
}
public void setStereoImageType(final StereoImageType type) {
mStereoType = type;
}
public byte[] takeSnapshot() {
synchronized (mLockObj) {
mIsWaitingSnapshot = true;
try {
mLockObj.wait();
} catch (InterruptedException e) {
return null;
}
mIsWaitingSnapshot = false;
return mSnapshot;
}
}
public int getOutputWidth() {
return (!mIsStereo || mStereoType == StereoImageType.HALF)
? getScreenWidth() : getScreenWidth() * 2;
}
public int getOutputHeight() {
return getScreenHeight();
}
private void readPixelBuffer() {
int w = getOutputWidth();
int h = getOutputHeight();
int bitmapBuffer[] = new int[w * h];
int bitmapSource[] = new int[w * h];
IntBuffer intBuffer = IntBuffer.wrap(bitmapBuffer);
intBuffer.position(0);
GLES20.glReadPixels(0, 0, w, h, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, intBuffer);
int offset1, offset2;
for (int i = 0; i < h; i++) {
offset1 = i * w;
offset2 = (h - i - 1) * w;
for (int j = 0; j < w; j++) {
int texturePixel = bitmapBuffer[offset1 + j];
int blue = (texturePixel >> 16) & 0xff;
int red = (texturePixel << 16) & 0x00ff0000;
int pixel = (texturePixel & 0xff00ff00) | red | blue;
bitmapSource[offset2 + j] = pixel;
}
}
Bitmap bitmap = Bitmap.createBitmap(bitmapSource, w, h, Bitmap.Config.ARGB_8888);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
mSnapshot = baos.toByteArray();
}
/**
* onDrawFrame Method
*
* @param gl10 GL10 Object
*/
@Override
public void onDrawFrame(final GL10 gl10) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
int width = getOutputWidth();
int height = getOutputHeight();
if (mIsStereo) {
Camera[] cameras = mCamera.getCamerasForStereo(DISTANCE_EYES);
int halfWidth = width / 2;
GLES20.glViewport(0, 0, halfWidth, height);
draw(cameras[0]);
GLES20.glViewport(halfWidth, 0, halfWidth, height);
draw(cameras[1]);
} else {
GLES20.glViewport(0, 0, width, height);
draw(mCamera);
}
synchronized (mLockObj) {
if (mIsWaitingSnapshot) {
readPixelBuffer();
mLockObj.notifyAll();
}
}
}
public void requestToUpdateTexture() {
mTextureUpdate = true;
}
private void draw(final Camera camera) {
Matrix.setIdentityM(mModelMatrix, 0);
Matrix.setIdentityM(mViewMatrix, 0);
Matrix.setIdentityM(mProjectionMatrix, 0);
if (mTextureUpdate && null != mTexture && !mTexture.isRecycled()) {
if (mTextures[0] != 0) {
GLES20.glDeleteTextures(1, mTextures, 0);
}
loadTexture(mTexture);
mTextureUpdate = false;
}
float x = camera.getPosition().x();
float y = camera.getPosition().y();
float z = camera.getPosition().z();
float frontX = camera.getFrontDirection().x();
float frontY = camera.getFrontDirection().y();
float frontZ = camera.getFrontDirection().z();
float upX = camera.getUpperDirection().x();
float upY = camera.getUpperDirection().y();
float upZ = camera.getUpperDirection().z();
float fov = camera.mFovDegree;
if (mFlipVertical) {
frontY *= -1;
}
Matrix.setLookAtM(mViewMatrix, 0, x, y, z, frontX, frontY, frontZ, upX, upY, upZ);
Matrix.perspectiveM(mProjectionMatrix, 0, fov, getScreenAspect(), Z_NEAR, Z_FAR);
GLES20.glUniformMatrix4fv(mModelMatrixHandle, 1, false, mModelMatrix, 0);
GLES20.glUniformMatrix4fv(mProjectionMatrixHandle, 1, false, mProjectionMatrix, 0);
GLES20.glUniformMatrix4fv(mViewMatrixHandle, 1, false, mViewMatrix, 0);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextures[0]);
GLES20.glUniform1i(mTexHandle, 0);
mShell.draw(mPositionHandle, mUVHandle);
}
/**
* onSurfaceChanged Method
* @param gl10 GLObject (not used)
* @param width Screen width
* @param height Screen height
*/
@Override
public void onSurfaceChanged(final GL10 gl10, final int width, final int height) {
if (!isScreenSizeMutable()) {
mScreenWidth = width;
mScreenHeight = height;
}
}
/**
* onSurfaceCreated Method
* @param gl10 GLObject
* @param config EGL Setting Object
*/
@Override
public void onSurfaceCreated(final GL10 gl10, final EGLConfig config) {
int vShader = loadShader(GLES20.GL_VERTEX_SHADER, VSHADER_SRC);
int fShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FSHADER_SRC);
int program = GLES20.glCreateProgram();
GLES20.glAttachShader(program, vShader);
GLES20.glAttachShader(program, fShader);
GLES20.glLinkProgram(program);
GLES20.glUseProgram(program);
mPositionHandle = GLES20.glGetAttribLocation(program, "aPosition");
mUVHandle = GLES20.glGetAttribLocation(program, "aUV");
mProjectionMatrixHandle = GLES20.glGetUniformLocation(program, "uProjection");
mViewMatrixHandle = GLES20.glGetUniformLocation(program, "uView");
mTexHandle = GLES20.glGetUniformLocation(program, "uTex");
mModelMatrixHandle = GLES20.glGetUniformLocation(program, "uModel");
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}
private float getScreenAspect() {
int width = mIsStereo && mStereoType == StereoImageType.HALF ? mScreenWidth / 2 : mScreenWidth;
return (float) width / (float) (mScreenHeight == 0 ? 1 : mScreenHeight);
}
/**
* Sets the texture for the sphere
*
* @param texture Photo object for texture
*/
public void setTexture(final Bitmap texture) {
if (mTexture != null && mIsDestroyTextureOnUpdate) {
try {
GLES20.glDeleteTextures(1, mTextures, 0);
//checkGlError("AAA", "glDeleteTextures");
mTexture.recycle();
} catch (Throwable e) {
e.printStackTrace();
}
}
mTexture = texture;
mTextureUpdate = true;
}
/**
* Acquires the set texture
*
* @return Photo object for texture
*/
public Bitmap getTexture() {
return mTexture;
}
/**
* GL error judgment method for debugging
* @param glOperation Message output character string
*/
private void checkGlError(final String glOperation) {
int error;
while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
mLogger.warning(glOperation + ": glError " + error);
throw new RuntimeException(glOperation + ": glError " + error);
}
}
/**
* Texture setting method
*
* @param texture Setting texture
*/
public void loadTexture(final Bitmap texture) {
GLES20.glGenTextures(1, mTextures, 0);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextures[0]);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, texture, 0);
}
private int loadShader(final int type, final String shaderCode) {
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
public int getScreenWidth() {
return mScreenWidth;
}
public int getScreenHeight() {
return mScreenHeight;
}
public boolean isStereo() {
return mIsStereo;
}
public void setScreenSettings(final int width, final int height, final boolean isStereo) {
boolean isChanged = false;
if (isScreenSizeMutable()) {
if (mScreenWidth != width || mScreenHeight != height) {
isChanged = true;
}
mScreenWidth = width;
mScreenHeight = height;
}
if (mIsStereo != isStereo) {
isChanged = true;
}
mIsStereo = isStereo;
if (isChanged && mSurfaceListener != null) {
mSurfaceListener.onSurfaceChanged(width, height, isStereo);
}
}
public void setScreenSizeMutable(final boolean isMutable) {
mIsScreenSizeMutable = isMutable;
}
public boolean isScreenSizeMutable() {
return mIsScreenSizeMutable;
}
public void setSphereRadius(final float radius) {
if (radius != mShell.getRadius()) {
mShell = new UVSphere(radius, SHELL_DIVIDES);
}
}
public Camera getCamera() {
return mCamera;
}
public void setCamera(final Camera camera) {
mCamera = camera;
}
public static class CameraBuilder {
private float mFovDegree;
private Vector3D mPosition;
private Vector3D mFrontDirection;
private Vector3D mUpperDirection;
private Vector3D mRightDirection;
private Quaternion mAttitude;
public CameraBuilder(final Camera camera) {
mFovDegree = camera.mFovDegree;
mPosition = new Vector3D(camera.mPosition);
mFrontDirection = new Vector3D(camera.mFrontDirection);
mUpperDirection = new Vector3D(camera.mUpperDirection);
mRightDirection = new Vector3D(camera.mRightDirection);
if (camera.mAttitude != null) {
mAttitude = new Quaternion(camera.mAttitude);
}
}
public CameraBuilder() {
this(new Camera());
}
public Camera create() {
Camera camera = new Camera(mFovDegree, mPosition,
mFrontDirection,
mUpperDirection,
mRightDirection,
mAttitude);
return camera;
}
public void setFov(float degree) {
mFovDegree = degree;
}
public void setPosition(final Vector3D p) {
mPosition = p;
}
public void slideHorizontal(final float delta) {
mPosition = new Vector3D(
delta * mRightDirection.x() + mPosition.x(),
delta * mRightDirection.y() + mPosition.y(),
delta * mRightDirection.z() + mPosition.z()
);
}
public void rotateByEulerAngle(final float roll, final float yaw, final float pitch) {
Vector3D lastFrontDirection = mFrontDirection;
float lat = (90.0f - pitch);
float lng = yaw;
float x = (float) (Math.sin(lat) * Math.cos(lng));
float y = (float) (Math.cos(lat));
float z = (float) (Math.sin(lat) * Math.sin(lng));
mFrontDirection = new Vector3D(x, y, z);
float dx = mFrontDirection.x() - lastFrontDirection.x();
float dy = mFrontDirection.y() - lastFrontDirection.y();
float dz = mFrontDirection.z() - lastFrontDirection.z();
float theta = roll;
Quaternion q = new Quaternion(
(float) Math.cos(theta / 2.0f),
mFrontDirection.multiply((float) Math.sin(theta / 2.0f))
);
mUpperDirection = rotate(mUpperDirection, q);
mRightDirection = mRightDirection.add(new Vector3D(dx, dy, dz));
mRightDirection = rotate(mRightDirection, q);
}
public void rotate(final Quaternion q) {
mFrontDirection = rotate(new Vector3D(1, 0, 0), q);
// mUpperDirection = rotate(new Vector3D(0, 1, 0), q);
mRightDirection = rotate(new Vector3D(0, 0, 1), q);
}
private static Vector3D rotate(final Vector3D v, final Quaternion q) {
Quaternion p = new Quaternion(0, v);
Quaternion r = q.conjugate();
Quaternion qpr = r.multiply(p).multiply(q);
return qpr.imaginary();
}
}
public static class Camera {
private final float mFovDegree;
private final Vector3D mPosition;
private final Vector3D mFrontDirection;
private final Vector3D mUpperDirection;
private final Vector3D mRightDirection;
private final Quaternion mAttitude;
public Camera(final float fovDegree, final Vector3D position,
final Vector3D frontDirection, final Vector3D upperDirection,
final Vector3D rightDirection,
final Quaternion attitude) {
mFovDegree = fovDegree;
mPosition = position;
mFrontDirection = frontDirection;
mUpperDirection = upperDirection;
mRightDirection = rightDirection;
mAttitude = attitude;
}
public Camera() {
this(90, new Vector3D(0.0f, 0.0f, 0.0f),
new Vector3D(1.0f, 0.0f, 0.0f),
new Vector3D(0.0f, 1.0f, 0.0f),
new Vector3D(0.0f, 0.0f, 1.0f),
Quaternion.quaternionFromAxisAndAngle(new Vector3D(1.0f, 0.0f, 0.0f), 0));
}
public Vector3D getPosition() {
return mPosition;
}
public Vector3D getFrontDirection() {
return mFrontDirection;
}
public Vector3D getUpperDirection() {
return mUpperDirection;
}
public Vector3D getRightDirection() {
return mRightDirection;
}
public Camera[] getCamerasForStereo(final float distance) {
CameraBuilder leftCamera = new CameraBuilder(this);
leftCamera.slideHorizontal(-1 * (distance / 2.0f));
CameraBuilder rightCamera = new CameraBuilder(this);
rightCamera.slideHorizontal((distance / 2.0f));
return new Camera[] {
leftCamera.create(),
rightCamera.create()
};
}
}
public interface SurfaceListener {
void onSurfaceChanged(final int width, final int height, final boolean isStereo);
}
public enum StereoImageType {
HALF,
DOUBLE
}
}