前面介绍了Android OpenGL的开发基础,绘制了一个3D的物体,在立体空间控制一个3D对象,但如何来构建一个3D的场景呢?接下来就讲讲怎样去完成一个3D世界的场景吧。
首先,我们应该明白的是,任何一个复杂的对象都是由一些简单的三角形构成的,所以在创建一个复杂的3D场景之前,要先定义一个场景的数据结构。三角形本质上是由一些(两个以上)顶点组成的多边形,顶点是最基本的分类单位,它包含了OpenGL真正有用的数据,我们用3D空间中的坐标值(x,y,z)以及它们的纹理坐标(u,v)来定义三角形的每个顶点。当然啦,每个对象都不只是由一个三角形构成的,因此可以通过一个List来存储这些三角形。
数据结构代码如下:
import java.util.ArrayList; import java.util.List; //VERTEX顶点结构 class VERTEX { float x, y, z;// 3D 坐标 float u, v;// 纹理坐标 public VERTEX(float x,float y,float z,float u,float v) { this.x = x; this.y = y; this.z = z; this.u = u; this.v = v; } } //TRIANGLE三角形结构 class TRIANGLE { // VERTEX矢量数组,大小为3 VERTEX[] vertex = new VERTEX[3]; } //SECTOR区段结构 class SECTOR { // Sector中的三角形个数 int numtriangles; // 三角行的list List<TRIANGLE> triangle = new ArrayList<TRIANGLE>(); }
一个场景必然由很多个顶点组成,由于这些顶点的数据量过大,所以这里将这些顶点存放到一个和游戏一起打包的文件中,然后在程序中通过装载这个文件来取得数据。在这里将顶点数据存放到“assets/data/world.txt”文件中,其读取文件的代码如下:
import java.io.IOException; import java.io.InputStream; import android.content.res.AssetManager; import android.content.res.Resources; public class GLFile { public static Resources resources; public GLFile(Resources resources) { GLFile.resources = resources; } public static InputStream getFile(String name){ AssetManager am = GLFile.resources.getAssets(); try { return am.open(name); } catch (IOException e) { e.printStackTrace(); return null; } } }
装载图片的代码前面也已经说过,在此就不详细说了。。代码如下:
import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; public class GLImage { public static Bitmap mBitmap; public static void load(Resources resources) { mBitmap = BitmapFactory.decodeResource(resources, R.drawable.img); } }
然后创建一个MainActivity.java,代码跟以前的差不多,也不详细讲解了。。不懂的可以去我前两篇帖子。。代码如下:
public class MainActivity extends Activity { GLRender renderer = new GLRender(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); GLImage.load(this.getResources()); new GLFile(this.getResources()); GLSurfaceView glView = new GLSurfaceView(this); glView.setRenderer(render); setContentView(glView); } }
接下来就是整个应用的重头戏了。。先创建一个GLRender.java文件,根据http://www.apkbus.com/android-121526-1-1.html ,当然也会少不了下面的3个抽象方法的:
public void onDrawFrame(GL10 gl){}
public void onSurfaceChanged(GL10 gl, int width, int height){}
public void onSurfaceCreated(GL10 gl, EGLConfig config){}
onSurfaceCreated先实现下载纹理,然后就是一些设置,这些在前两篇中说到过了。。最后用读取资源数据,这里用了一个SetupWorld()函数。代码如下:
@Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { LoadGLTextures(gl); gl.glEnable(GL10.GL_TEXTURE_2D); gl.glBlendFunc(GL10.GL_SRC_ALPHA,GL10.GL_ONE); gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); gl.glClearDepthf(1.0f); gl.glDepthFunc(GL10.GL_LESS); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glShadeModel(GL10.GL_SMOOTH); gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST); SetupWorld(); }
SetupWorld()函数就是从”assets/data/world.txt”中读取数据,每取出3个点就构成一个三角形,代码如下:
public void SetupWorld() { BufferedReader br = new BufferedReader(new InputStreamReader(GLFile.getFile("data/world.txt"))); TRIANGLE triangle = new TRIANGLE(); int vertexIndex = 0; try { String line = null; while((line = br.readLine()) != null){ if(line.trim().length() <= 0 || line.startsWith("/")){ continue; } String part[] = line.trim().split("\\s+"); float x = Float.valueOf(part[0]); float y = Float.valueOf(part[1]); float z = Float.valueOf(part[2]); float u = Float.valueOf(part[3]); float v = Float.valueOf(part[4]); VERTEX vertex = new VERTEX(x, y, z, u, v); triangle.vertex[vertexIndex] = vertex; vertexIndex ++; if(vertexIndex == 3){ vertexIndex = 0; sector1.triangle.add(triangle); triangle = new TRIANGLE(); } } } catch (IOException e) { e.printStackTrace(); } }
LoadGLTextures(GL10 gl) 实现的是下载纹理,这部分在http://www.apkbus.com/android-121768-1-1.html 有较详细的讲到。。也不细说了哈。。代码如下:
public void LoadGLTextures(GL10 gl) { IntBuffer textureBuffer = IntBuffer.allocate(3); gl.glGenTextures(3, textureBuffer); texture[0] = textureBuffer.get(); gl.glBindTexture(GL10.GL_TEXTURE_2D, texture[0]); gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_NEAREST); GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, GLImage.mBitmap, 0); texture[1] = textureBuffer.get(2); gl.glBindTexture(GL10.GL_TEXTURE_2D, texture[1]); gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR); gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, GLImage.mBitmap, 0); }
接下来就来实现onDrawFrame(GL10 gl)函数吧。首先要去掉每个三角形的顶点数据,然后就是装在纹理贴图,将这些数据绘制到屏幕上就构建了所指定的场景样式了。代码如下:
@Override public void onDrawFrame(GL10 gl) { gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); // Clear The Screen And The Depth Buffer gl.glLoadIdentity(); // Reset The View float xtrans = -xpos; float ztrans = -zpos; float ytrans = -walkbias-0.25f; float sceneroty = 360.0f - yrot; FloatBuffer vertexPointer = FloatBuffer.wrap(new float[9]); FloatBuffer texCoordPointer = FloatBuffer.wrap(new float[6]); gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexPointer); gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, texCoordPointer); gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); gl.glLoadIdentity(); gl.glRotatef(lookupdown, 1.0f, 0.0f, 0.0f); gl.glRotatef(sceneroty, 0.0f, 1.0f, 0.0f); gl.glTranslatef(xtrans, ytrans, ztrans); gl.glBindTexture(GL10.GL_TEXTURE_2D, texture[1]); for(TRIANGLE triangle : sector1.triangle) { vertexPointer.clear(); texCoordPointer.clear(); gl.glNormal3f(0.0f, 0.0f, 1.0f); for(int i=0; i<3; i++) { VERTEX vt = triangle.vertex[i]; vertexPointer.put(vt.x); vertexPointer.put(vt.y); vertexPointer.put(vt.z); texCoordPointer.put(vt.u); texCoordPointer.put(vt.v); } gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 4); } gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY); gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); }
剩下一个就是onSurfaceChanged(GL10 gl, int width, int height)啦,这个跟前面的说的区别不大,直接看代码吧。。
@Override public void onSurfaceChanged(GL10 gl, int width, int height) { float ratio = (float) width / height; //设置OpenGL场景的大小 gl.glViewport(0, 0, width, height); //设置投影矩阵 gl.glMatrixMode(GL10.GL_PROJECTION); //重置投影矩阵 gl.glLoadIdentity(); // 设置视口的大小 gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10); // 选择模型观察矩阵 gl.glMatrixMode(GL10.GL_MODELVIEW); // 重置模型观察矩阵 gl.glLoadIdentity(); }
其实,到这里就可以说已经搞定了3D的场景效果啦。。不过这个静态的。。
下面就将通过按键事件处理镜头的移动和旋转,具体代码如下:
public boolean onKeyUp(int keyCode, KeyEvent event) { switch ( keyCode ) { case KeyEvent.KEYCODE_DPAD_LEFT: yrot -= 1.5f; break; case KeyEvent.KEYCODE_DPAD_RIGHT: yrot += 1.5f; break; case KeyEvent.KEYCODE_DPAD_UP: // 沿游戏者所在的X平面移动 xpos -= (float)Math.sin(heading*piover180) * 0.05f; // 沿游戏者所在的Z平面移动 zpos -= (float)Math.cos(heading*piover180) * 0.05f; if (walkbiasangle >= 359.0f)// 如果walkbiasangle大于359度 { walkbiasangle = 0.0f;// 将 walkbiasangle 设为0 } else { walkbiasangle+= 10;// 如果 walkbiasangle < 359 ,则增加 10 } // 使游戏者产生跳跃感 walkbias = (float)Math.sin(walkbiasangle * piover180)/20.0f; break; case KeyEvent.KEYCODE_DPAD_DOWN: // 沿游戏者所在的X平面移动 xpos += (float)Math.sin(heading*piover180) * 0.05f; // 沿游戏者所在的Z平面移动 zpos += (float)Math.cos(heading*piover180) * 0.05f; // 如果walkbiasangle小于1度 if (walkbiasangle <= 1.0f) { walkbiasangle = 359.0f;// 使 walkbiasangle 等于 359 } else { walkbiasangle-= 10;// 如果 walkbiasangle > 1 减去 10 } // 使游戏者产生跳跃感 walkbias = (float)Math.sin(walkbiasangle * piover180)/20.0f; break; } return false; }
因为有按键事件,所以要在MAinActivity.java 中加上如下代码:
@Override public boolean onKeyUp(int keyCode, KeyEvent event){ renderer.onKeyUp(keyCode, event); return super.onKeyUp(keyCode, event); }
额。。结束了哦。。期待效果图了吧。。见下图吧:
其实,这个还有许多的地方可以加以改进的,比如可以用鼠标或者触笔来实现对场景的缩放,毕竟现在很多的手机和平板都没有按键的了。。(PS:LZ最近很忙,就不弄这个了哈。。等改天有空再来改进吧)
还有就是考虑到程序运行的效率,可以减少处理镜头背面的三角形的绘制,也即是观察者视线不能看到的地方,这样会使程序运行得更加流畅。。(PS:还是那就是,LZ最近很忙。。)
代码下载链接:http://download.csdn.net/detail/klcf0220/5526237
参考链接:http://developer.android.com/guide/topics/graphics/opengl.html
http://www.cnblogs.com/android100/archive/2012/06/27/2565438.html
喜欢开源,乐意分享的大神们,欢迎加入QQ群:176507146,你值的拥有哦!