基于Android opengles的魔方开发总结

基于Android opengles的魔方开发总结(一)

1. 前言

在看见apidemo中自带的魔方例子后就一直想做一个可以触摸旋转的魔方,没事可以玩玩,于是在网上查找了大量的资料,根据自己的理解把魔方例子进行了改良,增加了贴图、触摸扭转、每帧处理时间的显示。代码在开发总结(四)后附加,效果具体见下图:

基于Android opengles的魔方开发总结_第1张图片

2. 参考资料

魔方开发简明指导 http://www.apkbus.com/android-2756-1-1.html

魔方贴图http://www.asiteof.me/2011/01/android_opengl/

如何用触屏方式点击魔方(射线拾取原理)http://www.ophonesdn.com/article/show/164

Android OpenGL射线拾取&手势旋转 http://vaero.blog.51cto.com/4350852/790620

3. 实现原理分析

3.1. 画立方体

将魔方由9个小方块Cube组成,其中每个方块包含8个顶点(GLVertex6个面(GLFace),将8个顶点放在CubemVertexList字段中,在每个GLFace中放置要绘制的顶点顺序和面的颜色、及贴图。

魔方按照以下流程绘制:

1.首先在onDrawFrame方法中执行(注意GLSurfaceView 的渲染模式有两种,一种是连续不断的更新屏幕,另一种为on-demand ,只有在调用requestRender() 在更新屏幕。 缺省为RENDERMODE_CONTINUOUSLY 持续刷新屏幕)。 

mWorld.draw(gl);

2.GLWorld类中的draw方法为:

//设置小方块的顶点数组

gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mVertexBuffer);

for(GLShape shape : mShapeList) {

shape.draw(gl);

}

3.GLShape类中的draw方法为:

for (GLFace face : mFaceList) {

face.draw(gl);

}

GLFace类中的draw方法为:

//启用顶点数组

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

//打开 忽略

后面 设置

gl.glEnable(GL10.GL_CULL_FACE);

//设置逆时针方法为面的

前面  如果 画图索引 顺序不是逆时针 反向 则无法看见

gl.glFrontFace(GL10.GL_CCW);

//明确指明忽略背面

gl.glCullFace(GL10.GL_BACK);

//不画被

遮挡 的图形部分

gl.glEnable(GL10.GL_DEPTH_TEST); 

//获取面的绘制顺序

    

ShortBuffer indicesBuffer = face.getIndicesBuffer();

indicesBuffer.position(0);

  

  //每相邻三个顶点组成一个三角形

gl.glDrawElements(GL10.

GL_TRIANGLE_STRIP ,4,  GL10. GL_UNSIGNED_SHORT , indicesBuffer);

在设置好每个方块的坐标和面的绘制顺序后(具体见代码

KubeActivity 类中的 makeGLWorld 方法 ),魔方的雏形就出来了。
基于Android opengles的魔方开发总结_第2张图片








基于Android opengles的魔方开发总结(二)

1.1. 贴图

为了便于观察魔方扭转时各方块的所在位置,就在每个方块的面上贴了一个数字的图片。

基于Android opengles的魔方开发总结_第3张图片

由于手机程序在运行时经常会切换到其它的程序中运行如接电话,所以将生成数字图片的方法放到Render类的onSurfaceChanged方法中。

生成数字图片代码为:

int imgSize = 64;

int fontSize = 20;

Bitmap bitmap = Bitmap.createBitmap(imgSize, imgSize, Bitmap.Config.ARGB_8888); 

Canvas canvas = new Canvas(bitmap);

//设置画布背景为透明,这样我们的纹理就只显示文字,而没有颜色背景

canvas.drawColor(Color.TRANSPARENT);

Paint p = new Paint();

//设置字体、字体大小和字体颜色

String familyName = "Times New Roman";

Typeface font = Typeface.create(familyName, Typeface.NORMAL);

p.setColor(Color.WHITE);

p.setTypeface(font);

p.setTextSize(fontSize);

//在Bitmap上绘制文字

String text = cube.id;

float textWidth = p.measureText(text);

canvas.drawText(cube.id,(imgSize - textWidth)/2,imgSize - fontSize, p); 

cube.loadBitmap(bitmap);

将图片生成后,在onDrawFrame运行时:

