在看见apidemo中自带的魔方例子后就一直想做一个可以触摸旋转的魔方,没事可以玩玩,于是在网上查找了大量的资料,根据自己的理解把魔方例子进行了改良,增加了贴图、触摸扭转、每帧处理时间的显示。代码在开发总结(四)后附加,效果具体见下图:
魔方开发简明指导 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
将魔方由9个小方块Cube组成,其中每个方块包含8个顶点(GLVertex)6个面(GLFace),将8个顶点放在Cube的mVertexList字段中,在每个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 方法 ),魔方的雏形就出来了。
为了便于观察魔方扭转时各方块的所在位置,就在每个方块的面上贴了一个数字的图片。
由于手机程序在运行时经常会切换到其它的程序中运行如接电话,所以将生成数字图片的方法放到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_2D, mTextureId);
/*
下一步需要给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_2D, mTextureId);
// 获取纹理的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坐标定义为左上角(0,0),右下角(1,1)(因为使用的2D Texture),下图坐标显示了UV坐标,右边为我们需要染色的平面的顶点顺序:
为了能正确的匹配,需要把UV坐标中的(0,1)映射到顶点0,(1,1)映射到顶点1等等。
float textureCoordinates[] = {0,1 , 1,1 , 1,0 , 0,0};
在本项目中由于顶点全部写到mVertexList中,所以设置UV坐标时要设置8个点,在贴图时不属于该面的点设为0,0即可。
如:
设定正面为逆时针方向
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});
为了能获取运行时的模型视图和投影矩阵以便计算手点击魔方时的接触面,故在设置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());
其中mAngleX和mAngleY是在GLSurfaceView中的onTouchEvent方法中根据手指在屏幕上的移动距离计算。
射线拾取原理
http://www.ophonesdn.com/article/show/164
射线拾取实现
http://vaero.blog.51cto.com/4350852/790620
当我们知道手接触到面时,就希望魔方能按照手指的滑动方向转动。
在该项目中将魔方分为9层:上、中1、下、左、中2、右、前、中3、后。
如图所示,当手指依次滑过6、7、8时,这三个方块同时是属于前层和上层,一般来说本次操作是希望上层旋转,而当使用射线拾取时,射线与前层距离最近的面时由6、7、8、15、16、17、24、25、26组成的面A;射线与上层距离最近的面是6、7、8组成的面B。射线到A和B面的距离应该是相等的(实际计算时还是有误差,误差在0.00001),这时可以计算A和B面的面积,去面积小的面所在层转动。
// 如果发生了相交
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;
}
}
}
为了实现局部转动,可以将需要转动的顶点乘以旋转矩阵,当转动结束时更新当前各层的方块。
旋转矩阵:
public void animateTransform(M4 transform) {
mAnimateTransform = transform;
//累计旋转的角度
if (mTransform != null)
transform = mTransform.multiply(transform);
Iterator<GLVertex> 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();
为了更好的查看程序运行速度,在界面的右下方增加每帧所用时间显示。
该文本是固定在屏幕右下角显示,原理为将要显示的文本绘在图片中,然后将图片直接贴到屏幕的右下角,具体编码如下:
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(imageWidth, imageHeight, 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_2D, mTextureID);
// 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_2D, mTextureID);
int[] crop = {0, imageHeight, imageWidth, -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,imageWidth, imageHeight);
gl.glDisable(GL10.GL_TEXTURE_2D);
全部源代码下载地址:
http://download.csdn.net/detail/tomatozq/4340801