项目一开始,自己对着一本《OpenGL ES 2.0 游戏开发(上卷)》撸了很长一段时间,里面学习到OpenGL 的挺多知识,包括着色器语言,还有大部分GL函数,纹理,光照等等。然而书中的所有Demo都采用一种模式,GLSurfaceView + MatrixState + ShaderUtil ,这几个构成了书中开发OpenGL的基本框架。这是很完善的框架。但是在3D坐标计算的时候我遇到了大麻烦。当需要切换视野,或者操作绘制出来的模型的3D坐标的时候,书中只用了些 x ,y ,z 等进行运算。恕我愚钝,虽然我觉得这种方法可以实现效果,但是当数据量很大的时候就有很多个 x ,y,z 并不好管理,而且有时候用一些 sin() cos() tan() 函数,初学者难免会懵逼。之后我查了很久资料没有发现在Android平台下有好的库能帮我们理解。于是我找到了IOS的书本。里面的一些思路值得借鉴。
当然我并不是说Android的书中没有什么好东西。MatrixState和ShaderUtil就是很好的东西,在Android上开发OpenGL 基本上都是这样的流程。但是着两个把一些流程封装了起来。
//加载制定shader的方法
public static int loadShader(
int shaderType, //shader的类型 GLES20.GL_VERTEX_SHADER GLES20.GL_FRAGMENT_SHADER
String source //shader的脚本字符串
) {
//创建一个新shader
int shader = GLES20.glCreateShader(shaderType);
//若创建成功则加载shader
if (shader != 0) {
//加载shader的源代码
GLES20.glShaderSource(shader, source);
//编译shader
GLES20.glCompileShader(shader);
//存放编译成功shader数量的数组
int[] compiled = new int[1];
//获取Shader的编译情况
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
if (compiled[0] == 0) {//若编译失败则显示错误日志并删除此shader
Log.e("ES20_ERROR", "Could not compile shader " + shaderType + ":");
Log.e("ES20_ERROR", GLES20.glGetShaderInfoLog(shader));
GLES20.glDeleteShader(shader);
shader = 0;
}
}
return shader;
}
上述方法是加载一段shader程序源码字符串,编译shader程序,然后返回一个代表该程序的id,其中shader程序一般有2个,片元着色器和顶点着色器(不会的话请自己看看一些OpenGL 书本普及一下)
//创建shader程序的方法
public static int createProgram(String vertexSource, String fragmentSource) {
//加载顶点着色器
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
if (vertexShader == 0) {
return 0;
}
//加载片元着色器
int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
if (pixelShader == 0) {
return 0;
}
//创建程序
int program = GLES20.glCreateProgram();
//若程序创建成功则向程序中加入顶点着色器与片元着色器
if (program != 0) {
//向程序中加入顶点着色器
GLES20.glAttachShader(program, vertexShader);
checkGlError("glAttachShader");
//向程序中加入片元着色器
GLES20.glAttachShader(program, pixelShader);
checkGlError("glAttachShader");
//链接程序
GLES20.glLinkProgram(program);
//存放链接成功program数量的数组
int[] linkStatus = new int[1];
//获取program的链接情况
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
//若链接失败则报错并删除程序
if (linkStatus[0] != GLES20.GL_TRUE) {
Log.e("ES20_ERROR", "Could not link program: ");
Log.e("ES20_ERROR", GLES20.glGetProgramInfoLog(program));
GLES20.glDeleteProgram(program);
program = 0;
}
}
return program;
}
上面的方法是创建一个程序,返回编译成功后的程序id,在绘制之前需要使用此id
//检查每一步操作是否有错误的方法
public static void checkGlError(String op) {
int error;
while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
Log.e("ES20_ERROR", op + ": glError " + error);
throw new RuntimeException(op + ": glError " + error);
}
}
上述代码作用是检查GL函数错误。定位出在执行那些GL函数的时候在发生了什么异常
//存储系统矩阵状态的类
public class MatrixState {
private static float[] mProjMatrix = new float[16];//4x4矩阵 投影用
private static float[] mVMatrix = new float[16];//摄像机位置朝向9参数矩阵
private static float[] currMatrix; //当前变换矩阵
static float[][] mStack = new float[10][16]; //用于保存变换矩阵的类
static int stackTop = -1; //标识栈顶的索引
/** * 初始化变换矩阵 */
public static void setInitStack() {
currMatrix = new float[16];
Matrix.setRotateM(currMatrix, 0, 0, 1, 0, 0); //除初始化无变换内容的矩阵
}
/** * 把变换矩阵保存到栈中 */
public static void pushMatrix() {
stackTop++;
for (int i = 0; i < 16; i++) {
mStack[stackTop][i] = currMatrix[i];
}
}
/** * 从栈中读取变换矩阵 */
public static void popMatrix() {
for (int i = 0; i < 16; i++) {
currMatrix[i] = mStack[stackTop][i];
}
stackTop--;
}
/** * 平移变换 */
public static void translate(float x, float y, float z) {
Matrix.translateM(currMatrix, 0, x, y, z);
}
/** * 旋转变换 * * @param angle * @param x * @param y */
public static void rotate(float angle, float x, float y, float z) {
Matrix.rotateM(currMatrix, 0, angle, x, y, z);
}
/** * 缩放变换 */
public static void scale(float x, float y, float z) {
Matrix.scaleM(currMatrix, 0, x, y, z);
}
//设置摄像机
static float[] cameraLocation=new float[3];//摄像机位置
/** * 设置摄像机 * * @param cx 摄像机位置x * @param cy 摄像机位置y * @param cz 摄像机位置z * @param tx 摄像机目标点x * @param ty 摄像机目标点y * @param tz 摄像机目标点z * @param upx 摄像机UP向量X分量 * @param upy 摄像机UP向量Y分量 * @param upz 摄像机UP向量Z分量 */
public static void setCamera(float cx, float cy, float cz, float tx, float ty, float tz, float upx, float upy, float upz) {
Matrix.setLookAtM(mVMatrix, 0, cx, cy, cz, tx, ty, tz, upx, upy, upz);
}
/** * 设置正交投影参数 * * @param left near面的left * @param right near面的right * @param bottom near面的bottom * @param top near面的top * @param near near面距离 * @param far far面距离 */
public static void setProjectOrtho(float left, float right, float bottom, float top, float near, float far) {
Matrix.orthoM(mProjMatrix, 0, left, right, bottom, top, near, far);
}
/** * 设置透视投影参数 * * @param left near面的left * @param right near面的right * @param bottom near面的bottom * @param top near面的top * @param near near面距离 * @param far far面距离 */
public static void setProjectFrustum(float left, float right, float bottom, float top, float near, float far) {
Matrix.frustumM(mProjMatrix, 0, left, right, bottom, top, near, far);
}
/** * 获取具体物体的变换之后的矩阵 * * @return */
public static float[] mMVPMatrix = new float[16];
public static float[] getFinalMatrix() {
Matrix.multiplyMM(mMVPMatrix, 0, mVMatrix, 0, currMatrix, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mMVPMatrix, 0);
return mMVPMatrix;
}
//获取具体物体的变换矩阵
public static float[] getMMatrix()
{
return currMatrix;
}
}
MatrixState 中有一下功能
由于MatrixState中维护的是一个static的float数组所以我们在操作的时候只需要调用方法并不需要关心内部的运行情况,在使用的时候需要注意: 在创建onSurfaceCreated中调用MatrixState.setInitStack()初始化栈,当需要调用变换的时候:顺序是 MatrixState.pushMatrix() -> 变换操作,绘制 -> MatrixState.popMatrix();
这样做的好处是,如果你需要变换一个物体,则会改变传入GL的总变换矩阵,所以我们用栈保存好变换前的矩阵,等变换完成后,在从栈中读取恢复矩阵。 如果不做此操作。GL绘制的物体都会执行同一个变换。
好了,上面说完这两个类的好处,接下来我要介绍一下IOS开发中使用的框架。这个框架是方便与我们对绘制出来的物体的3D坐标处理,对摄像机位置的放置等起着巨大的作用。IOS中的库名为GLKit,里面提供了一个概念——向量。
本人参考了GLKit的一些方法的实现方式,把代码转换成为用JAVA语言描述。
这是一个向量的概念,其中包含了3个坐标(x,y,z) 向量可以代表一个点,同时也可以代表一个方向和长度。我们所有的点都用向量表示。后面的运算也全部基于向量来运算。
两个向量相减
/** * 两个向量相减 */
public static GLKVector3 GLKVector3Subtract(GLKVector3 vectorLeft, GLKVector3 vectorRight) {
GLKVector3 v = new GLKVector3();
v.x = vectorLeft.x - vectorRight.x;
v.y = vectorLeft.y - vectorRight.y;
v.z = vectorLeft.z - vectorRight.z;
return v;
}
计算一个向量的长度,其实就是一个点到点(0,0,0)的距离长度
/** * 计算向量的长度 */
public static float GLKVector3Length(GLKVector3 vector) {
return (float) Math.sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z);
}
计算两个向量之间的距离
public static float GLKVector3Distance(GLKVector3 vectorStart, GLKVector3 vectorEnd) {
return GLKVector3Length(GLKVector3Subtract(vectorEnd, vectorStart));
}
根据基数计算从一个向量的比例,缩放向量。
public static GLKVector3 GLKVector3MultiplyScalar(GLKVector3 vector, float value) {
GLKVector3 v = new GLKVector3();
v.x = vector.x * value;
v.y = vector.y * value;
v.z = vector.z * value;
return v;
}
根据基数计算两个向量连成直线上的一点
/** * 从向量start 向向量end 移动 value 比列 */
public static GLKVector3 GLKVector3Lerp(GLKVector3 vectorStart, GLKVector3 vectorEnd, float value) {
GLKVector3 v = new GLKVector3();
v.x = vectorStart.x + value * (vectorEnd.x - vectorStart.x);
v.y = vectorStart.y + value * (vectorEnd.y - vectorStart.y);
v.z = vectorStart.z + value * (vectorEnd.z - vectorStart.z);
return v;
}
两个向量相加
public static GLKVector3 GLKVector3Add(GLKVector3 x, GLKVector3 y) {
GLKVector3 v = new GLKVector3();
v.x = x.x + y.x;
v.y = x.y + y.y;
v.z = x.z + y.z;
return v;
}
计算两个向量形成的矢量积,返回的是一个垂直于两个向量之间形成的平面的一个向量
/** 计算两个向量的矢量积 */
public static GLKVector3 GLKVector3CrossProduct(GLKVector3 vectorLeft, GLKVector3 vectorRight) {
GLKVector3 v = new GLKVector3();
v.x = vectorLeft.y * vectorRight.z - vectorLeft.z * vectorRight.y;
v.y = vectorLeft.z * vectorRight.x - vectorLeft.x * vectorRight.z;
v.z = vectorLeft.x * vectorRight.y - vectorLeft.y * vectorRight.x;
return v;
}
矢量标准化把一个向量转换成为(1,1,1)的向量
/** 向量标准化 */
public static GLKVector3 GLKVector3Normalize(GLKVector3 vector)
{
float scale = 1.0f / GLKVector3Length(vector);
GLKVector3 v = new GLKVector3(vector.x * scale, vector.y * scale, vector.z * scale);
return v;
}
返回将一个坐标经过一个矩阵转换后的坐标
/** * 将物体坐标转换成世界坐标 * @param matrixLeft * @param vectorRight * @return */
public static GLKVector3 GLKMatrix4MultiplyAndProjectVector3(float[] matrixLeft, GLKVector3 vectorRight) {
GLKVector4 v4 = GLKMatrix4MultiplyVector4(matrixLeft, GLKVector4Make(vectorRight.x, vectorRight.y, vectorRight.z, 1.0f));
return GLKVector3MultiplyScalar(GLKVector3Make(v4.x, v4.y, v4.z), 1.0f / v4.w);
}
public static GLKVector3 GLKVector3Make(float x, float y, float z) {
GLKVector3 v = new GLKVector3();
v.x = x;
v.y = y;
v.z = z;
return v;
}
public static GLKVector4 GLKMatrix4MultiplyVector4(float[] matrixLeft, GLKVector4 vector) {
GLKVector4 v4 = new GLKVector4();
v4.x = matrixLeft[0] * vector.x + matrixLeft[4] * vector.y + matrixLeft[8] * vector.z + matrixLeft[12] * vector.w;
v4.y = matrixLeft[1] * vector.x + matrixLeft[5] * vector.y + matrixLeft[9] * vector.z + matrixLeft[13] * vector.w;
v4.z = matrixLeft[2] * vector.x + matrixLeft[6] * vector.y + matrixLeft[10] * vector.z + matrixLeft[14] * vector.w;
v4.w = matrixLeft[3] * vector.x + matrixLeft[7] * vector.y + matrixLeft[11] * vector.z + matrixLeft[15] * vector.w;
return v4;
}
public static GLKVector4 GLKVector4Make(float x, float y, float z, float w) {
GLKVector4 v4 = new GLKVector4();
v4.x = x;
v4.y = y;
v4.z = z;
v4.w = w;
return v4;
}
还有很多很强大的函数,我这里就不一一列举了。 而有了这些函数的时候我们需要计算坐标的时候大大增加了灵活性。
下面我举一个例子来说明方便在那里。
在绘制场景中的树木的时候,我们经常用到一个技巧,就是通过标志板实现。但是使用标记板需要计算标记板的朝向以便在视野变换的时候保持数目看起来是立体的。
下面我贴出来OpenGL ES 2.0 书中的实现方法:
//根据摄像机位置计算数目面朝向
public void calculateBillboardDirection(){
float xspan=x-cx;
float zspan=z-cz;
if(zspan<=0)
{
yAngle=(float)Math.toDegrees(Math.atan(xspan/zspan));
}
else
{
yAngle=180+(float)Math.toDegrees(Math.atan(xspan/zspan));
}
Log.i("aaa","yAngle = " + yAngle);
}
//比较2棵树离摄像机的距离的方法
@Override
public int compareTo(SingleTree another) {
float xs = x-cx;
float zs = z-cz;
float xo = another.x - cx;
float zo = another.z - cz;
float disA = (float) Math.sqrt(xs * xs + zs * zs);
float disB = (float) Math.sqrt(xo * xo + zo * zo);
return ((disA-disB) == 0)?0:((disA-disB)>0)?-1:1; //远的 -1 ,近的 1
}
其中的cx,cy,cz 是摄像机的位置坐标
上面的运算需要用到一些很直观的运行 sin cos 开平方等。实在是难以理解,一时之间难以理解为什么用sin cos。
下面是用了这些函数的方法
@Override
public int compareTo(JTTreeInfo another) { //进行排序,用Collections.sort 可以升序排列
CameraPosition camera = JTGeoEngine.getInstance().getCameraPosition();
GLKVector3 otherPosition = another.mPosition;
float mDis = GeoUtils.GLKVector3Distance(camera.eyePosition, mPosition);
float oDis = GeoUtils.GLKVector3Distance(camera.eyePosition, otherPosition);
return ((mDis - oDis) == 0) ? 0 : ((mDis - oDis) > 0) ? -1 : 1;
}
//根据摄像机位置计算树木面朝向
public void calculateBillboardDirection() {
CameraPosition camera = JTGeoEngine.getInstance().getCameraPosition();
GLKVector3 lookDirection = GeoUtils.GLKVector3Subtract(camera.lookAtPosition, camera.eyePosition);
//得到视线方向
lookDirection = GeoUtils.GLKVector3Normalize(lookDirection);
GLKVector3 upUnitVector = GeoUtils.GLKVector3Make(0.0f, 0.0f, 1.0f);
//得到一个法向量。此法向量为垂直于视线方向的向量,所以,标致版只需要按此向量定义左右底部的位置即可。
GLKVector3 rightVector = GeoUtils.GLKVector3CrossProduct(upUnitVector, lookDirection);
//左右底部的位置为 x = x - w * 0.5
GLKVector3 leftBottomPosition = GeoUtils.GLKVector3Add(GeoUtils.GLKVector3MultiplyScalar(rightVector, w * -0.5f), mPosition);
GLKVector3 rightBottomPosition = GeoUtils.GLKVector3Add(GeoUtils.GLKVector3MultiplyScalar(rightVector, w * 0.5f), mPosition);
//左右上面的点为底部加上高度即可
GLKVector3 leftTopPosition = GeoUtils.GLKVector3Add(leftBottomPosition, GeoUtils.GLKVector3MultiplyScalar(upUnitVector, h));
GLKVector3 rightTopPosition = GeoUtils.GLKVector3Add(rightBottomPosition, GeoUtils.GLKVector3MultiplyScalar(upUnitVector, h));
mVertexBuffer.clear();
vertices[0] = rightBottomPosition.x;
vertices[1] = rightBottomPosition.y;
vertices[2] = rightBottomPosition.z;
vertices[3] = rightTopPosition.x;
vertices[4] = rightTopPosition.y;
vertices[5] = rightTopPosition.z;
vertices[6] = leftBottomPosition.x;
vertices[7] = leftBottomPosition.y;
vertices[8] = leftBottomPosition.z;
vertices[9] = leftTopPosition.x;
vertices[10] = leftTopPosition.y;
vertices[11] = leftTopPosition.z;
mVertexBuffer.put(vertices);
mVertexBuffer.position(0);
}
这样的代码简单明了很多,而且有很多都是有思路可循。(不要觉得我的代码多就不好,,我指的简单是说逻辑简单,不是单纯的代码量减少。)