if (bInitTexture) { 

bInitTexture = false;

int[] textures = new int[1]; 

//生成纹理编号

gl.glGenTextures(1, textures, 0); 

mTextureId = textures[0]; 

  

//绑定到GL10.GL_TEXTURE_2D

gl.glBindTexture(GL10.GL_TEXTURE_2DmTextureId); 

  

/*

下一步需要给Texture填充设置参数,用来渲染的Texture可能比要渲染的区域大或者小,这是需要设置Texture需要放大或是缩小时OpenGL的模式:需要比较清晰的图像使用GL10.GL_NEAREST

*/

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER,GL10.GL_LINEAR); 

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER,GL10.GL_LINEAR); 

  

/*

如何去渲染这些不存在的Texture部分

有两种设置

GL_REPEAT 重复Texture。 

GL_CLAMP_TO_EDGE 只靠边线绘制一次

*/

//水平方向靠边

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,GL10.GL_CLAMP_TO_EDGE); 

//垂直方向重复

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,GL10.GL_REPEAT); 

//纹理贴图和材质混合的方式,如果选择GL10.REPLACE则只显示纹理

gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE,GL10.GL_MODULATE);

//然后是将Bitmap资源和Texture绑定起来   

GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, mBitmap, 0); 

}

//设置面的背景色

gl.glColor4f(color.red, color.green, color.blue, color.alpha);

//如果面的背景色不为黑色则贴图,黑色面为不可见区域:

if (!color.equals(GLColor.BLACK)) { 

 gl.glEnable(GL10.GL_TEXTURE_2D); 

 //启用纹理坐标数组

 gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); 

// 每次绘图时都需要绑定 

gl.glBindTexture(GL10.GL_TEXTURE_2DmTextureId); 

// 获取纹理的UV坐标数组

FloatBuffer textureBuffer = getTextureBuffer();

textureBuffer.position(0);

gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);

}

 

ShortBuffer indicesBuffer = getIndicesBuffer();

indicesBuffer.position(0);

    

gl.glDrawElements(GL10.GL_TRIANGLE_STRIP,4, GL10.GL_UNSIGNED_SHORT, indicesBuffer);

        

 if (!color.equals(GLColor.BLACK)) { 

   gl.glDisable(GL10.GL_TEXTURE_2D); 

    gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY); 

}

UV Mapping

告知OpenGL库如何将Bitmap的像素映射到Mesh上。这可以分为两步来完成:

UV Mapping指将Bitmap的像素映射到Mesh上的顶点。UV坐标定义为左上角(00),右下角(11)(因为使用的2D Texture),下图坐标显示了UV坐标,右边为我们需要染色的平面的顶点顺序:

基于Android opengles的魔方开发总结_第4张图片

为了能正确的匹配,需要把UV坐标中的(01)映射到顶点0,(11)映射到顶点1等等。

 float textureCoordinates[] = {0,1 , 1,1 , 1,0 , 0,0}; 

基于Android opengles的魔方开发总结_第5张图片

在本项目中由于顶点全部写到mVertexList中,所以设置UV坐标时要设置8个点,在贴图时不属于该面的点设为0,0即可。

如:


基于Android opengles的魔方开发总结_第6张图片

设定正面为逆时针方向

 bottomFace.setIndices(new short[] { 4, 0, 5, 1, });

 bottomFace.setTextureCoordinates(new float[] { 0,1 , 1,1, 0,0, 0,0, 0,0, 1,0, 0,0, 0,0});








基于Android opengles的魔方开发总结(三)

1. 整体旋转

为了能获取运行时的模型视图和投影矩阵以便计算手点击魔方时的接触面,故在设置opengl时不采用opengl自带的api,而采用托管矩阵的方式,对矩阵进行运算后再直接赋给opengl

onSurfaceChanged方法中:

//GLU.gluPerspective(gl, 45f, ratio, 2,12);

//改为托管矩阵运行

Matrix4f.gluPersective(45.0f,ratio,0.1f,100,AppConfig.gMatProject);

gl.glLoadMatrixf(AppConfig.gMatProject.asFloatBuffer());

onDrawFrame方法中:

//    gl.glTranslatef(0, 0, -3.0f);

Matrix4f.gluLookAt(mvEye,mvCenter,mvUp, AppConfig.gMatView);

gl.glLoadMatrixf(AppConfig.gMatView.asFloatBuffer());

AppConfig.gMatModel.setIdentity();

if (AppConfig.gbNeedPick && !touchInCubeSphere()) {

mAngleX += offsetX;

mAngleY += offsetY;

}

//      gl.glRotatef(mAngle,        0, 1, 0);

//      gl.glRotatef(mAngle*0.25f,  1, 0, 0);

        //矩阵旋转

   Matrix4f matRotX = new Matrix4f();

matRotX.setIdentity();

matRotX.rotX((float) (mAngleX * Math.PI / 180));

