/*
* Copyright (C) 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.apps.santatracker.games.simpleengine;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.RectF;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import android.opengl.Matrix;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.ArrayList;
public final class Renderer {
public static final int DIM_HEIGHT = BitmapTextureMaker.DIM_HEIGHT;
public static final int DIM_WIDTH = BitmapTextureMaker.DIM_WIDTH;
// for getRelativePos()
public static final int REL_CENTER = 0;
public static final int REL_LEFT = 1;
public static final int REL_TOP = 2;
public static final int REL_RIGHT = 3;
public static final int REL_BOTTOM = 4;
private boolean mInitDone = false;
private ArrayList<Sprite> mSprites = new ArrayList<Sprite>(64);
private ArrayList<Sprite> mSpriteRecycleBin = new ArrayList<Sprite>(64);
// Non null only when we are currently loading bitmaps. So this will be null
// if and only if we have all the requested bitmaps ready as textures.
private BitmapTextureMaker mBitmapTextureMaker = null;
// Non null only when we are currently making text textures.
private TextTextureMaker mTextTextureMaker = null;
// current surface dimensions
int mSurfWidth = 0;
int mSurfHeight = 0;
// coordinate system bounds
RectF mBounds = new RectF();
// information about each texture requested
private ArrayList<TexInfo> mTexInfo = new ArrayList<TexInfo>();
private class TexInfo {
int glTex = 0; // OpenGL texture handle, if loaded; 0 if not yet loaded
float aspect; // aspect ratio (width/height), computed when texture is loaded
int width, height; // computed when texture is loaded
// texture request parameters
static final int TYPE_IMAGE = 0;
static final int TYPE_TEXT = 1;
int type = TYPE_IMAGE;
int resId; // (for TYPE_IMAGE, it's a drawable; for TYPE_TEXT, it's a string res id)
String name;
// for TYPE_IMAGE only:
int dimType;
float maxDim;
// for TYPE_TEXT only:
float fontSize;
int textAnchor = TEXT_ANCHOR_CENTER | TEXT_ANCHOR_MIDDLE;
int textColor = 0xffffffff;
}
public static final int TEXT_ANCHOR_CENTER = 0x00;
public static final int TEXT_ANCHOR_LEFT = 0x01;
public static final int TEXT_ANCHOR_RIGHT = 0x02;
private static final int TEXT_ANCHOR_HORIZ_MASK = 0x0f;
public static final int TEXT_ANCHOR_TOP = 0x10;
public static final int TEXT_ANCHOR_BOTTOM = 0x20;
public static final int TEXT_ANCHOR_MIDDLE = 0x00;
private static final int TEXT_ANCHOR_VERT_MASK = 0xf0;
// locations of attributes and uniforms in our shader
private int mLocMatrix = -1;
private int mLocColor = -1;
private int mLocTintFactor = -1;
private int mLocSampler = -1;
private int mLocPosition = -1;
private int mLocTexCoord = -1;
// quad data
private static float[] QUAD_GEOM = { // screenX, screenY, z, u, v
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.0f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
};
private final int SIZEOF_FLOAT = 4;
private final int QUAD_GEOM_VERTEX_COUNT = 4;
private final int QUAD_GEOM_STRIDE = 5 * SIZEOF_FLOAT;
private final int QUAD_GEOM_POS_OFFSET = 0;
private final int QUAD_GEOM_TEXCOORD_OFFSET = 3;
FloatBuffer mQuadGeomBuf = null;
// projection matrix
float[] mProjMat = null;
// temp working matrices
float[] mTmpMatA = new float[16];
float[] mTmpMatB = new float[16];
// color used to clear the screen
private static final int DEFAULT_CLEAR_COLOR = 0xffff0000;
Renderer() {
}
public void onGLSurfaceCreated(Context ctx) {
initGL();
refreshTextures(ctx);
mInitDone = true;
}
private int compileShader(int type, String source) {
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, source);
GLES20.glCompileShader(shader);
return shader;
}
private int linkProgram(int vertShader, int fragShader) {
int program = GLES20.glCreateProgram();
GLES20.glAttachShader(program, vertShader);
GLES20.glAttachShader(program, fragShader);
GLES20.glLinkProgram(program);
return program;
}
private void initGL() {
Logger.d("Initializing OpenGL");
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
Logger.d("Compiling vertex shader.");
int vertShader = compileShader(GLES20.GL_VERTEX_SHADER, ShaderSource.VERTEX_SHADER);
Logger.d("Vertex shader is " + vertShader);
Logger.d("Vertex shader compilation log: " + GLES20.glGetShaderInfoLog(vertShader));
int fragShader = compileShader(GLES20.GL_FRAGMENT_SHADER, ShaderSource.FRAG_SHADER);
Logger.d("Fragment shader is " + fragShader);
Logger.d("Fragment shader compilation log: " + GLES20.glGetShaderInfoLog(fragShader));
int program = linkProgram(vertShader, fragShader);
Logger.d("Program is " + program);
Logger.d("Program linking log: " + GLES20.glGetProgramInfoLog(program));
Logger.d("Activating shader.");
GLES20.glUseProgram(program);
// get locations
mLocMatrix = GLES20.glGetUniformLocation(program, "u_Matrix");
mLocColor = GLES20.glGetUniformLocation(program, "u_Color");
mLocTintFactor = GLES20.glGetUniformLocation(program, "u_TintFactor");
mLocSampler = GLES20.glGetUniformLocation(program, "u_Sampler");
mLocPosition = GLES20.glGetAttribLocation(program, "a_Position");
mLocTexCoord = GLES20.glGetAttribLocation(program, "a_TexCoord");
Logger.d("Locations: " +
"mLocMatrix=" + mLocMatrix + "; " +
"mLocColor=" + mLocColor + "; " +
"mLocTintFactor=" + mLocTintFactor + "; " +
"mLocSampler=" + mLocSampler + "; " +
"mLocPosition=" + mLocPosition + "; " +
"mLocTexCoord=" + mLocTexCoord);
ByteBuffer bb = ByteBuffer.allocateDirect(SIZEOF_FLOAT * QUAD_GEOM.length);
bb.order(ByteOrder.nativeOrder());
mQuadGeomBuf = bb.asFloatBuffer();
mQuadGeomBuf.put(QUAD_GEOM);
mQuadGeomBuf.position(0);
// set up opengl blending
GLES20.glEnable(GLES20.GL_BLEND);
// we use GL_ONE instead of GL_SRC_ALPHA because Android premultiplies
// the alpha channel in the PNG by r,g,b, so if we use GL_SRC_ALPHA, we get
// a gray halo around things.
GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);
}
private void pushTex(int tex) {
GLES20.glUniform1i(mLocSampler, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, tex);
GLES20.glBindTexture(GLES20.GL_TEXTURE0, tex);
}
private void pushColor(float r, float g, float b, float a, float factor) {
GLES20.glUniform4f(mLocColor, r, g, b, a);
GLES20.glUniform1f(mLocTintFactor, factor);
}
private void drawQuad(float centerX, float centerY, float width, float height,
float rotation) {
// compute final matrix
float[] modelViewM = mTmpMatA;
float[] finalM = mTmpMatB;
Matrix.setIdentityM(modelViewM, 0);
Matrix.translateM(modelViewM, 0, centerX, centerY, 0.0f);
Matrix.rotateM(modelViewM, 0, rotation, 0.0f, 0.0f, 1.0f);
Matrix.scaleM(modelViewM, 0, width, height, 1.0f);
Matrix.multiplyMM(finalM, 0, mProjMat, 0, modelViewM, 0);
// push matrix
GLES20.glUniformMatrix4fv(mLocMatrix, 1, false, finalM, 0);
// push positions
GLES20.glEnableVertexAttribArray(mLocPosition);
mQuadGeomBuf.position(QUAD_GEOM_POS_OFFSET);
GLES20.glVertexAttribPointer(mLocPosition, 3, GLES20.GL_FLOAT, false,
QUAD_GEOM_STRIDE, mQuadGeomBuf);
// push texture coordinates
GLES20.glEnableVertexAttribArray(mLocTexCoord);
mQuadGeomBuf.position(QUAD_GEOM_TEXCOORD_OFFSET);
GLES20.glVertexAttribPointer(mLocTexCoord, 2, GLES20.GL_FLOAT, false,
QUAD_GEOM_STRIDE, mQuadGeomBuf);
// draw
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, QUAD_GEOM_VERTEX_COUNT);
}
void calcCoordSystemBounds(int surfWidth, int surfHeight, RectF outBounds) {
if (surfWidth > surfHeight) {
// landscape orientation -- height is set to 1.0, width is proportional
outBounds.right = (surfWidth / (float) surfHeight) * 0.5f;
outBounds.left = -outBounds.right;
outBounds.top = 0.5f;
outBounds.bottom = -0.5f;
} else {
// portrait orientation -- width is set to 1.0, height is proportional
outBounds.top = (surfWidth / (float) surfHeight) * 0.5f;
outBounds.bottom = -outBounds.right;
outBounds.right = 0.5f;
outBounds.left = -0.5f;
}
}
public final float convertScreenX(float screenX) {
return mBounds.left + (screenX / mSurfWidth) * (mBounds.right - mBounds.left);
}
public final float convertScreenY(float screenY) {
float factor = 1 - (screenY / mSurfHeight);
return mBounds.bottom + factor * (mBounds.top - mBounds.bottom);
}
public final float convertScreenDeltaX(float deltaX) {
return convertScreenX(deltaX) - convertScreenX(0.0f);
}
public final float convertScreenDeltaY(float deltaY) {
return convertScreenY(deltaY) - convertScreenY(0.0f);
}
void onGLSurfaceChanged(int width, int height) {
Logger.d("Renderer.onGLSurfaceChanged " + width + "x" + height);
GLES20.glViewport(0, 0, width, height);
mSurfHeight = height;
mSurfWidth = width;
// calculate bounds for our coordinate system
calcCoordSystemBounds(mSurfWidth, mSurfHeight, mBounds);
// set up projection matrix
mProjMat = new float[16];
Matrix.orthoM(mProjMat, 0, mBounds.left, mBounds.right, mBounds.bottom,
mBounds.top, -1.0f, 1.0f);
}
void generateImageTextures() {
int count = mBitmapTextureMaker.getBitmapCount();
int i;
for (i = 0; i < count; i++) {
int texIndex = mBitmapTextureMaker.getTag(i);
generateTexture(texIndex, mBitmapTextureMaker.getBitmap(i));
}
mBitmapTextureMaker.dispose();
mBitmapTextureMaker = null;
}
void generateTextTextures() {
int count = mTextTextureMaker.getCount();
int i;
for (i = 0; i < count; i++) {
int texIndex = mTextTextureMaker.getTag(i);
generateTexture(texIndex, mTextTextureMaker.getBitmap(i));
}
mTextTextureMaker.dispose();
mTextTextureMaker = null;
}
private void generateTexture(int texIndex, Bitmap bmp) {
int[] texH = new int[1];
GLES20.glGenTextures(1, texH, 0);
TexInfo ti = mTexInfo.get(texIndex);
bitmapToGLTexture(texH[0], bmp);
ti.glTex = texH[0];
ti.width = bmp.getWidth();
ti.height = bmp.getHeight();
ti.aspect = bmp.getWidth() / (float) bmp.getHeight();
}
void bitmapToGLTexture(int texH, Bitmap bmp) {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texH);
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);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bmp, 0);
}
// returns true if all resources are ready, false if still loading
boolean prepareFrame() {
if (mBitmapTextureMaker != null && mBitmapTextureMaker.isFinishedLoading()) {
// bitmap bank finished loading bitmaps -- time to convert them to textures!
generateImageTextures();
} else if (mTextTextureMaker != null && mTextTextureMaker.isFinishedLoading()) {
// text texture generator finished -- time to convert into textures!
generateTextTextures();
}
// we are ready if and only if there are no pending images/text to load
return mBitmapTextureMaker == null && mTextTextureMaker == null;
}
private void parseColor(int color, float[] out) {
long c = color;
out[0] = ((c & 0x00ff0000L) >>> 16) / 255.0f;
out[1] = ((c & 0x0000ff00L) >>> 8) / 255.0f;
out[2] = (c & 0x000000ffL) / 255.0f;
out[3] = ((c & 0xff000000L) >>> 24) / 255.0f;
// our setup requires that we premultiply the alpha. This is because we're using
// blending mode glBlend(GL_ONE, GL_ONE_MINUS_SRC_ALPHA), to be compatible with
// how GLUtils loads PNGs.
out[0] *= out[3];
out[1] *= out[3];
out[2] *= out[3];
}
private float[] mTmpColor = new float[4];
public void doFrame() {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
if (mBitmapTextureMaker != null || mTextTextureMaker != null) {
// we are still loading textures, so don't render any sprites yet
return;
}
int i, size = mSprites.size();
for (i = 0; i < size; i++) {
Sprite s = mSprites.get(i);
drawSprite(s);
}
}
private boolean drawSprite(Sprite s) {
if (!s.enabled) {
return false;
}
float tintFactor = s.tintFactor;
TexInfo ti = null;
if (s.texIndex >= 0 && s.texIndex < mTexInfo.size()) {
ti = mTexInfo.get(s.texIndex);
pushTex(ti.glTex);
} else {
pushTex(0);
tintFactor = 1.0f;
}
parseColor(s.color, mTmpColor);
pushColor(mTmpColor[0], mTmpColor[1], mTmpColor[2], mTmpColor[3], tintFactor);
float width = s.width, height = s.height, x = s.x, y = s.y;
if (ti != null && ti.type == TexInfo.TYPE_IMAGE) {
width = s.width;
height = s.height;
if (Float.isNaN(width) && ti != null) {
// auto calculate width based on texture aspect ratio
width = s.width = s.height * ti.aspect;
}
if (Float.isNaN(height) && ti != null) {
// auto calculate height based on texture aspect ratio
height = s.height = s.width / ti.aspect;
}
} else if (ti != null && ti.type == TexInfo.TYPE_TEXT) {
// text images don't respect width/height -- they render at whatever
// size they were created, in order to respect the originally requested font size
width = pixelsToLogical(ti.width);
height = pixelsToLogical(ti.height);
// adjust x,y according to text anchor parameter
int horizAnchor = ti.textAnchor & TEXT_ANCHOR_HORIZ_MASK;
int vertAnchor = ti.textAnchor & TEXT_ANCHOR_VERT_MASK;
if (horizAnchor == TEXT_ANCHOR_LEFT) {
x += width * 0.5f;
} else if (horizAnchor == TEXT_ANCHOR_RIGHT) {
x -= width * 0.5f;
}
if (vertAnchor == TEXT_ANCHOR_TOP) {
y += height * 0.5f;
} else if (vertAnchor == TEXT_ANCHOR_BOTTOM) {
y -= height * 0.5f;
}
}
drawQuad(x, y, width, height, s.rotation);
return true;
}
private float pixelsToLogical(int pixels) {
float logicalWidth = mBounds.width();
float pixelsWidth = mSurfWidth;
return (pixels / (float) pixelsWidth) * logicalWidth;
}
public int requestImageTex(int resId, String name, int dimType, float maxDim) {
TexInfo ti = new TexInfo();
ti.type = TexInfo.TYPE_IMAGE;
ti.resId = resId;
ti.name = name;
ti.dimType = dimType;
ti.maxDim = maxDim;
ti.glTex = 0; // loading
ti.aspect = Float.NaN; // unknown for now
mTexInfo.add(ti);
return mTexInfo.size() - 1;
}
public int requestTextTex(int resId, String name, float fontSize, int textAnchor, int color) {
TexInfo ti = new TexInfo();
ti.type = TexInfo.TYPE_TEXT;
ti.resId = resId;
ti.fontSize = fontSize;
ti.glTex = 0; // loading
ti.aspect = Float.NaN; // unknown for now
ti.textAnchor = textAnchor;
ti.textColor = color;
mTexInfo.add(ti);
return mTexInfo.size() - 1;
}
public int requestTextTex(int resId, String name, float fontSize) {
return requestTextTex(resId, name, fontSize,
TEXT_ANCHOR_CENTER | TEXT_ANCHOR_MIDDLE, 0xffffffff);
}
public float getLeft() {
return mBounds.left;
}
public float getRight() {
return mBounds.right;
}
public float getTop() {
return mBounds.top;
}
public float getBottom() {
return mBounds.bottom;
}
public float getWidth() {
return mBounds.width();
}
public float getHeight() {
// height() doesn't work because in our coord system top > bottom,
// and RectF doesn't like that.
return mBounds.top - mBounds.bottom;
}
public void deleteTextures() {
int[] arr = new int[1];
for (TexInfo ti : mTexInfo) {
if (ti.glTex > 0) {
arr[0] = ti.glTex;
GLES20.glDeleteTextures(1, arr, 0);
ti.glTex = 0;
}
}
mTexInfo.clear();
}
public void reset() {
deleteTextures();
deleteSprites();
}
private void refreshTextures(Context ctx) {
if (mTexInfo.size() > 0) {
for (TexInfo ti : mTexInfo) {
ti.glTex = 0;
}
startLoadingTexs(ctx);
}
}
void startLoadingTexs(Context ctx) {
mBitmapTextureMaker = new BitmapTextureMaker();
mTextTextureMaker = new TextTextureMaker();
int i = 0;
for (i = 0; i < mTexInfo.size(); i++) {
TexInfo ti = mTexInfo.get(i);
if (ti.type == TexInfo.TYPE_IMAGE) {
mBitmapTextureMaker.request(i, ti.resId, ti.name, ti.dimType, ti.maxDim);
} else if (ti.type == TexInfo.TYPE_TEXT) {
mTextTextureMaker.requestTex(i, ctx.getString(ti.resId), ti.fontSize, ti.textColor);
}
}
// start loading the textures in a background thread
mBitmapTextureMaker.startLoading(ctx);
mTextTextureMaker.startLoading(ctx);
}
void dispose() {
if (mBitmapTextureMaker != null) {
mBitmapTextureMaker.dispose();
mBitmapTextureMaker = null;
}
if (mTextTextureMaker != null) {
mTextTextureMaker.dispose();
mTextTextureMaker = null;
}
}
public class Sprite {
// note: NaN on width OR height means "compute automatically based on texture's
// aspect ratio.
public boolean enabled;
public float x, y, width, height;
public int texIndex;
public int color;
public float tintFactor;
public float rotation;
public Sprite() {
clear();
}
public Sprite clear() {
x = y = 0.0f;
width = height = 1.0f;
texIndex = -1;
color = 0xff000080;
tintFactor = 1.0f;
enabled = true;
rotation = 0.0f;
return this;
}
}
public Sprite createSprite() {
Sprite s;
if (mSpriteRecycleBin.size() > 0) {
s = mSpriteRecycleBin.remove(mSpriteRecycleBin.size() - 1);
} else {
s = new Sprite();
}
mSprites.add(s);
return s;
}
public void deleteSprite(Sprite s) {
s.clear();
mSprites.remove(s);
mSpriteRecycleBin.add(s);
}
public void deleteSprites() {
int i;
for (i = 0; i < mSprites.size(); i++) {
mSpriteRecycleBin.add(mSprites.get(i).clear());
}
mSprites.clear();
}
public float getRelativePos(int relativeTo, float delta) {
return delta + (relativeTo == REL_RIGHT ? mBounds.right :
relativeTo == REL_LEFT ? mBounds.left :
relativeTo == REL_TOP ? mBounds.top :
relativeTo == REL_BOTTOM ? mBounds.bottom : 0.0f);
}
public void bringToFront(Sprite sp) {
int idx = mSprites.indexOf(sp);
if (idx >= 0 && idx < mSprites.size()) {
mSprites.remove(idx);
mSprites.add(sp);
}
}
public void sendToBack(Sprite sp) {
int idx = mSprites.indexOf(sp);
if (idx > 0 && idx < mSprites.size()) {
mSprites.remove(idx);
mSprites.add(0, sp);
}
}
}