第4章 千里之行,始于足下——3D开发基础知识
千里之行,始于足下,本章笔者将结合一些简单有趣的小例子,来介绍移动设备应用中如何运用OpenGL ES标准开发3D效果,包括OpenGL ES的介绍,3D场景中物体的绘制方式,以及3D场景中的投影技术。
4.1 OpenGL ES简介
OpenGL是由SGI公司开发的一套3D图形软件接口标准,由于具有体系结构简单合理、使用方便、与操作平台无关等优点,OpenGL迅速成为了一种3D图形接口的标准,并陆续在各种平台上得以实现,而OpenGL ES就是众多版本中的一个子集。
4.1.1 师出名门的OpenGL ES
OpenGL ES(OpenGL for Embedded Systems)是专为内嵌和移动设备设计的一个2D/3D轻量图形库,它是OpenGL三维图形API的子集,是根据手持和移动平台的特点对OpenGL 3D图形API标准进行裁剪定制而成的。
由于3D UI的移动嵌入式应用受到随时移动等因素的影响,一般不能提供无线的电源连接,只能在有限大的电源限制下工作。如何以更低的功耗完成3D场景的渲染,成为OpenGL推出OpenGL ES标准的原因。OpenGL ES标准在渲染3D场景的同时,也达到了降低功耗的效果。
OpenGL ES基本上是OpenGL1.3的子集,同时加入了一些扩展。这使得该API更加灵活,比如现在一下用不到的功能可以先暂时删除,当内嵌硬件发展到一定水平后,再将相应的功能添加进来。
目前OpenGL ES主要包含两方面的基本内容:Commom Profile支持全3D功能,保证游戏正常运行;Safety Critiacl Profile,是商业软件设计所需要的,华丽的界面在这里不是做优先考虑的,安全性才是关键,它只能提供最小化的3D功能。
大家也不要以为OpenGL ES是OpenGL的缩水版而觉得很差劲,新发布的Sony PlayStation 3采用的就是OpenGL ES,可见OpenGL ES还是比较强大的,足以承载各种3D游戏的渲染,图4-1和图4-2就是游戏中采用OpenGL ES标准来渲染3D场景的效果图。
图4-1 游戏场景一截图
图4-2 游戏场景二截图
4.1.2 三角形组成的世界
3D场景中的3D模型的最基本单位是称为顶点的vertex,它代表三维空间中的一个点。通过它们(顶点)可以构建简单的二维图形,而通过不同的二维图形则可搭建复杂的三维立体模型。例如,多边形就是由点构成的,而三维物体是由多个多边形组成的。如图4-3所示的国家大剧院就好像是由成千上万个四边形组成的。
图4-3 国家大剧院
尽管OpenGL支持多种多边形,但是很不幸的是OpenGL ES目前只支持三角形,这主要是出于性能的原因。但从性能来说,支持三角形跟支持其他多边形没有多大区别,因为任何多边形都可以拆分成多个三角形,只是相对麻烦一点儿。
OpenGL ES采用的是三维笛卡儿坐标系,如图4-4所示。通常开发中的3D场景都要对应到这个坐标系中去,场景内构建的所用或部分物体的顶点的X、Y、Z坐标值以顶点数组的形式给出。一个顶点数组是包括场景中部分或所有顶点坐标数据的简单数组。例如,场景中有n个顶点,则坐标值有3n个,则顶点数组的尺寸为3n。
图4-4 三维笛卡儿坐标系
前面已经提到过,OpenGL ES中只允许使用三角形进行填充。如图4-5给出的示意图,明确地阐述了用平面三角形搭建的立体模型的原理。
图4-5 长方体示意图
从图中可以看出这是一个长方体,有6个面,每个面都是一个矩形,并且每个矩形都可以切分成两个三角形。因此在OpenGL ES中,一个长方体可以通过12个三角形来搭建。
提示:其他形状的立体模型同样可以通过三角形来搭建,有兴趣的读者可以去尝试一下,笔者就不再赘述了。
OpenGL ES中有一项功能叫做背面剪裁,含义是打开背面剪裁功能后,视角在一个三角形的背面时不渲染此三角形,即看不到这个三角形,此功能可以大大提高渲染效率。
因此,很重要的就是确定在既定的观察方向上渲染三角形,否则就有可能出现看不到图像的情况。三角形的正反面是这样确定的,当面对一个三角形时,若顶点的顺序是逆时针则位于三角形的正面,反之则是反面,如图4-6所示。
图4-6 三角形的卷绕
提示:上述是默认情况,也可以自己设置正反面的顶点卷绕顺序,在后面的章节中会为读者介绍。
4.1.3 第一个OpenGL ES三角形
上一小节介绍了在OpenGL ES的3D世界中各物体都是由三角形搭建的,接下来笔者将带领读者写一个简单的例子,来更详细地为读者介绍如何在应用中绘制一个三角形。具体步骤如下。
(1)打开Eclipse,导入Sample4_1的项目。
(2)然后介绍layout目录下的main.xml文件。
代码位置:本书随书光盘中源代码\第4章\Sample4_1\res\layout\main.xml。
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:orientation="vertical" //垂直方向 4 android:id="@+id/main_liner" //线性布局的ID 5 android:layout_width="fill_parent" //填充整个屏幕 6 android:layout_height="fill_parent"> 7 </LinearLayout>
● 第2~7行定义了一个垂直方向上的线性布局。
● 第3行,设置线性布局的方向为垂直方向,还可以设置为水平方向horizontal。
● 第4行,自定义此线性布局的ID。
● 第5~6行,设置此线性布局的大小为填充整个屏幕。
(3)下面介绍MyActivity.java类的代码内容。
代码位置:本书随书光盘中源代码\第4章\Sample4_1\src\wyf\swq\MyActivity.java。
1 package wyf.swq; 2 import android.app.Activity; //引入相关包 3 import android.os.Bundle; 4 import android.widget.LinearLayout; 5 public class MyActivity extends Activity { 6 /** Called when the activity is first created. */ 7 private MySurfaceView mSurfaceView; //声明MySurfaceView对象 8 @Override 9 public void onCreate(Bundle savedInstanceState) { 10 super.onCreate(savedInstanceState); /继承父类的onCreate()方法 11 setContentView(R.layout.main); //设置布局文件 12 mSurfaceView=new MySurfaceView(this); //创建MySurfaceView对象 13 mSurfaceView.requestFocus(); //获取焦点 14 mSurfaceView.setFocusableInTouchMode(true); / /设置为可触控 15 LinearLayout ll=(LinearLayout)this.findViewById(R.id.main_liner); //获得线性布局的引用 16 ll.addView(mSurfaceView); //将视图加载到线性布局中 17 } 18 @Override 19 protected void onPause() { 20 // TODO Auto-generated method stub 21 super.onPause(); //继承父类的onPause()方法 22 mSurfaceView.onPause(); //调用onPause()方法 23 } 24 @Override 25 protected void onResume() { 26 // TODO Auto-generated method stub 27 super.onResume(); //继承父类的onResume()方法 28 mSurfaceView.onResume(); //调用onResume()方法 29 } 30 }
● 第8~17行,重写onCreate()方法,在创建时为Activity设置布局。
● 第18~23行,重写onPause()方法,在暂停的同时保存mSurfaceView。
● 第24~29行,重写onResume()方法,在恢复的同时恢复mSurfaceView。
(4)接下来介绍MySurfaceView.java类的代码内容。
代码位置:本书随书光盘中源代码\第4章\Sample4_1\src\wyf\swq\MySurfaceView.java。
1 package wyf.swq; 2 import javax.microedition.khronos.egl.EGLConfig; //引入相关包 3 import javax.microedition.khronos.opengles.GL10; 4 import android.content.Context; 5 import android.opengl.GLSurfaceView; //引入相关包 6 import android.view.MotionEvent; 7 public class MySurfaceView extends GLSurfaceView { 8 private final float TOUCH_SCALE_FACTOR=180.0f/320; //角度缩放比例 9 private SceneRenderer myRenderer; //场景渲染器 10 private float myPreviousY; //上次屏幕上的触控位置的Y坐标 11 private float myPreviousX; //上次屏幕上的触控位置的X坐标 12 public MySurfaceView(Context context) { 13 super(context); 14 // TODO Auto-generated constructor stub 15 myRenderer=new SceneRenderer(); //创建场景渲染器 16 this.setRenderer(myRenderer); //设置渲染器 17 this.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); //渲染模式为主动渲染 18 } 19 @Override 20 public boolean onTouchEvent(MotionEvent event) { //触摸事件回调方法 21 // TODO Auto-generated method stub 22 float y=event.getY(); //获得当前触点的Y坐标 23 float x=event.getX(); //获得当前触点的X坐标 24 switch(event.getAction()){ 25 case MotionEvent.ACTION_MOVE: 26 float dy=y-myPreviousY; //滑动距离在y轴方向上的垂直距离 27 float dx=x-myPreviousX; //活动距离在x轴方向上的垂直距离 28 myRenderer.tr.yAngle+=dx*TOUCH_SCALE_FACTOR; //设置沿y轴旋转角度 29 myRenderer.tr.zAngle+=dy*TOUCH_SCALE_FACTOR; //设置沿z轴旋转角度 30 requestRender(); //渲染画面 31 } 32 myPreviousY=y; //前一次触控位置的Y坐标 33 myPreviousX=x; //前一次触控位置的X坐标 34 return true; //事件成功返回true 35 } 36 private class SceneRenderer implements GLSurfaceView.Renderer{ 37 Triangle tr=new Triangle(); //创建三角形对象 38 public SceneRenderer(){ //构造器 39 } 40 @Override 41 public void onDrawFrame(GL10 gl) { //重写onDrawFrame()方法 42 // TODO Auto-generated method stub 43 gl.glEnable(GL10.GL_CULL_FACE); //设置为打开背面剪裁 44 gl.glShadeModel(GL10.GL_SMOOTH); //设置着色模型为平滑着色 45 gl.glFrontFace(GL10.GL_CCW); //设置自定义卷绕顺序:逆时针为正面 46 gl.glClear(GL10.GL_COLOR_BUFFER_BIT|GL10.GL_DEPTH_BUFFER_BIT); //清除缓存 47 gl.glMatrixMode(GL10.GL_MODELVIEW); //设置当前矩阵为模式矩阵 48 gl.glLoadIdentity(); //设置当前矩阵为单位矩阵 49 gl.glTranslatef(0, 0, -2.0f); //把坐标系往z轴负方向平移2.0f个单位 50 tr.drawSelf(gl); //调用具体绘制方法 51 } 52 @Override 53 public void onSurfaceChanged(GL10 gl, int width, int height) { //重写方法 54 // TODO Auto-generated method stub 55 gl.glViewport(0, 0, width, height); //设置视口大小和位置 56 gl.glMatrixMode(GL10.GL_PROJECTION); //设置矩阵为投影矩阵 57 gl.glLoadIdentity(); //设置矩阵为单位矩阵 58 float ratio=(float)width/height; //比例大小 59 gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10); //设置投影模式 60 } 61 @Override 62 public void onSurfaceCreated(GL10 gl, EGLConfig config) { //重写方法 63 // TODO Auto-generated method stub 64 gl.glDisable(GL10.GL_DITHER); //关闭抗抖动 65 gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST); //设置模式 66 gl.glClearColor(0, 0, 0, 0); //设置屏幕背景色为黑色 67 gl.glEnable(GL10.GL_DEPTH_TEST); //启用深度检测 68 }}}
● 第2~17行,引入相关类及自定义视图来加载图像。
● 第8行,角度缩放比例,即屏幕上滑动的距离对应到物体旋转的角度值。
● 第12~17行,构造器,初始化时加载渲染器,并将渲染器模式设置为主动渲染,即画面发生变化时能自动重新绘制新场景。
● 第19~35行,重写了触控事件回调方法,这里主要用于计算在屏幕上滑动多少距离对应物体应该旋转多少度。
● 第36~68行,定义渲染器类,并实现其内部的相关方法,用于渲染场景。
● 第40~51行,重写了onDrawFrame()方法,主要用于绘制画面时被调用,包括执行绘制方法及之前的准备工作,包括背面剪裁、平滑着色、自定义卷绕、清除颜色、深度缓存、设置矩阵模式,以及调整空间坐标系位置。
● 第52~60行,重写了onSurfaceChanged()方法,主要用于当屏幕横竖发生变化时被调用,可调整视口,具体实现下一小节介绍。
● 第61~68行,重写了onSurfaceCreated()方法,主要用于创建SurfaceView时被调用,用于初始化相关设置,包括屏幕背景色、深度检测等。
(5)接着将介绍Triangle.java类的代码内容。该类用于构造三角形。
代码位置:本书随书光盘中源代码\第4章\Sample4_1\src\wyf\swq\Triangle.java。
1 package wyf.swq; 2 import java.nio.ByteBuffer; //引入相关包 3 import java.nio.ByteOrder; 4 import java.nio.IntBuffer; 5 import javax.microedition.khronos.opengles.GL10; //引入相关包 6 public class Triangle { 7 private IntBuffer myVertexBuffer; //顶点坐标数据缓冲 8 private IntBuffer myColorBuffer; //顶点着色数据缓冲 9 private ByteBuffer myIndexBuffer; //顶点构建的索引数据缓冲 10 int vCount=0; //顶点数量 11 int iCount=0; //索引数量 12 float yAngle=0; //绕y轴旋转的角度 13 float zAngle=0; //绕z轴旋转的角度 14 public Triangle(){ 15 vCount=3; //设定顶点数量 16 final int UNIT_SIZE=10000; //缩放比例 17 int []vertices=new int[] { //创建顶点数据数组 18 -8*UNIT_SIZE,6*UNIT_SIZE,0, 19 -8*UNIT_SIZE,-6*UNIT_SIZE,0, 20 8*UNIT_SIZE,-6*UNIT_SIZE,0 21 }; 22 //创建顶点坐标数据缓存,由于不同平台字节顺序不同,数据单元不是字节的要经ByteBuffer转换 23 ByteBuffer vbb=ByteBuffer.allocateDirect(vertices.length*4); //开辟新的内存块 24 vbb.order(ByteOrder.nativeOrder()); //设置为本地平台的字节顺序 25 myVertexBuffer=vbb.asIntBuffer(); //转换为int型缓冲 26 myVertexBuffer.put(vertices); //向缓冲区中放入顶点坐标数据 27 myVertexBuffer.position(0); //设置缓冲区的起始位置 28 final int one=65535; //支持65535色色彩通道 29 int []colors=new int[]{ //创建顶点颜色值数组 30 one,one,one,0, //每个顶点4个色彩值RGBA 31 one,one,one,0, 32 one,one,one,0 33 }; 34 ByteBuffer cbb=ByteBuffer.allocateDirect(colors.length*4); //开辟新的内存块 35 cbb.order(ByteOrder.nativeOrder()); //设置为本地平台的字节顺序 36 myColorBuffer=cbb.asIntBuffer(); //转换为int型缓冲 37 myColorBuffer.put(colors); //向缓冲区中放入顶点颜色坐标数据 38 myColorBuffer.position(0); //设置缓冲区的起始位置 39 //为三角形构造索引数据初始化 40 iCount=3; //设定索引数量 41 byte []indices=new byte[] { //创建索引数组 42 0,1,2 43 }; 44 //创建三角形构造索引数据缓冲 45 myIndexBuffer=ByteBuffer.allocateDirect(indices.length); //开辟新的内存块 46 myIndexBuffer.put(indices); //向缓冲区中放入索引坐标数据 47 myIndexBuffer.position(0); //设置缓冲区的起始位置 48 } 49 public void drawSelf(GL10 gl){ //实现具体绘制方法 50 gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); //启用顶点坐标数组 51 gl.glEnableClientState(GL10.GL_COLOR_ARRAY); //启用顶点颜色数组 52 gl.glRotatef(yAngle,0,1,0); //根据yAngle的角度值,绕y轴旋转yAngle 53 gl.glRotatef(zAngle,0,0,1); //根据zAngle的角度值,绕z轴旋转zAngle 54 gl.glVertexPointer( //为画笔指定顶点坐标数据 55 3, //每个顶点的坐标数量为3 56 GL10.GL_FIXED, //顶点坐标值的类型为GL_FIXED,整型 57 0, //连续顶点坐标数据之间的间隔 58 myVertexBuffer //顶点坐标来源 59 ); 60 gl.glColorPointer( //为画笔指定顶点颜色数据 61 4, //每个顶点的颜色值数量为4 62 GL10.GL_FIXED, //顶点坐标值的类型为GL_FIXED,整型 63 0, //连续顶点坐标数据之间的间隔 64 myColorBuffer //顶点颜色值来源 65 ); 66 gl.glDrawElements( //绘制图形 67 GL10.GL_TRIANGLES, //填充模式,这里是以三角形方式填充的 68 iCount, //顶点数量 69 GL10.GL_UNSIGNED_BYTE, //索引值的类型 70 myIndexBuffer //索引值数据 71 ); 72 }}
● 第2~72行,引入相关类,定义要绘制的Triangle类。
● 第7~13行,声明相关变量,包括顶点缓存、顶点颜色缓存、顶点索引缓存、顶点数、索引数及旋转角度等变量。
● 第14~48行,Triangle类的构造器,用于初始化相关数据,包括初始化三角形的顶点数据缓冲,初始化三角形的颜色数据缓冲,初始化三角形的索引数据缓冲。
● 第15~27行,初始化三角形的顶点数据缓冲,创建整型类型的顶点数据数组。由于创建的顶点数据数组的元素是整型的,所以需要通过ByteBuffer将其转换为本地平台的字节顺序。
● 第28~39行,初始化三角形的颜色数据缓冲,方式与初始化顶点数组相似,不再赘述。
● 第39~47行,初始化三角形的索引数据缓冲,创建索引数据数组,创建索引数据缓冲,由于其数据单元是字节而不需要通过ByteBuffer将其转换为本地平台的字节顺序。
● 第49~71行,定义应用程序中具体实现场景物体的绘制方法,包括启用相应数组,实现场景中物体的旋转,为画笔指定顶点坐标数据,为画笔指定顶点颜色数据,并用画笔实现绘图。
(6)最终实现效果图如图4-7所示,图4-7(a)为正面时截图,图4-7(b)为旋转时截图。
图4-7(a) 正面时截图
图4-7(b) 旋转时截图
4.2 不同的绘制方式
在前面的例子中绘制三角形时采用的是GL_TRIANGLES绘制模式,其实OpenGL ES还支持很多种绘制模式,分别针对不同情况。下面就开始一一介绍。
4.2.1 点和线的绘制
前面说过OpenGL ES世界中3D模型最基本的单位是点,任何其他模型都是通过点来构建的,而点与点之间最简单的联系就是线。
俗话说“磨刀不误砍柴工”,在介绍例子之前,先为大家介绍一下在OpenGL ES中有哪些绘制方式,以及每种绘制方式的特点,再了解了这些知识之后再为读者讲解例子。
1. GL_POINTS
把每个顶点作为一个点进行处理,索引数组中的第n个顶点即定义了点n,共绘制N个点。例如,索引数组{0,1,2,3,4}。效果如图4-8所示的点的绘制。
图4-8 点的绘制示意图
提示:n代表顶点编号,N代表顶点的个数。索引数组中的值代表顶点数组中顶点的编号。
2. GL_LINES
把每两个顶点作为一条独立的线段面,索引数组中的第2n和2n+1顶点定义了第n条线段,总共绘制了 N/2条线段。如果 N 为奇数,则忽略最后一个顶点。例如,索引数组{0,3,2,1}。效果如图4-9所示的线的第1种绘制。
图4-9 线的第1种绘制示意图
3. GL_LINE_STRIF
绘制索引数组中从第0个顶点到最后一个顶点依次相连的一组线段,第n和n+1个顶点定义了线段n,总共绘制N-1条线段。例如,索引数组{0,3,2,1}。效果如图4-10所示的线的第2种绘制。
图4-10 线的第2种绘制示意图
4. GL_LINE_LOOP
绘制索引数组中从第0个顶点到最后一个顶点依次相连的一组线段,最终最后一个顶点与第0个顶点相连。第n和n+1个顶点定义了线段n,最后一条线段是由顶点N-1和0之间定义,总共绘制N条线段。例如,索引数组{0,3,2,1}。效果如图4-11所示的线的第3种绘制。
图4-11 线的第3种绘制示意图
5. GL_TRIANGLES
把索引数组中的每3个顶点作为一个独立三角形。索引数组中第3n、3n+1和3n+2顶点定义了第n个三角形,总共绘制N/3个三角形。例如,索引数组{0,1,2,2,1,3}。效果如图4-12所示的三角形的绘制1。
图4-12 三角形的绘制1示意图
6. GL_TRIANGLE_STRIP
绘制一组相连的三角形。对于索引数组中的第n个点:若n为奇数,第n+1,n和n+2顶点定义了第n个三角形;若n为偶数,第n,n+1和n+2顶点定义了第n个三角形。总共绘制N-2个三角形。例如,索引数组{0,1,2,3,4}。效果如图4-13所示的三角形的绘制2。
图4-13 三角形的绘制2示意图
7. GL_TRIANGLE_FAN\
绘制一组相连的三角形。三角形是由索引数组中的第0个顶点及其后给定的顶点所确定。顶点0,n+1和n+2定义了第n个三角形,总共绘制N-2个三角形。例如,索引数组{0,1,2,3,4}。效果如图4-14所示的三角形的绘制3。
图4-14 三角形的绘制3示意图
由于之前已经向读者介绍过了绘制三角形模式的例子,所以下面笔者就带领大家通过一个简单的小例子为大家介绍如何通过上述不同的绘制方式绘制点跟线。具体步骤如下。
(1)打开Eclipse,导入名为:Sample4_2的项目。
(2)然后向读者介绍layout目录下的main.xml文件,但由于本例与上一小节Sample4_1项目的这部分代码相同,所以请读者参考上一小节相关代码,这里不再赘述。
代码位置:本书随书光盘中源代码\第4章\Sample4_2\res\layout\main.xml。
(3)关于MyActivity.java类的介绍。由于本例与上一小节Sample4_1项目的这部分代码相同,所以同样请读者参考上一小节相关代码,这里不再赘述。
代码位置:本书随书光盘中源代码\第4章\Sample4_2\src\wyf\swq\MyActivity.java。
(4)关于对MySurfaceView.java类的介绍,由于本例与上一小节Sample4_1项目的这部分代码基本相同,所以只需将相应位置的代码替换成对应代码即可,其余部分这里不再赘述。
代码位置:本书随书光盘中源代码\第4章\Sample4_2\src\wyf\swq\MySurfaceView.java。
将Sample4_1项目中MySurfaceView的第37行代码替换成如下代码。
代码位置:本书随书光盘中源代码\第4章\Sample4_1\src\wyf\swq\MySurfaceView.java。
1 Points p=new Points(); //创建点类对象 2 Lines l=new Lines(); //创建线类对象
提示:第1~2行,分别创建点类对象和线类对象。
将Sample4_1项目中MySurfaceView的第49~50行代码替换成如下代码。
代码位置:本书随书光盘中源代码\第4章\Sample4_1\src\wyf\swq\MySurfaceView.java。
1 gl.glTranslatef(0.5f, 0, -2.0f); //移动坐标系 2 p.drawSelf(gl); //调用绘制点的方法 3 gl.glTranslatef(-1.0f,0,0); //移动坐标系 4 l.drawSelf(gl); //调用绘制线的方法
● 第1~2行,将坐标系往z轴负方向移动2个单位,以便能通过视口看到图像,再往x轴正方向移动0.5个单位,再绘制点,以便点在右边出现。
● 第3~4行,再将坐标系往x轴负方向移动1个单位,再绘制线,以便在左边出现。
● 在做平移旋转操作时,应当先进行平移操作,之后再进行旋转操作。
(5)下面介绍Points.java类的代码内容。
代码位置:本书随书光盘中源代码\第4章\Sample4_2\src\wyf\swq\Points.java。
1 package wyf.swq; 2 import java.nio.ByteBuffer; //引入相关包 3 import java.nio.ByteOrder; 4 import java.nio.IntBuffer; 5 import javax.microedition.khronos.opengles.GL10; 6 public class Triangle { 7 private IntBuffer myVertexBuffer; //顶点坐标数据缓冲 8 private IntBuffer myColorBuffer; //顶点着色数据缓冲 9 private ByteBuffer myIndexBuffer; //顶点构建的索引数据缓冲 10 int vCount=0; //顶点数量 11 int iCount=0; //索引数量 12 float yAngle=0; //绕y轴旋转的角度 13 float zAngle=0; //绕z轴旋转的角度 14 public Triangle(){ 15 vCount=4; //设定顶点数量 16 final int UNIT_SIZE=10000; //缩放比例 17 int []vertices=new int[] { //创建顶点数据数组 18 -2*UNIT_SIZE,3*UNIT_SIZE,0, 19 1*UNIT_SIZE,1*UNIT_SIZE,0, 20 -1*UNIT_SIZE,-2*UNIT_SIZE,0, 21 2*UNIT_SIZE,-3*UNIT_SIZE,0 22 }; 23 //创建顶点坐标数据缓存,由于不同平台字节顺序不同,数据单元不是字节的要经ByteBuffer转换 24 ByteBuffer vbb=ByteBuffer.allocateDirect(vertices.length*4); //开辟新的内存块 25 vbb.order(ByteOrder.nativeOrder()); //设置为本地平台的字节顺序 26 myVertexBuffer=vbb.asIntBuffer(); //转换为int型缓冲 27 myVertexBuffer.put(vertices); //向缓冲区中放入顶点坐标数据 28 myVertexBuffer.position(0); //设置缓冲区的起始位置 29 final int one=65535; //支持65535色色彩通道 30 int []colors=new int[]{ //创建顶点颜色值数组 31 one,one,one,0, //每个顶点4个色彩值RGBA 32 one,one,one,0, 33 one,one,one,0, 34 one,one,one,0 35 }; 36 ByteBuffer cbb=ByteBuffer.allocateDirect(colors.length*4); //开辟新的内存块 37 cbb.order(ByteOrder.nativeOrder()); //设置为本地平台的字节顺序 38 myColorBuffer=cbb.asIntBuffer(); //转换为int型缓冲 39 myColorBuffer.put(colors); //向缓冲区放入顶点颜色坐标数据 40 myColorBuffer.position(0); //设置缓冲区的起始位置 41 //为三角形构造索引数据初始化 42 iCount=4; //设定索引数量 43 byte []indices=new byte[] { //创建索引数组 44 0,1,2 45 }; 46 //创建三角形构造索引数据缓冲 47 myIndexBuffer=ByteBuffer.allocateDirect(indices.length); //开辟新的内存块 48 myIndexBuffer.put(indices); //向缓冲区中放入索引坐标数据 49 myIndexBuffer.position(0); //设置缓冲区的起始位置 50 } 51 public void drawSelf(GL10 gl){ //实现具体绘制方法 52 gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); //启用顶点坐标数组 53 gl.glEnableClientState(GL10.GL_COLOR_ARRAY); //启用顶点颜色数组 54 gl.glRotatef(yAngle,0,1,0); //根据yAngle的角度值,绕y轴旋转yAngle 55 gl.glRotatef(zAngle,0,0,1); //根据zAngle的角度值,绕z轴旋转zAngle 56 gl.glVertexPointer( //为画笔指定顶点坐标数据 57 3, //每个顶点的坐标数量为3 58 GL10.GL_FIXED, //顶点坐标值的类型为GL_FIXED,整型 59 0, //连续顶点坐标数据之间的间隔 60 myVertexBuffer //顶点坐标来源 61 ); 62 gl.glColorPointer( //为画笔指定顶点颜色数据 63 4, //每个顶点的颜色值数量为4 64 GL10.GL_FIXED, //顶点坐标值的类型为GL_FIXED,整型 65 0, //连续顶点坐标数据之间的间隔 66 myColorBuffer //顶点颜色值来源 67 ); 68 gl.glDrawElements( //绘制图形 69 GL10.GL_POINTS, //以点方式填充 70 iCount, //顶点数量 71 GL10.GL_UNSIGNED_BYTE, //索引值的类型 72 myIndexBuffer //索引值数据 73 ); 74 }}
● 第2~74行,引入相关类,定义要绘制的Triangle类。
● 第7~13行,声明相关变量,包括顶点缓存、顶点颜色缓存、顶点索引缓存、顶点数、索引数及旋转角度等变量。
● 第14~50行,Triangle类的构造器,用于初始化相关数据,包括初始化三角形的顶点数据缓冲,初始化三角形的颜色数据缓冲,初始化三角形的索引数据缓冲。
● 第15~27行,初始化三角形的顶点数据缓冲,创建整型类型的顶点数据数组,由于创建的顶点数据数组的元素是整型的,所以需要通过ByteBuffer将其转换为本地平台的字节顺序。
● 第28~39行,初始化三角形的颜色数据缓冲,方式与初始化顶点数组相似,不再赘述。
● 第39~47行,初始化三角形的索引数据缓冲,创建索引数据数组,创建索引数据缓冲,由于其数据单元是字节而不需要通过ByteBuffer将其转换为本地平台的字节顺序。
● 第49~74行,定义应用程序中具体实现场景物体的绘制方法,包括启用相应数组,实现场景中物体的旋转,为画笔指定顶点坐标数据,为画笔指定顶点颜色数据,并用画笔实现绘图。
(6)接着介绍Lines.java类的构造方法。
代码位置:本书随书光盘中源代码\第4章\Sample4_2\src\wyf\swq\Lines.java。
由于此类与上述的Points类基本相似,所以只需将相应位置的代码替换成对应代码即可,其余部分这里不再赘述。
将Sample4_2项目中Points.java的第68~73行代码替换成如下代码。
代码位置:本书随书光盘中源代码\第4章\Sample4_2\src\wyf\swq\Points.java。
1 gl.glDrawElements( //索引法绘制图形 2 GL10.GL_LINES, //以线方式填充 3 iCount, //索引数量 4 GL10.GL_UNSIGNED_BYTE, //索引值的类型 5 myIndexBuffer //索引值数据 6 );
提示:第2行,与Points.java类不同的是,这里启用了以线的方式填充的方法,这样在绘制时就绘制这种模式的线了。
(7)最终效果如图4-15所示的点和线的绘制。
图4-15 点和线的绘制
由上述例子读者可以发现,要绘制什么模式的点、线和三角形时,在绘制方法的第1个参数中设置为上述介绍的你所需要的绘制模式即可。有兴趣的读者可以自己尝试一下,笔者就不再这里赘述了。
4.2.2 索引法绘制三角形
细心的读者可能发现,之前的例子都是通过索引法的方式绘制的。所谓索引法就是通过调用gl.glDrawElements()方法绘制各种简单的几何图形。
方法glDrawElements(int mode, int count, int type,Buffer indices)一共有4个参数,分别是mode定义什么样的图元被画出来,count定义一共有多少个索引值,type定义索引数组使用的类型,indices表示绘制顶点使用的索引缓存。
下面笔者就通过一个更复杂一点的例子来介绍如何使用索引法来绘制图形,并介绍一些新的特效和功能及其实现。具体步骤如下。
(1)打开Eclipse,导入名为Sample4_3的项目。
(2)下面介绍main.xml的相关配置。
代码位置:本书随书光盘中源代码\第4章\Sample4_3\res\layout\main.xml。
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:orientation="vertical" //设置为垂直方向 4 android:id="@+id/main_liner" //ID编号 5 android:layout_width="fill_parent" //填充整个屏幕 6 android:layout_height="fill_parent" 7 > 8 <ToggleButton 9 android:textOff="打开背面剪裁" //按钮上的文字 10 android:textOn="关闭背面剪裁" 11 android:checked="false" //不设置默认 12 android:id="@+id/ToggleButton01" //ID编号 13 android:layout_width="fill_parent" //横向填满 14 android:layout_height="wrap_content"> //纵向适中 15 </ToggleButton> 16 <ToggleButton 17 android:textOff="打开平滑着色" //按钮上的文字 18 android:textOn="关闭平滑着色" 19 android:checked="false" //不设置默认 20 android:id="@+id/ToggleButton02" //ID编号 21 android:layout_width="fill_parent" //横向填满 22 android:layout_height="wrap_content"> //纵向适中 23 </ToggleButton> 24 <ToggleButton 25 android:textOff="打开自定义卷绕" //按钮上的文字 26 android:textOn="使用默认卷绕" 27 android:checked="false" //不设置默认 28 android:id="@+id/ToggleButton03" //ID编号 29 android:layout_width="fill_parent" //横向填满 30 android:layout_height="wrap_content"> //纵向适中 31 </ToggleButton> 32 </LinearLayout>
提示:第2~32行,定义了一个线性布局,方向为竖直方向,横纵填满整个屏幕,其中包含了3个开关按钮,并设置成横向填满、纵向适中的形式。
(3)接着介绍MyActivity.java类的代码内容。
代码位置:本书随书光盘中源代码\第4章\Sample4_3\src\wyf\swq\MyActivity.java。
1 package wyf.swq; 2 import android.app.Activity; //引入相关包 3 import android.os.Bundle; 4 import android.widget.CompoundButton; 5 import android.widget.LinearLayout; 6 import android.widget.ToggleButton; 7 import android.widget.CompoundButton.OnCheckedChangeListener; 8 public class MyActivity extends Activity { 9 /** Called when the activity is first created. */ 10 private MySurfaceView mSurfaceView; //声明MySurfaceView对象 11 @Override 12 public void onCreate(Bundle savedInstanceState) { 13 super.onCreate(savedInstanceState); //继承父类方法 14 setContentView(R.layout.main); //设置布局文件 15 mSurfaceView=new MySurfaceView(this); //创建MySurfaceView对象 16 mSurfaceView.requestFocus(); //获取焦点 17 mSurfaceView.setFocusableInTouchMode(true); //设置为可触控 18 LinearLayout ll=(LinearLayout)this.findViewById(R.id.main_liner); //获得线性布局的引用 19 ll.addView(mSurfaceView);// 20 ToggleButton tb01=(ToggleButton)this.findViewById(R.id.ToggleButton01); //获得开关按钮的引用 21 tb01.setOnCheckedChangeListener(new FirstListener()); //为按钮注册监听器 22 ToggleButton tb02=(ToggleButton)this.findViewById(R.id.ToggleButton02); //获得开关按钮的引用 23 tb02.setOnCheckedChangeListener(new SecondListener()); 24 ToggleButton tb03=(ToggleButton)this.findViewById(R.id.ToggleButton03); //获得开关按钮的引用 25 tb03.setOnCheckedChangeListener(new ThirdListener()); 26 } 27 class FirstListener implements OnCheckedChangeListener{ //声明第1个按钮的监听器 28 @Override 29 public void onCheckedChanged(CompoundButton buttonView, //重写方法 30 boolean isChecked) { 31 // TODO Auto-generated method stub 32 mSurfaceView.setBackFlag(!mSurfaceView.isBackFlag()); //实现功能 33 } 34 } 35 class SecondListener implements OnCheckedChangeListener{ //声明按钮的监听器 36 @Override 37 public void onCheckedChanged(CompoundButton buttonView, //重写方法 38 boolean isChecked) { 39 // TODO Auto-generated method stub 40 mSurfaceView.setSmoothFlag(!mSurfaceView.isSmoothFlag()); //实现功能 41 } 42 } 43 class ThirdListener implements OnCheckedChangeListener{ //声明按钮的监听器 44 @Override 45 public void onCheckedChanged(CompoundButton buttonView, //重写方法 46 boolean isChecked) { 47 // TODO Auto-generated method stub 48 mSurfaceView.setSelfCulling(!mSurfaceView.isSelfCulling());//实现功能 49 } 50 } 51 @Override 52 protected void onPause() { 53 // TODO Auto-generated method stub 54 super.onPause(); //继承父类onPause()方法 55 mSurfaceView.onPause(); //调用onPause()方法 56 } 57 @Override 58 protected void onResume() { 59 // TODO Auto-generated method stub 60 super.onResume(); //继承父类onResume()方法 61 mSurfaceView.onResume(); //调用onResume()方法 62 } 63 }
● 第2~10行,引入了相关包,并声明了MySurfaceView对象。
● 第11~26行,重写了onCreate()方法,即在创建MyActivity对象时为Activity设置布局文件,为布局文件中的按钮视图添加相应的监听器。
● 第27~50行,定义了为布局文件中的按钮添加的监听器类。分别用于监听背面剪裁、平滑着色、自定义卷绕3个按钮,并实现相应的功能。
● 第51~56行,重写onPause()方法,继承父类的方法,同时将MySurfaceView视图也挂起。
● 第57~62行,重写onResume()方法,继承父类的方法,同时将MySurfaceView视图也恢复。
(4)对MySurfaceView.java场景类做详细介绍。
代码位置:本书随书光盘中源代码\第4章\Sample4_3\src\wyf\swq\MySurfaceView.java。
1 package wyf.swq; 2 import javax.microedition.khronos.egl.EGLConfig; //引入相关包 3 import javax.microedition.khronos.opengles.GL10; 4 import android.content.Context; 5 import android.opengl.GLSurfaceView; 6 import android.view.MotionEvent; 7 public class MySurfaceView extends GLSurfaceView { 8 private final float TOUCH_SCALE_FACTOR=180.0f/320; //角度缩放比例 9 private SceneRenderer myRenderer; //场景渲染器 10 private boolean backFlag=false; //是否打开背面剪裁的标志位 11 private boolean smoothFlag=false; //是否打开平面着色的标志位 12 private boolean selfCulling=false; //是否采用自定义卷绕顺序的标志位 13 private float myPreviousY; //上次屏幕上的触控位置的Y坐标 14 private float myPreviousX; //上次屏幕上的触控位置的X坐标 15 public MySurfaceView(Context context) { 16 super(context); 17 // TODO Auto-generated constructor stub 18 myRenderer=new SceneRenderer(); //创建场景渲染器 19 this.setRenderer(myRenderer); //设置渲染器 20 this.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); //渲染模式为主动渲染 21 } 22 public void setBackFlag(boolean flag){ //设置背面剪裁的标志位 23 this.backFlag=flag; 24 } 25 public boolean isBackFlag(){ //设置平面着色的标志位 26 return backFlag; 27 } 28 public void setSmoothFlag(boolean flag){ //设置自定义卷绕顺序的标志位 29 this.smoothFlag=flag; 30 } 31 public boolean isSmoothFlag(){ //判断背面剪裁的标志位 32 return smoothFlag; 33 } 34 public void setSelfCulling(boolean flag){ //判断平面着色的标志位 35 this.selfCulling=flag; 36 } 37 public boolean isSelfCulling(){ //判断自定义卷绕顺序的标志位 38 return selfCulling; 39 } 40 //触摸事件回调方法 41 @Override 42 public boolean onTouchEvent(MotionEvent event) { 43 // TODO Auto-generated method stub 44 float y=event.getY(); //获得当前触点的Y坐标 45 float x=event.getX(); //获得当前触点的X坐标 46 switch(event.getAction()){ 47 case MotionEvent.ACTION_MOVE: 48 float dy=y-myPreviousY; //滑动距离在y轴方向上的垂直距离 49 float dx=x-myPreviousX; //活动距离在x轴方向上的垂直距离 50 myRenderer.tp.yAngle+=dx*TOUCH_SCALE_FACTOR; //设置沿y轴的旋转角度 51 myRenderer.tp.zAngle+=dy*TOUCH_SCALE_FACTOR; //设置沿z轴的旋转角度 52 requestRender(); //渲染画面 53 } 54 myPreviousY=y; //上一次触点的Y坐标 55 myPreviousX=x; //上一次触点的X坐标 56 return true; 57 } 58 private class SceneRenderer implements GLSurfaceView.Renderer{ 59 TrianglePair tp=new TrianglePair(); //创建三角形对象 60 public SceneRenderer(){} //渲染器构造器 61 @Override 62 public void onDrawFrame(GL10 gl) { 63 // TODO Auto-generated method stub 64 if(backFlag){ 65 gl.glEnable(GL10.GL_CULL_FACE); //设置打开背面剪裁 66 } 67 else{ 68 gl.glDisable(GL10.GL_CULL_FACE); //设置关闭背面剪裁 69 } 70 71 if(smoothFlag){ 72 gl.glShadeModel(GL10.GL_SMOOTH); //设置着色模型为平滑着色 73 } 74 else{ 75 gl.glShadeModel(GL10.GL_FLAT); //设置着色模型为不平滑着色 76 } 77 78 if(selfCulling){ 79 gl.glFrontFace(GL10.GL_CW); //设置自定义卷绕顺序:顺时针为正面 80 } 81 else{ 82 gl.glFrontFace(GL10.GL_CCW); //设置自定义卷绕顺序:逆时针为正面 83 } 84 gl.glClear(GL10.GL_COLOR_BUFFER_BIT|GL10.GL_DEPTH_BUFFER_BIT);//清除缓存 85 gl.glMatrixMode(GL10.GL_MODELVIEW); //设置当前矩阵为模式矩阵 86 gl.glLoadIdentity(); //设置当前矩阵为单位矩阵 87 gl.glTranslatef(0, 0, -2.0f); //移动坐标系 88 tp.drawSelf(gl); //绘制图形 89 } 90 @Override 91 public void onSurfaceChanged(GL10 gl, int width, int height) { 92 // TODO Auto-generated method stub 93 gl.glViewport(0, 0, width, height); //设置视口 94 gl.glMatrixMode(GL10.GL_PROJECTION); //设置为投影矩阵 95 gl.glLoadIdentity(); //设置为单位矩阵 96 float ratio=(float)width/height; //设置视口比例 97 gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10); //设置为透视投影 98 } 99 @Override 100 public void onSurfaceCreated(GL10 gl, EGLConfig config) { 101 // TODO Auto-generated method stub 102 gl.glDisable(GL10.GL_DITHER); //关闭抗抖动 103 gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,GL10.GL_FASTEST); // Hint模式 104 gl.glClearColor(0, 0, 0, 0); //设置屏幕背景色为黑色 105 gl.glEnable(GL10.GL_DEPTH_TEST); //启用深度检测 106 }}}
● 第2~14行,引入了相关包,并声明了相关变量和对象。
● 第15~21行,MySurfaceView的构造器,即在创建MySurfaceView对象的同时,为其设置渲染器及之渲染模式。
● 第22~39行,定义了设置及判断背面剪裁、平滑着色、自定义卷绕标志位的方法。
● 第40~57行,定义了触摸回调方法,实现屏幕触控,以及在屏幕上滑动而使场景物体旋转的功能。
● 第58~106行,定义了渲染器内部类,主要实现图像的渲染、屏幕横竖发生变化时的措施及创建MySurfaceView时初始化一些功能。
● 第62~89行,重写onDrawFrame()方法,具体实施背面剪裁、平滑着色、自定义卷绕3个功能,清除颜色缓冲,平移变化,实际调用绘制方法,从而绘制不同时刻的图像。
● 第90~98行,重写onSurfaceChanged()方法,在屏幕横竖空间位置发生变化时自动调用,具体功能及实现将在下一小节介绍,这里不再赘述。
● 第99~106行,重写onSurfaceCreated()方法,当MySurfaceView创建时被调用,用于初始化一些功能,包括屏幕背景颜色、绘制模式、是否深度检测等。
(5)然后介绍TrianglePair.java类的内容。
代码位置:本书随书光盘中源代码\第4章\Sample4_3\src\wyf\swq\TrianglePair.java。
1 package wyf.swq; 2 import java.nio.ByteBuffer; //引入相关包 3 import java.nio.ByteOrder; 4 import java.nio.IntBuffer; 5 import javax.microedition.khronos.opengles.GL10; 6 public class TrianglePair { 7 private IntBuffer myVertexBuffer; //顶点坐标数据缓冲 8 private IntBuffer myColorBuffer; //顶点着色数据缓冲 9 private ByteBuffer myIndexBuffer; //顶点索引数据缓冲 10 int vCount=0; //顶点数量 11 int iCount=0; //索引数量 12 float yAngle=0; //绕y轴旋转的角度 13 float zAngle=0; //绕z轴旋转的角度 14 public TrianglePair(){ 15 vCount=6; //设置顶点数量 16 final int UNIT_SIZE=10000; //缩放比例 17 int []vertices=new int[]{ 18 -8*UNIT_SIZE,10*UNIT_SIZE,0, 19 -2*UNIT_SIZE,2*UNIT_SIZE,0, 20 -8*UNIT_SIZE,2*UNIT_SIZE,0, 21 8*UNIT_SIZE,2*UNIT_SIZE,0, 22 8*UNIT_SIZE,10*UNIT_SIZE,0, 23 2*UNIT_SIZE,10*UNIT_SIZE,0 24 }; 25 //创建顶点坐标数据缓存,不同平台字节顺序不同,数据单元不是字节的要经过ByteBuffer转换 26 ByteBuffer vbb=ByteBuffer.allocateDirect(vertices.length*4); //分配的内存块 27 vbb.order(ByteOrder.nativeOrder()); //设置本地平台的字节顺序 28 myVertexBuffer=vbb.asIntBuffer(); //转换为int型缓冲 29 myVertexBuffer.put(vertices); //向缓冲区中放入顶点坐标数据 30 myVertexBuffer.position(0); //设置缓冲区的起始位置 31 final int one=65535; //支持65535色色彩通道 32 int []colors=new int[]{ //顶点颜色值数组 33 one,one,one,0, 34 0,0,one,0, 35 0,0,one,0, 36 one,one,one,0, 37 one,0,0,0, 38 one,0,0,0 39 }; 40 ByteBuffer cbb=ByteBuffer.allocateDirect(colors.length*4); //分配的内存块 41 cbb.order(ByteOrder.nativeOrder()); //设置本地平台的字节顺序 42 myColorBuffer=cbb.asIntBuffer(); //转换为int型缓冲 43 myColorBuffer.put(colors); //向缓冲区中放入顶点颜色数据 44 myColorBuffer.position(0); //设置缓冲区的起始位置 45 //为三角形构造索引数据初始化 46 iCount=6; 47 byte []indices=new byte[]{ //创建索引数组 48 0,1,2, 49 3,4,5 50 }; 51 //创建三角形构造索引数据缓冲 52 myIndexBuffer=ByteBuffer.allocateDirect(indices.length); //分配的内存块 53 myIndexBuffer.put(indices); //向缓冲区中放入顶点索引数据 54 myIndexBuffer.position(0); //设置缓冲区的起始位置 55 } 56 public void drawSelf(GL10 gl){ 57 gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); //启用顶点坐标数组 58 gl.glEnableClientState(GL10.GL_COLOR_ARRAY); //启用顶点颜色数组 59 gl.glRotatef(yAngle,0,1,0); //绕y轴旋转yAngle 60 gl.glRotatef(zAngle,0,0,1); 61 gl.glVertexPointer( //为画笔指定顶点坐标数据 62 3, //每个顶点的坐标数量为3 63 GL10.GL_FIXED, //顶点坐标值的类型为整型 64 0, //连续顶点坐标数据之间的间隔 65 myVertexBuffer //顶点坐标数量 66 ); 67 gl.glColorPointer( //为画笔指定顶点颜色数据 68 4, 69 GL10.GL_FIXED, 70 0, 71 myColorBuffer 72 ); 73 gl.glDrawElements( //索引法绘制图形 74 GL10.GL_TRIANGLES, //以三角形方式填充 75 iCount, //索引数量 76 GL10.GL_UNSIGNED_BYTE, //索引类型 77 myIndexBuffer //顶点数量 78 ); 79 }}
● 第2~79行,引入相关类,定义要绘制的TrianglePair类。
● 第7~13行,声明相关变量,包括顶点缓存、顶点颜色缓存、顶点索引缓存、顶点数、索引数及旋转角度等变量。
● 第14~55行,Triangle类的构造器,用于初始化相关数据,包括初始化三角形的顶点数据缓冲、初始化三角形的颜色数据缓冲、初始化三角形的索引数据缓冲。
● 第15~30行,初始化三角形的顶点数据缓冲,创建整型类型的顶点数据数组,由于创建的顶点数据数组的元素是整型的,所以需要通过ByteBuffer将其转换为本地平台的字节顺序。
● 第31~44行,初始化三角形的颜色数据缓冲,方式与初始化顶点数组相似,不再赘述。
● 第45~54行,初始化三角形的索引数据缓冲,创建索引数据数组,创建索引数据缓冲,由于其数据单元是字节故不需要通过ByteBuffer将其转换为本地平台的字节顺序。
● 第56~78行,定义应用程序中具体实现场景物体的绘制方法,包括启用相应数组、实现场景中物体的旋转、为画笔指定顶点坐标数据、为画笔指定顶点颜色数据,并用画笔实现绘图。
从上述代码中,读者可以发现使用索引法绘制图像,就是在建立顶点缓冲后,通过索引缓冲中的索引去任意调用顶点缓冲中的对应顶点来排列顶点顺序,从而绘制各个位置上的点来达到绘制图形的目的的。
(6)最终效果如图4-16所示的三角形对。
图4-16(a) 三角形对
图4-16(b) 开启平滑着色
另外,从上述案例中还可以看出,OpenGL ES程序在执行真正的绘制工作之前有很多准备工作,下面就来梳理一下。
1. 清除颜色缓存与深度缓存
即代码gl.glClear(GL10.GL_COLOR_BUFFER_BIT|GL10.GL_DEPTH_BUFFER_BIT),OpenGL ES保存了一系列缓存(buffers),即用于绘制各方面的内存块。颜色缓存保存当前帧各像素的颜色。基本上就是用户在屏幕上看到的情况。
深度缓存保存了每个潜在像素离观察者距离的信息,使用此信息可以确定一个像素是否需要被绘制出来。这两个缓存是OpenGL ES中最常见的缓存。还有其他类型的一些缓存,如模板缓存和累计缓存等。
图4-16(c) 开启背面剪裁
图4-16(d) 开启自定义卷绕
2. 加载单位变换矩阵
即代码gl.glMatrixMode(GL10.GL_MODELVIEW)和gl.glLoadIdentity(),此工作将清除虚拟世界中的一切平移、旋转、缩放或其他变化,并将观察者置于原点。
4.2.3 顶点法绘制三角形
顶点法绘制图形与索引法绘制图形的区别就是,顶点法没有建立索引缓冲,而是直接在建立顶点缓冲的同时,将顶点数组中的顶点排好顺序,最后在调用绘制方法时使用顶点缓冲。
由于都是绘制三角形的例子,所以前面的代码参见Sample4_3即可,不同的就是TrianglePair.java类,下面笔者就通过代码带大家具体了解一下顶点法的使用。
代码位置:本书随书光盘中源代码\第4章\Sample4_4\src\wyf\swq\TrianglePair.java。
1 package wyf.swq; 2 import java.nio.ByteBuffer; //引入相关包 3 import java.nio.ByteOrder; 4 import java.nio.IntBuffer; 5 import javax.microedition.khronos.opengles.GL10; 6 public class TrianglePair { 7 private IntBuffer myVertexBuffer; //顶点坐标数据缓冲 8 private IntBuffer myColorBuffer; //顶点着色数据缓冲 9 int vCount=0; //顶点数量 10 float yAngle=0; //绕y轴旋转的角度 11 float zAngle=0; //绕z轴旋转的角度 12 public TrianglePair(){ 13 vCount=6; //设置顶点数量 14 final int UNIT_SIZE=10000; //缩放比例 15 int []vertices=new int[]{ 16 -8*UNIT_SIZE,10*UNIT_SIZE,0, 17 -2*UNIT_SIZE,2*UNIT_SIZE,0, 18 -8*UNIT_SIZE,2*UNIT_SIZE,0, 19 8*UNIT_SIZE,2*UNIT_SIZE,0, 20 8*UNIT_SIZE,10*UNIT_SIZE,0, 21 2*UNIT_SIZE,10*UNIT_SIZE,0 22 }; 23 //创建顶点坐标数据缓存,不同平台字节顺序不同,数据单元不是字节的要经过ByteBuffer转换 24 ByteBuffer vbb=ByteBuffer.allocateDirect(vertices.length*4); //分配的内存块 25 vbb.order(ByteOrder.nativeOrder()); //设置本地平台的字节顺序 26 myVertexBuffer=vbb.asIntBuffer(); //转换为int型缓冲 27 myVertexBuffer.put(vertices); //向缓冲区中放入顶点坐标数据 28 myVertexBuffer.position(0); //设置缓冲区的起始位置 29 final int one=65535; //支持65535色色彩通道 30 int []colors=new int[]{ //顶点颜色值数组 31 one,one,one,0, 32 0,0,one,0, 33 0,0,one,0, 34 one,one,one,0, 35 one,0,0,0, 36 one,0,0,0 37 }; 38 ByteBuffer cbb=ByteBuffer.allocateDirect(colors.length*4); //分配的内存块 39 cbb.order(ByteOrder.nativeOrder()); //设置本地平台的字节顺序 40 myColorBuffer=cbb.asIntBuffer(); //转换为int型缓冲 41 myColorBuffer.put(colors); //向缓冲区中放入顶点颜色数据 42 myColorBuffer.position(0); //设置缓冲区的起始位置 43 } 44 public void drawSelf(GL10 gl){ 45 gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); //启用顶点坐标数组 46 gl.glEnableClientState(GL10.GL_COLOR_ARRAY); //启用顶点颜色数组 47 gl.glRotatef(yAngle,0,1,0); //绕y轴旋转yAngle 48 gl.glRotatef(zAngle,0,0,1); 49 gl.glVertexPointer( //为画笔指定顶点坐标数据 50 3, //每个顶点的坐标数量为3 51 GL10.GL_FIXED, //顶点坐标值的类型为整型 52 0, //连续顶点坐标数据之间的间隔 53 myVertexBuffer //顶点坐标数量 54 ); 55 gl.glColorPointer( //为画笔指定顶点颜色数据 56 4, 57 GL10.GL_FIXED, 58 0, 59 myColorBuffer 60 ); 61 gl.glDrawElements( //顶点法绘制图形 62 GL10.GL_TRIANGLES, //以三角形方式填充 63 0, //开始点编号 64 vCount //顶点数量 65 ); 66 }}
● 第2~65行,引入相关类,定义要绘制的TrianglePair类。
● 第7~11行,声明相关变量,包括顶点缓存、顶点颜色缓存、顶点数及旋转角度等变量。
● 第132~43行,Triangle类的构造器,用于初始化相关数据,包括初始化三角形的顶点数据缓冲、初始化三角形的颜色数据缓冲,注意这里没有初始化三角形的索引数据缓冲。
● 第13~28行,初始化三角形的顶点数据缓冲,创建整型类型的顶点数据数组,由于创建的顶点数据数组的元素是整型的,所以需要通过ByteBuffer将其转换为本地平台的字节顺序。
● 第29~42行,初始化三角形的颜色数据缓冲,方式与初始化顶点数组相似,不再赘述。
● 第45~65行,定义应用程序中具体实现场景物体的绘制方法,包括启用相应数组、实现场景中物体的旋转、为画笔指定顶点坐标数据、为画笔指定顶点颜色数据,并用画笔实现绘图。注意这里调用的是glDrawElements()方法,即用顶点法绘制图形。
由于篇幅有限,这里不再贴图,最后呈现的效果和Sample4_3的运行结果一样,有兴趣的读者,可以运行源代码,查看运行效果图,笔者这里不再赘述。
4.3 不一样的投影,不一样的世界
经过前面的介绍,大家应该已经知道所谓三维系统最终还是要在二维的显示设备上显示,而将三维模型在二维显示设备上显示就需要进行投影。
在OpenGL ES中支持两种投影方式:正交投影和透视投影。
4.3.1 正交投影
所谓的正交投影是平行投影的一种,观察者的视线是平行的,不产生真实世界远大近小的透视效果。
在正交投影中,可视的空间区域为长方体,如图4-17所示。设置正交投影的语句为:gl.glOrthof(left, right, bottom, top, near, far)。
图4-17 正交投影示意图
其中的left与right为视口左右侧对应的x坐标,bottom与top为视口上下侧对应的y坐标,near与far为可视空间区域最近端与最远端的z坐标。要注意的是,OpenGL ES中不支持无限远的可视区域。视口就可以看做手机屏幕上的指定矩形区域。
4.3.2 透视投影
而透视投影属于非平行投影,观察者的视线在远处是相交的,视线相交即为灭点,如图4-18所示。通过透视投影,可以产生现实世界中近大远小的效果。因此使用透视投影可以得到更加真实的3D感受,游戏中较多采用的就是透视投影。
图4-18 透视投影示意图
从前面可以看出,在透视投影中,可视的空间区域为梯台,设置透视投影的语句为:gl.glFrustumf(left,right,bottom,top,near,far)。其中的left与right为视口左右侧对应的x坐标,bottom与top为视口上下侧对应的y坐标,near与far为可视空间区域最近端与最远端的z坐标。要注意的是,OpenGL ES中不支持无限远的可视区域。
同时在透视投影中还存在视角问题,这就和照相机的镜头分广角的和长焦的一样。视角大的情况下,在同样的距离上可以看到更广阔范围内的内容,如图4-19所示为视角的比较。
图4-19 视角的比较示意图
从上面两幅图中可以看出,左边的情况下视角小,右边的情况视角大。视角的计算公式如下:
a=2arctg(left/near)
上述公式是水平方向的视角,垂直方向的视角可以依次类推,公式为:
a=2arctg(top/near)
提示:上述两个计算公式是在左右或上下对称的情况下推导出来的,若左右或上下不对称,则两个半角要分别计算。
可以从上面的情况下总结出如下的简单规律:在left、right、top、bottom值不变的情况下,near值越小,视角越大。
在实际开发中,对left、right、top、bottom、near、far 6个值的调整是非常重要的,合适的值组可以产生很好的效果,不合适的值组甚至有可能影响内容的正确显示。
前面介绍中一直用到了视口的概念,也知道了视口可以看做手机屏幕上的指定矩形区域。一般情况下,在游戏娱乐应用程序中视口往往占用整个屏幕。
在OpenGL ES中视口的设置非常简单,使用如下语句:gl.glViewport(x,y,width,height);x、y为视口矩形在屏幕左上侧点的坐标,width、height为视口的宽度与高度。如图4-20所示为视口的设置。
图4-20 视口的设置示意图
4.3.3 两种投影的比较
在介绍了上述两种投影之后,笔者将带领读者通过一个简单的小例子来为大家介绍这两种投影的使用,并通过运行的结果,让读者实际体会到这两种投影的区别。具体步骤如下。
(1)打开Eclipse,导入名为Sample4_5的项目。
(2)然后介绍main.xml的相关配置。
代码位置:本书随书光盘中源代码\第4章\Sample4_5\res\layout\main.xml。
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:id="@+id/main_liner" //布局的ID 4 android:orientation="vertical" //布局的方向 5 android:layout_width="fill_parent" //横纵填满屏幕 6 android:layout_height="fill_parent"> 7 <ToggleButton 8 android:textOff="正交投影" //按钮文字 9 android:textOn="透视投影" 10 android:checked="false" //不设置默认值 11 android:id="@+id/ToggleButton01" //按钮ID 12 android:layout_width="fill_parent" //横向填满 13 android:layout_height="wrap_content"> //纵向适中 14 </ToggleButton> 15 </LinearLayout>
提示:第2~15行,定义了一个线性布局,方向为竖直方向,横纵填满整个屏幕,其中包含一个开关按钮,并设置成横向填满、纵向适中的形式。
(3)接着将介绍MyActivity.java控制类的代码内容。
代码位置:本书随书光盘中源代码\第4章\Sample4_5\src\wyf\swq\MyActivity.java。
1 package wyf.swq; 2 import wyf.swq.MySurfaceView; //引入相关包 3 import android.app.Activity; 4 import android.os.Bundle; 5 import android.widget.CompoundButton; 6 import android.widget.LinearLayout; 7 import android.widget.ToggleButton; 8 import android.widget.CompoundButton.OnCheckedChangeListener; 9 public class MyActivity extends Activity { 10 private MySurfaceView mSurfaceView; //声明MySurfaceView对象 11 @Override 12 protected void onCreate(Bundle savedInstanceState) { //重写onCreate方法 13 super.onCreate(savedInstanceState); 14 setContentView(R.layout.main); //设置布局 15 mSurfaceView = new MySurfaceView(this); //创建MySurfaceView对象 16 mSurfaceView.requestFocus(); //获取焦点 17 mSurfaceView.setFocusableInTouchMode(true); //设置为可触控 18 LinearLayout ll=(LinearLayout)findViewById(R.id.main_liner); //获得布局引用 19 ll.addView(mSurfaceView); //添加MySurfaceView对象 20 //控制是否打开背面剪裁的ToggleButton 21 ToggleButton tb=(ToggleButton)this.findViewById(R.id.ToggleButton01);//获得按钮引用 22 tb.setOnCheckedChangeListener(new MyListener()); //为按钮设置监听器 23 } 24 class MyListener implements OnCheckedChangeListener{ //定义监听器 25 @Override 26 public void onCheckedChanged(CompoundButton buttonView, 27 boolean isChecked) { 28 // TODO Auto-generated method stub 29 mSurfaceView.isPerspective=!mSurfaceView.isPerspective; //投影之间切换 30 mSurfaceView.requestRender(); //重新绘制 31 } 32 } 33 @Override 34 protected void onResume() { //重写onResume方法 35 super.onResume(); //继承父类的onResume方法 36 mSurfaceView.onResume(); //同时恢复MySurfaceView对象 37 } 38 @Override 39 protected void onPause() { //重写onPause方法 40 super.onPause(); //继承父类的onPause方法 41 mSurfaceView.onPause(); //同时挂起MySurfaceView对象 42 } }
● 第2~10行,引入了相关包,并声明了MySurfaceView对象。
● 第11~23行,重写了onCreate方法,即在创建MyActivity对象时为Activity设置布局文件,为布局文件中的开关按钮视图添加相应的监听器。
● 第24~31行,定义了为布局文件中的按钮添加的监听器类。主要用于两种投影之间进行切换的开关按钮,并实现相应的功能。
● 第33~37行,重写onPause方法,继承父类的方法,同时将MySurfaceView视图挂起。
● 第38~42行,重写onResume方法,继承父类的方法,同时将MySurfaceView视图恢复。
(4)然后介绍MySurfaceView.java类的相关内容。
代码位置:本书随书光盘中源代码\第4章\Sample4_5\src\wyf\swq\MySurfaceView.java。
1 package wyf.swq; 2 import android.opengl.GLSurfaceView; //引入相关包 3 import android.view.MotionEvent; 4 import javax.microedition.khronos.egl.EGLConfig; 5 import javax.microedition.khronos.opengles.GL10; 6 import android.content.Context; 7 class MySurfaceView extends GLSurfaceView { 8 private final float TOUCH_SCALE_FACTOR = 180.0f/320; //角度缩放比例 9 private SceneRenderer mRenderer; //场景渲染器 10 public boolean isPerspective=false; //投影标志位 11 private float mPreviousY; //上次的触控位置Y坐标 12 public float xAngle=0; //整体绕x轴旋转的角度 13 public MySurfaceView(Context context) { 14 super(context); 15 mRenderer = new SceneRenderer(); //创建场景渲染器 16 setRenderer(mRenderer); //设置渲染器 17 setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);//主动渲染 18 } 19 //触摸事件回调方法 20 @Override 21 public boolean onTouchEvent(MotionEvent e) { 22 float y = e.getY(); 23 switch (e.getAction()) { //获取动作 24 case MotionEvent.ACTION_MOVE: //判断是否是滑动 25 float dy = y - mPreviousY; //计算触控笔y位移 26 xAngle+= dy * TOUCH_SCALE_FACTOR; //设置沿x轴旋转角度 27 requestRender(); //重绘画面 28 } 29 mPreviousY = y; //作为上一次触点的Y坐标 30 return true; 31 } 32 private class SceneRenderer implements GLSurfaceView.Renderer { 33 Hexagon[] ha=new Hexagon[]{ //六边形数组 34 new Hexagon(0), 35 new Hexagon(-2), 36 new Hexagon(-4), 37 new Hexagon(-6), 38 new Hexagon(-8), 39 new Hexagon(-10), 40 new Hexagon(-12), 41 }; 42 public SceneRenderer(){} //渲染器构造类 43 @Override 44 public void onDrawFrame(GL10 gl) { 45 gl.glMatrixMode(GL10.GL_PROJECTION); //设置当前矩阵为投影矩阵 46 gl.glLoadIdentity(); //设置当前矩阵为单位矩阵 47 float ratio = (float) 320/480; //计算透视投影的比例 48 if(isPerspective){ 49 gl.glFrustumf(-ratio, ratio, -1, 1, 1f, 10); //调用此方法计算产生透视投影矩阵 50 } 51 else{ 52 gl.glOrthof(-ratio, ratio, -1, 1, 1, 10); //调用此方法计算产生正交投影矩阵 53 } 54 gl.glEnable(GL10.GL_CULL_FACE); //设置为打开背面剪裁 55 gl.glShadeModel(GL10.GL_SMOOTH); //设置着色模型为平滑着色 56 gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); //清除缓存 57 gl.glMatrixMode(GL10.GL_MODELVIEW); //设置当前矩阵为模式矩阵 58 gl.glLoadIdentity(); //设置当前矩阵为单位矩阵 59 gl.glTranslatef(0, 0f, -1.4f); //沿z轴向远处推 60 gl.glRotatef(xAngle, 1, 0, 0); //绕x轴旋转指定角度 61 for(Hexagon th:ha){ 62 th.drawSelf(gl); //循环绘制六边形数组中的每个六边形 63 } 64 } 65 @Override 66 public void onSurfaceChanged(GL10 gl, int width, int height) { 67 gl.glViewport(0, 0, width, height); //设置视窗大小及位置 68 } 69 @Override 70 public void onSurfaceCreated(GL10 gl, EGLConfig config) { 71 gl.glDisable(GL10.GL_DITHER); //关闭抗抖动 72 gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,GL10.GL_FASTEST); //设置Hint 73 gl.glClearColor(0,0,0,0); //设置屏幕背景色为黑色 74 gl.glEnable(GL10.GL_DEPTH_TEST); //启用深度测试 75 }}}
● 第2~12行,引入了相关包,并声明了相关变量和对象。
● 第13~18行,MySurfaceView的构造器,即在创建MySurfaceView对象的同时,为之设置渲染器及其渲染模式。
● 第19~31行,定义了触摸回调方法,实现屏幕触控,以及在屏幕上滑动使场景物体旋转的功能。
● 第32~75行,定义渲染器内部类,主要实现图像的渲染,屏幕横竖发生变化时的措施及创建MySurfaceView时初始化一些功能。
● 第33~42行,创建六边形数组,其每个元素分别是不同位置上的六边形,并定义渲染器构造器。
● 第43~64行,重写onDrawFrame方法,具体实施投影的设置,背面剪裁、平滑着色、自定义卷绕3个功能,清除颜色缓冲,平移变化,实际调用绘制方法,从而实现绘制不同时刻的图像。
● 第45~53行,加载投影矩阵,并根据开关设定的投影标准选择设置投影方式,由于这里设计给读者观察两种投影之间的切换,故将这些代码写在onDrawFrame方法中,平时开发都是固定一种投影模式,都会写在onSurfaceChanged中。
● 第65~68行,重写onSurfaceChanged方法,在屏幕横竖空间位置发生变化时自动调用,这里用于设置视口在屏幕上的位置及大小,是设置投影的一部分。由于加载投影矩阵和投影模式都已在onDrawFrame方法中写好,而视口设置一般不会有变化,故仍然写在这。
● 第70~75行,重写onSurfaceCreated方法,当MySurfaceView创建时被调用,用于初始化一些功能,包括屏幕背景颜色、绘制模式、是否深度检测等。
(5)接着对Hexagon.java类做相关介绍。
代码位置:本书随书光盘中源代码\第4章\Sample4_1\src\wyf\swq\Hexagon.java。
1 package wyf.swq; 2 import java.nio.ByteBuffer; //引入相关包 3 import java.nio.ByteOrder; 4 import java.nio.IntBuffer; 5 import javax.microedition.khronos.opengles.GL10; 6 public class Hexagon { 7 private IntBuffer mVertexBuffer; //顶点坐标数据缓冲 8 private IntBuffer mColorBuffer; //顶点着色数据缓冲 9 private ByteBuffer mIndexBuffer; //顶点构建索引数据缓冲 10 int vCount=0; //图形顶点数量 11 int iCount=0; //索引顶点数量 12 public Hexagon(int zOffset){ 13 //顶点坐标数据的初始化 14 vCount=7; //顶点数量 15 final int UNIT_SIZE=10000; 16 int vertices[]=new int[]{ //创建顶点数组 17 0*UNIT_SIZE,0*UNIT_SIZE,zOffset*UNIT_SIZE, //每个顶点3个坐标 18 2*UNIT_SIZE,3*UNIT_SIZE,zOffset*UNIT_SIZE, 19 4*UNIT_SIZE,0*UNIT_SIZE,zOffset*UNIT_SIZE, 20 2*UNIT_SIZE,-3*UNIT_SIZE,zOffset*UNIT_SIZE, 21 -2*UNIT_SIZE,-3*UNIT_SIZE,zOffset*UNIT_SIZE, 22 -4*UNIT_SIZE,0*UNIT_SIZE,zOffset*UNIT_SIZE, 23 -2*UNIT_SIZE,3*UNIT_SIZE,zOffset*UNIT_SIZE 24 }; 25 //创建顶点坐标数据缓冲 26 ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length*4); //分配新内存 27 vbb.order(ByteOrder.nativeOrder()); //设置字节顺序 28 mVertexBuffer = vbb.asIntBuffer(); //转换为int型缓冲 29 mVertexBuffer.put(vertices); //向缓冲区中放入顶点坐标数据 30 mVertexBuffer.position(0); //设置缓冲区起始位置 31 //顶点着色数据的初始化 32 final int one = 65535; 33 int colors[]=new int[]{ //创建顶点颜色值数组 34 0,0,one,0, //每个顶点4个色彩值RGBA 35 0,one,0,0, 36 0,one,one,0, 37 one,0,0,0, 38 one,0,one,0, 39 one,one,0,0, 40 one,one,one,0 41 }; 42 //创建顶点着色数据缓冲 43 ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length*4); //开辟新内存 44 cbb.order(ByteOrder.nativeOrder()); //设置字节顺序 45 mColorBuffer = cbb.asIntBuffer(); //转换为int型缓冲 46 mColorBuffer.put(colors); //向缓冲区中放入顶点着色数据 47 mColorBuffer.position(0); //设置缓冲区起始位置 48 //三角形构造索引数据初始化 49 iCount=18; //索引数量 50 byte indices[]=new byte[]{ //创建索引数组 51 0,2,1, 52 0,3,2, 53 0,4,3, 54 0,5,4, 55 0,6,5, 56 0,1,6 57 }; 58 //创建三角形构造索引数据缓冲 59 mIndexBuffer = ByteBuffer.allocateDirect(indices.length); //分配新内存 60 mIndexBuffer.put(indices); //向缓冲区放入数据 61 mIndexBuffer.position(0); //设置缓冲区起始位置 62 } 63 public void drawSelf(GL10 gl){ 64 gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); //启用顶点坐标数组 65 gl.glEnableClientState(GL10.GL_COLOR_ARRAY); //启用顶点颜色数组 66 gl.glVertexPointer( //为画笔指定顶点坐标数据 67 3, //每个顶点的坐标数量为3 68 GL10.GL_FIXED, //顶点坐标值的类型为GL_FIXED 69 0, //连续顶点坐标数据之间的间隔 70 mVertexBuffer //顶点坐标数据 71 ); 72 gl.glColorPointer ( //为画笔指定顶点着色数据 73 4, //设置每个颜色值的组成数量 74 GL10.GL_FIXED, //顶点颜色值的类型为GL_FIXED 75 0, //连续顶点着色数据之间的间隔 76 mColorBuffer //顶点着色数据 77 ); 78 gl.glDrawElements( //索引法绘制图形 79 GL10.GL_TRIANGLES, //以三角形方式填充 80 iCount, //索引数量 81 GL10.GL_UNSIGNED_BYTE, //索引值的尺寸 82 mIndexBuffer //索引值数据 83 ); 84 }}
● 第2~6行,引入相关类,定义要绘制的Hexagon类。
● 第7~11行,声明相关变量,包括顶点缓存、顶点颜色缓存、顶点索引缓存、顶点数、索引数等变量。
● 第12~62行,Hexagon类的构造器,用于初始化相关数据,包括初始化六边形的顶点数据缓冲,初始化六变形的颜色数据缓冲,初始化六边形的索引数据缓冲。其中,第14~30行,初始化六边形的顶点数据缓冲,创建整型类型的顶点数据数组,由于创建的顶点数据数组的元素是整型的,所以需要通过ByteBuffer将其转换为本地平台的字节顺序。第31~47行,初始化六边形的颜色数据缓冲,方式与初始化顶点数组相似,不再赘述。第48~61行,初始化六边形的索引数据缓冲,创建索引数据数组,创建索引数据缓冲,由于其数据单元是字节,故不需要通过ByteBuffer将其转换为本地平台的字节顺序。
● 第63~84行,定义应用程序中具体实现场景物体的绘制方法,包括启用相应数组,实现场景中物体的旋转,为画笔指定顶点坐标数据,为画笔指定顶点颜色数据,并用画笔实现绘图。
(6)最后效果图,如图4-21所示的正交投影效果和图4-22所示的透视投影效果。
图4-21 正交投影效果
图4-22 透视投影效果
从图中读者可以发现正交投影没有远大近小的效果,透视投影有近大远小的效果。
4.3.4 近大远小的原理
前面已经提到过正交投影不产生近大远小的效果,而透视投影会产生类似人通过肉眼观察现实世界一样的近大远小的效果,如图4-23所示为一组可视平行线在视觉效果上在很远处相交于一点。接下来将介绍近大远小的原理。
图4-23 近大远小示意图
(1)首先先介绍正交投影的产生原理,如图4-24所示。
图4-24 正交投影原理示意图
在正交投影下,视景体是一个平行的长方形,就相当于一个箱子。和透视投影不同,视景体两端的大小是相等的。物体和摄像机之间的距离并不影响它看上去的大小。当物体经过投影之后,保持它们的实际大小和角度。如图中,物体所占可视空间区域截面中的比例不因距离视平面的远近而改变。近处的物体和远处的物体投影到视平面上的大小相等。
(2)下面介绍一下透视投影的产生原理,如图4-25所示。
图4-25 透视投影原理示意图
透视投影最显著的特征就是透视缩短,物体距离摄像机(视平面)越远,它在最终图像中看上去就越小。这是因为透视投影的视景体可以看成是一个金字塔的平截头体(顶部被一个平行于底面的平面截除)。位于视景体内部的物体被投影到金字塔的顶点,也就是摄像机或观察点的位置。靠近观察点的物体看上去更大一些,因为和远处的物体相比,它们占据了视景体中相对较大的区域(如图,在可视空间区域截面中所占大小比例较大)。
4.4 本章小结
本章介绍了3D开发中的基础知识,包括OpenGL ES的介绍,以及OpenGL ES中绘制模型的原理,并通过点、线和三角形的绘制介绍了OpenGL ES中绘制模型的几种绘制方式。最后又介绍了3D场景中常用的两种投影方式,并透过例子比较了这两种投影的区别。笔者希望读者通过本章的学习能对3D有一个基本的了解,并为后续的学习提供帮助。