AppConfig.gMatModel.mul(matRotX);

Matrix4f matRotY = new Matrix4f();

matRotY.setIdentity();

matRotY.rotY((float) (mAngleY * Math.PI / 180));

AppConfig.gMatModel.mul(matRotY);

gl.glMultMatrixf(AppConfig.gMatModel.asFloatBuffer());

其中mAngleXmAngleY是在GLSurfaceView中的onTouchEvent方法中根据手指在屏幕上的移动距离计算。

2. 射线拾取

射线拾取原理

http://www.ophonesdn.com/article/show/164

射线拾取实现

http://vaero.blog.51cto.com/4350852/790620

3. 接触面判断

当我们知道手接触到面时,就希望魔方能按照手指的滑动方向转动。

在该项目中将魔方分为9层:上、中1、下、左、中2、右、前、中3、后。

基于Android opengles的魔方开发总结_第7张图片

   如图所示,当手指依次滑过678时,这三个方块同时是属于前层和上层,一般来说本次操作是希望上层旋转,而当使用射线拾取时,射线与前层距离最近的面时由678151617242526组成的面A;射线与上层距离最近的面是678组成的面B。射线到AB面的距离应该是相等的(实际计算时还是有误差,误差在0.00001),这时可以计算AB面的面积,去面积小的面所在层转动。

// 如果发生了相交

if (transformedRay.intersectTriangle(v0, v1, v2, location)) {

Log.d("GLWorld""层" + layer.index + "与射线相交,距离屏幕:" + location.w);

if (!bFound) {

// 如果是初次检测到,需要存储射线原点与三角形交点的距离值

bFound = true;

closeDis = location.w;

nearstLayer = layer;

nearest[0] = v0;

nearest[1] = v1;

nearest[2] = v2;

else {

// 如果之前已经检测到相交事件,则需要把新相交点与之前的相交数据相比较

// 最终保留离射线原点更近的(如误差则一定范围内就判断相交平面的面积)

if(Math.abs(closeDis-location.w)<0.0001){

//与平面距离近似相等则判断三角形面积

//比当前的大则说明面靠前

double area1 = calculateArea(nearest[0],nearest[1],nearest[2]);

double area2 = calculateArea(v0,v1,v2);

if (area2>area1) {

nearstLayer = layer;

nearest[0] = v0;

nearest[1] = v1;

nearest[2] = v2;

}

}

else if (closeDis > location.w) {

closeDis = location.w;

nearstLayer = layer;

nearest[0] = v0;

nearest[1] = v1;

nearest[2] = v2;

}

}

}

4. 魔方转动

    为了实现局部转动,可以将需要转动的顶点乘以旋转矩阵,当转动结束时更新当前各层的方块。

旋转矩阵:

基于Android opengles的魔方开发总结_第8张图片

public void animateTransform(M4 transform) {

mAnimateTransform = transform;

//累计旋转的角度

if (mTransform != null)

transform = mTransform.multiply(transform);

Iterator iter = mVertexList.iterator();

while (iter.hasNext()) {

GLVertex vertex = iter.next();

//mWorld.transformVertex(vertex, transform);

vertex.update(mVertexBuffer, transform);

}

}

魔方在旋转前,预先将方块顺时针和逆时针将要选择到的位置进行设置,在旋转完毕时只需要按设置好的数组更新每层的方块。

    /*

     * 跟每一层方块位置进行编号(由上至下),旋转后坐标一定要写对,否则坐标保存的与看到的不同。(注意:这里的编号是位置编号不是方块编号)

     * 0 1 2 2 5 8

     * 3 4 5 ->顺时针选择90度->1 4 7

     * 6 7 80 3 6

     * */

    static int[][] mLayerCWPermutations = {

        // permutation for UP layer 最上层顺时针旋转90度后布局

        { 2, 5, 8, 1, 4, 7, 0, 3, 6, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 },

        // permutation for DOWN layer 最下层顺时针旋转90度后布局

        { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20, 23, 26, 19, 22, 25, 18, 21, 24 },

        // permutation for LEFT layer 左侧旋转90度

        { 6, 1, 2, 15, 4, 5, 24, 7, 8, 3, 10, 11, 12, 13, 14, 21, 16, 17, 0, 19, 20, 9, 22, 23, 18, 25, 26 },

        // permutation for RIGHT layer 右侧旋转90度

        { 0, 1, 8, 3, 4, 17, 6, 7, 26, 9, 10, 5, 12, 13, 14, 15, 16, 23, 18, 19, 2, 21, 22, 11, 24, 25, 20 },

        // permutation for FRONT layer 前面旋转90度

        { 0, 1, 2, 3, 4, 5, 24, 15, 6, 9, 10, 11, 12, 13, 14, 25, 16, 7, 18, 19, 20, 21, 22, 23, 26, 17, 8 },

        // permutation for BACK layer 后面旋转90度

        { 18, 9, 0, 3, 4, 5, 6, 7, 8, 19, 10, 1, 12, 13, 14, 15, 16, 17, 20, 11, 2, 21, 22, 23, 24, 25, 26 },

        // permutation for MIDDLE layer (中间面绕X轴旋转90度)

        { 0, 7, 2, 3, 16, 5, 6, 25, 8, 9, 4, 11, 12, 13, 14, 15, 22, 17, 18, 1, 20, 21, 10, 23, 24, 19, 26 },

        // permutation for EQUATOR layer (中间绕Y轴旋转90度)

        { 0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 14, 17, 10, 13, 16, 9, 12, 15, 18, 19, 20, 21, 22, 23, 24, 25, 26 },

        // permutation for SIDE layer (中间绕Z轴旋转90度)

        { 0, 1, 2, 21, 12, 3, 6, 7, 8, 9, 10, 11, 22, 13, 4, 15, 16, 17, 18, 19, 20, 23, 14, 5, 24, 25, 26 }

    };

 //如果旋转到位

    mCurrentLayer.setAngle(mEndAngle);

    mCurrentLayer.endAnimation();

    mCurrentLayer = null;

    layerID=-1;           

 //adjust mPermutation based on the completed layer rotation

    int[] newPermutation = new int[27];

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

//更新各层的方块

//mCurrentLayerPermutation[i]相当于方块的在数组中的地址

    newPermutation[i] = mPermutation[mCurrentLayerPermutation[i]];

    }

mPermutation = newPermutation;

updateLayers();

 







基于Android opengles的魔方开发总结(四)

1.1. 屏幕中固定位置文本显示

为了更好的查看程序运行速度,在界面的右下方增加每帧所用时间显示。

基于Android opengles的魔方开发总结_第9张图片

该文本是固定在屏幕右下角显示,原理为将要显示的文本绘在图片中,然后将图片直接贴到屏幕的右下角,具体编码如下:

if(mTextureID==-1){

    int fontSize = 32;

    //设置字体、字体大小和字体颜色

    Paint p = new Paint();

    String familyName = "Times New Roman";

    Typeface font = Typeface.create(familyName, Typeface.NORMAL);

    p.setColor(Color.RED);

    p.setTypeface(font);

    p.setTextSize(fontSize);

    

    //在Bitmap上绘制文字

    String text = "旋转方块";

    int textWidth = (int) Math.ceil(p.measureText(text));

            int textHeight = (int) Math.ceil(-p.ascent()) + (int) Math.ceil(p.descent());

            

            imageWidth = textWidth;

            imageHeight = textHeight + 10;

            

Bitmap bitmap = Bitmap.createBitmap(imageWidthimageHeight, Bitmap.Config.ARGB_8888); 

    

    Canvas canvas = new Canvas(bitmap);

    canvas.drawText(text,0,textHeight, p); 

    

int[] textures = new int[1];

        gl.glGenTextures(1, textures, 0);

        mTextureID = textures[0];

        gl.glBindTexture(GL10.GL_TEXTURE_2DmTextureID);

        // Use Nearest for performance.

  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_NEAREST);

        gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,GL10.GL_CLAMP_TO_EDGE);

        gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,GL10.GL_CLAMP_TO_EDGE);

        //GL10.GL_MODULATE

        gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE,GL10.GL_REPLACE);

        GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0); 

}

        gl.glEnable(GL10.GL_TEXTURE_2D);

        gl.glBindTexture(GL10.GL_TEXTURE_2DmTextureID);

        int[] crop = {0, imageHeightimageWidth, -imageHeight};

        ((GL11)gl).glTexParameteriv(GL10.GL_TEXTURE_2D,GL11Ext.GL_TEXTURE_CROP_RECT_OES,crop, 0);

        

        //将纹理直接画到屏幕中某位置

        ((GL11Ext)gl).glDrawTexiOES((AppConfig.gpViewport[2] - imageWidth)/2, 10 , 0,imageWidthimageHeight);

        

        gl.glDisable(GL10.GL_TEXTURE_2D);


全部源代码下载地址:

http://download.csdn.net/detail/tomatozq/4340801






你可能感兴趣的:(基于Android opengles的魔方开发总结)