Android OpenGL ES 开发教程


Android OpenGL ES 开发教程 Android OpenGL ES 开发教程(1) 导言 Android ApiDemos 到目前为止,介绍完了出 View 以外的所有例子,在介绍 Graphics 示例时跳过了和 OpenGL ES 相关的例子,OpenGL ES 3D 图形开发 需要专门的开发教程,因此从今天开始一边继续 Android ApiDemos Views 例子 的解析,同时开始 Android OpenGL ES 开发教程。 在学习 Android OpenGL ES 开发之前,你必须具备 Java 语言开发经验和一些 Android 开发的基本知识,但并不需要有图形开发的经验,本教程也会涉及到一 些基本的线性几何知识,如矢量,矩阵运算等。 对于 Android 开发的基本知识,可以参见 Android 简明开发教程 ,特别注意的 是 Android 简明开发教程二:安装开发环境。本教程采用 Windows + Eclipse + Android SDK 作为开发的环境。 此外之前介绍的关于 Android OpenGL ES 开发的文章 Android OpenGL ES 开发中的 Buffer 使用 Android OpenGL ES 简明开发教程 也可以先看看,有助于学习 Android OpenGL ES 开发。 此外 Android SDK 中有关 OpenGL ES API 的开发文档 . android.opengl . javax.microedition.khronos.egl . javax.microedition.khronos.opengles . java.nio 注:上述 Android 文档基本为空,可以参见 JSR239 的文档,比较详细。 和 OpenGL ES Specification 都是学习时常用到的参考资料。 Android OpenGL ES 开发教程(2):关于 OpenGL ES 什么是 OpenGL ES? . OpenGL ES (为 OpenGL for Embedded System 的缩写) 为适用于嵌入 式系统的一个免费二维和三维图形库。 . 为桌面版本 OpenGL 的一个子集。 . OpenGL ES 定义了一个在移动平台上能够支持 OpenGL 最基本功能的精 简标准,以适应如手机,PDA 或其它消费者移动终端的显示系统。 . Khronos Group 定义和管理了 OpenGL ES 标准。 OpenGL 与 OpenGL ES 的关系 OpenGL ES 是基于桌面版本 OpenGL 的,下图显示了 OpenGL 和 OpenGL ES 之间的关系图 . OpenGL ES 1.0 基于 OpenGL 1.3 , 在 2003 年发布 . OpenGL ES 1.1 基于 OpenGL 1.5 , 在 2004 年发布 . OpenGL ES 2.0 基于 OpenGL2.0, 在 2007 年发布 . OpenGL 2.0 向下兼容 OpenGL 1.5 而 OpenGL ES 2.0 和 OpenGL ES 1.x 不兼容,是两种完全不同的实现。 OpenGL ES Profiles OpenGL ES 1.x 支持两种 Profile 以支持不同类型的嵌入设备。 1. The Common Profile 针对支持硬件浮点运算的设备,API 支持定点和浮 点运算。 2. The Common Lite Profile 针对不支持硬件浮点运算的设备,API 只支持 定点运算。 本教程主要针对 Common Profile 设备支持浮点运算。 Android OpenGL ES 开发教程(3) OpenGL ES 管道(Pipeline) 大部分图形系统都可以比作工厂中的装配线(Assemble line)或者称为管道 (Pipeline)。前一道的输出作为下道工序的输入。主 CPU 发出一个绘图指令,然 后可能由硬件部件完成坐标变换,裁剪,添加颜色或是材质,最后在屏幕上显示 出来。 OpenGL ES 1.x 的工序是固定的,称为 Fix-Function Pipeline,可以想象一个带 有很多控制开关的机器,尽管加工的工序是固定的,但是可以通过打开或关闭开 关来设置参数或者打开关闭某些功能。 OpenGL ES 2.0 允许提供编程来控制一些重要的工序,一些“繁琐”的工序比如 栅格化等仍然是固定的。 下图显示了 OpenGL ES 1.x 固定管道的结构图: . 管道“工序”大致可以分为 Transformation Stage 和 Rasterization Stage 两大步。 . OpenGL ES 支持的基本图形为 点 Point, 线 Line, 和三角形 Triangle , 其它所有复制图形都是通过这几种基本几何图形组合而成。 . 在发出绘图指令后,会对顶点(Vertices)数组进行指定的坐标变换或光照处 理。 . 顶点处理完成后,通过 Rasterizer 来生成像素信息,称为”Fragments“ 。 . 对于 Fragment 在经过 Texture Processing, Color Sum ,Fog 等处理并将 最终处理结果存放在内存中(称为 FrameBuffer)。 . OpenGL 2.0 可以通过编程来修改蓝色的步骤,称为 Programmable Shader. 以上管道中工序可以通过设置来打开或关闭某些功能(比如无需雾化 Fog 处理), 并可以为某个工序设置参数,比如设置 Vertext Array。 本教程主要介绍 OpenGL ES 1.1 编程,支持 OpenGL ES 2.0 的设备一般会同 时支持 OpenGL ES 1.1。 Android OpenGL ES 开发教程(4) OpenGL ES API 命名习惯 OpenGL ES 是个跨平台的 3D 图形开发包规范,最常见的实现是采用 C 语言实 现的,Android OpenGL ES 实现上是使用 Java 语言对底层的 C 接口进行了封 装,因此在 android.opengl javax.microedition.khronos.egl ,javax.microedition.khronos.opengles 包中定 义的 OpenGL 相关的类和方法带有很强的 C 语言色彩。 . 定义的常量都以 GL_为前缀。比如 GL10.GL_COLOR_BUFFER_BIT . OpenGL ES 指令以 gl 开头 ,比如 gl.glClearColor . 某些 OpenGL 指令以 3f 或 4f 结尾,3 和 4 代表参数的个数,f 代表参数类 型为浮点数,如 gl.glColor4f ,i,x 代表 int 如 gl.glColor4x . 对应以 v 结尾的 OpenGL ES 指令,代表参数类型为一个矢量(Vector) , 如 glTexEnvfv . 所有 8-bit 整数对应到 byte 类型,16-bit 对应到 short 类型,32-bit 整数(包 括 GLFixed)对应到 int 类型,而所有 32-bit 浮点数对应到 float 类型。 . GL_TRUE,GL_FALSE 对应到 boolean 类型 . C 字符串((char*)) 对应到 Java 的 UTF-8 字符串。 在前面 Android OpenGL ES 开发中的 Buffer 使用 说过 OpenGL ES 说过为了 提高性能,通常将顶点,颜色等值存放在 java.nio 包中定义的 Buffer 类中。下 表列出了 OpenGL ES 指令后缀, Java 类型,Java Buffer(java.nio)类型的对照 表 如下面代码 将为顶点指定 color 值,使用 FloatBuffer 来存放顶点的 Color 数组 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // The colors mapped to the vertices. float[] colors = { 1f, 0f, 0f, 1f, // vertex 0 red 0f, 1f, 0f, 1f, // vertex 1 green 0f, 0f, 1f, 1f, // vertex 2 blue 1f, 0f, 1f, 1f, // vertex 3 magenta }; ... // float has 4 bytes, colors (RGBA) * 4 bytes ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length * 4); cbb.order(ByteOrder.nativeOrder()); colorBuffer = cbb.asFloatBuffer(); colorBuffer.put(colors); colorBuffer.position(0); Android OpenGL ES 开发教程(5) 关于 EGL OpenGL ES 的 javax.microedition.khronos.opengles 包定义了平台无关的 GL 绘图指令,EGL(javax.microedition.khronos.egl ) 则定义了控制 displays ,contexts 以及 surfaces 的统一的平台接口。 . Display(EGLDisplay) 是对实际显示设备的抽象。 . Surface(EGLSurface)是对用来存储图像的内存区域 FrameBuffer 的抽 象,包括 Color Buffer, Stencil Buffer ,Depth Buffer. . Context (EGLContext) 存储 OpenGL ES 绘图的一些状态信息。 使用 EGL 的绘图的一般步骤: 1. 获取 EGLDisplay 对象 2. 初始化与 EGLDisplay 之间的连接。 3. 获取 EGLConfig 对象 4. 创建 EGLContext 实例 5. 创建 EGLSurface 实例 6. 连接 EGLContext 和 EGLSurface. 7. 使用 GL 指令绘制图形 8. 断开并释放与 EGLSurface 关联的 EGLContext 对象 9. 删除 EGLSurface 对象 10. 删除 EGLContext 对象 11. 终止与 EGLDisplay 之间的连接。 一般来说在 Android 平台上开发 OpenGL ES 应用,无需直接使用 javax.microedition.khronos.egl 包中的类按照上述步骤来使用 OpenGL ES 绘 制图形,在 Android 平台中提供了一个 android.opengl 包,类 GLSurfaceView 提供了对 Display,Surface,Context 的管理,大大简化了 OpenGL ES 的程序框架, 对应大部分 OpenGL ES 开发,只需调用一个方法来设置 OpenGLView 用到的 GLSurfaceView.Renderer。可以参见 Android OpenGL ES 简明开发教程二:构造 OpenGL ES View Android OpenGL ES 开发教程(6) GLSurfaceView Android OpenGL ES 相关的包主要定义在 . javax.microedition.khronos.opengles GL 绘图指令 . javax.microedition.khronos.egl EGL 管理 Display, surface 等 . android.opengl Android GL 辅助类,连接 OpenGL 与 Android View, Activity . javax.nio Buffer 类 其中 GLSurfaceView 为 android.opengl 包中核心类: . 起到连接 OpenGL ES 与 Android 的 View 层次结构之间的桥梁作用。 . 使得 Open GL ES 库适应于 Anndroid 系统的 Activity 生命周期。 . 使得选择合适的 Frame buffer 像素格式变得容易。 . 创建和管理单独绘图线程以达到平滑动画效果。 . 提供了方便使用的调试工具来跟踪 OpenGL ES 函数调用以帮助检查错误。 使用过 Java ME ,JSR 239 开发过 OpenGL ES 可以看到 Android 包 javax.microedition.khronos.egl ,javax.microedition.khronos.opengles 和 JSR239 基本一致,因此理论上不使用 android.opengl 包中的类也可以开发 Android 上 OpenGL ES 应用,但此时就需要自己使用 EGL 来管理 Display,Context, Surfaces 的创建,释放,捆绑,可以参见 Android OpenGL ES 开发教程(5):关于 EGL 。 使用 EGL 实现 GLSurfaceView 一个可能的实现如下: 1 2 3 4 5 6 class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable { public GLSurfaceView(Context context) { super(context); mHolder = getHolder(); mHolder.addCallback(this); 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 mHolder.setType(SurfaceHolder.SURFACE_TYPE_GPU); } public void setRenderer(Renderer renderer) { mRenderer = renderer; } public void surfaceCreated(SurfaceHolder holder) { } public void surfaceDestroyed(SurfaceHolder holder) { running = false; try { thread.join(); } catch (InterruptedException e) { } thread = null; } public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { synchronized(this){ mWidth = w; mHeight = h; thread = new Thread(this); 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 thread.start(); } } public interface Renderer { void EGLCreate(SurfaceHolder holder); void EGLDestroy(); int Initialize(int width, int height); void DrawScene(int width, int height); } public void run() { synchronized(this) { mRenderer.EGLCreate(mHolder); mRenderer.Initialize(mWidth, mHeight); running=true; while (running) { mRenderer.DrawScene(mWidth, mHeight); } mRenderer.EGLDestroy(); } } private SurfaceHolder mHolder; 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 private Thread thread; private boolean running; private Renderer mRenderer; private int mWidth; private int mHeight; } class GLRenderer implements GLSurfaceView.Renderer { public GLRenderer() { } public int Initialize(int width, int height){ gl.glClearColor(1.0f, 0.0f, 0.0f, 0.0f); return 1; } public void DrawScene(int width, int height){ gl.glClear(GL10.GL_COLOR_BUFFER_BIT); egl.eglSwapBuffers(eglDisplay, eglSurface); } public void EGLCreate(SurfaceHolder holder){ int[] num_config = new int[1]; EGLConfig[] configs = new EGLConfig[1]; int[] configSpec = { 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 EGL10.EGL_RED_SIZE, 8, EGL10.EGL_GREEN_SIZE, 8, EGL10.EGL_BLUE_SIZE, 8, EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT, EGL10.EGL_NONE }; this.egl = (EGL10) EGLContext.getEGL(); eglDisplay = this.egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); this.egl.eglInitialize(eglDisplay, null); this.egl.eglChooseConfig(eglDisplay, configSpec, configs, 1, num_config); eglConfig = configs[0]; eglContext = this.egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, null); eglSurface = this.egl.eglCreateWindowSurface(eglDisplay, eglConfig, holder, null); this.egl.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext); gl = (GL10)eglContext.getGL(); } 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 public void EGLDestroy(){ if (eglSurface != null) { egl.eglMakeCurrent(eglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); egl.eglDestroySurface(eglDisplay, eglSurface); eglSurface = null; } if (eglContext != null) { egl.eglDestroyContext(eglDisplay, eglContext); eglContext = null; } if (eglDisplay != null) { egl.eglTerminate(eglDisplay); eglDisplay = null; } } private EGL10 egl; private GL10 gl; private EGLDisplay eglDisplay; private EGLConfig eglConfig; private EGLContext eglContext; 112 113 private EGLSurface eglSurface; } 可以看到需要派生 SurfaceView ,并手工创建,销毁 Display,Context ,工作繁琐。 使用 GLSurfaceView 内部提供了上面类似的实现,对于大部分应用只需调用一 个方法来设置 OpenGLView 用到的 GLSurfaceView.Renderer. 1 public void setRenderer(GLSurfaceView.Renderer renderer) GLSurfaceView.Renderer 定义了一个统一图形绘制的接口,它定义了如下三个 接口函数: 1 2 3 4 5 6 // Called when the surface is created or recreated. public void onSurfaceCreated(GL10 gl, EGLConfig config) // Called to draw the current frame. public void onDrawFrame(GL10 gl) // Called when the surface changed size. public void onSurfaceChanged(GL10 gl, int width, int height) . onSurfaceCreated : 在这个方法中主要用来设置一些绘制时不常变化的 参数,比如:背景色,是否打开 z-buffer 等。 . onDrawFrame: 定义实际的绘图操作。 . onSurfaceChanged: 如果设备支持屏幕横向和纵向切换,这个方法将发生 在横向<->纵向互换时。此时可以重新设置绘制的纵横比率。 如果有需要,也可以通过函数来修改 GLSurfaceView 一些缺省设置: . setDebugFlags(int) 设置 Debug 标志。 . setEGLConfigChooser (boolean) 选择一个 Config 接近 16bitRGB 颜色模 式,可以打开或关闭深度(Depth)Buffer ,缺省为 RGB_565 并打开至少有 16bit 的 depth Buffer. . setEGLConfigChooser(EGLConfigChooser) 选择自定义 EGLConfigChooser。 . setEGLConfigChooser(int, int, int, int, int, int) 指定 red ,green, blue, alpha, depth ,stencil 支持的位数,缺省为 RGB_565 ,16 bit depth buffer. GLSurfaceView 缺省创建为 RGB_565 颜色格式的 Surface ,如果需要支持透明 度,可以调用 getHolder().setFormat(PixelFormat.TRANSLUCENT). GLSurfaceView 的渲染模式有两种,一种是连续不断的更新屏幕,另一种为 on-demand ,只有在调用 requestRender() 在更新屏幕。 缺省为 RENDERMODE_CONTINUOUSLY 持续刷新屏幕。 Android OpenGL ES 开发教程(7) 创建实例应用 OpenGLDemos 程序框架 有了前面关于 Android OpenGL ES 的介绍,可以开始创建示例程序 OpenGLDemos。 使用 Eclipse 创建一个 Android 项目 . Project Name: OpenGLDemos . Build Target: Android 1.6 ( >1.5 即可) . Application Name: Android OpenGL ES Demos . Package Name: com.pstreets.opengl.demo . Create Activity:AndroidOpenGLDemo 采用 Android ApiDemos 类似的方法,AndroidOpenGLDemo 为一 ListActivity , 可以使用 PackageManager 读取所有 Category 为 guidebee.intent.category.opengl.SAMPLE_CODE 的 Activity。 Android ApiDemos 示例解析(2): SimpleAdapter,ListActivity,PackageManager 创建一个 OpenGLRenderer 实现 GLSurfaceView.Renderer 接口: 1 2 3 4 5 6 7 public class OpenGLRenderer implements Renderer { private final IOpenGLDemo openGLDemo; public OpenGLRenderer(IOpenGLDemo demo){ openGLDemo=demo; } public void onSurfaceCreated(GL10 gl, EGLConfig config) { // Set the background color to black ( rgba ). 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 gl.glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Enable Smooth Shading, default not really needed. gl.glShadeModel(GL10.GL_SMOOTH); // Depth buffer setup. gl.glClearDepthf(1.0f); // Enables depth testing. gl.glEnable(GL10.GL_DEPTH_TEST); // The type of depth testing to do. gl.glDepthFunc(GL10.GL_LEQUAL); // Really nice perspective calculations. gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST); } public void onDrawFrame(GL10 gl) { if(openGLDemo!=null){ openGLDemo.DrawScene(gl); } } public void onSurfaceChanged(GL10 gl, int width, int height) { 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 // Sets the current view port to the new size. gl.glViewport(0, 0, width, height); // Select the projection matrix gl.glMatrixMode(GL10.GL_PROJECTION); // Reset the projection matrix gl.glLoadIdentity(); // Calculate the aspect ratio of the window GLU.gluPerspective(gl, 45.0f, (float) width / (float) height, 0.1f, 100.0f); // Select the modelview matrix gl.glMatrixMode(GL10.GL_MODELVIEW); // Reset the modelview matrix gl.glLoadIdentity(); } } 为简洁起见,为所有的示例定义了一个接口 IOpenGLDemo, 1 2 3 4 public interface IOpenGLDemo { public void DrawScene(GL10 gl); } DrawScene 用于实际的 GL 绘图示例代码,其它的初始化工作基本就由 GLSurfaceView 和 OpenGLRenderer 完成,其中 onSurfaceCreated 和 onSurfaceChanged 中的代码含义现在无需了解,后面会有具体介绍,只要知道 它们是用来初始化 GLSurfaceView 就可以了。 最后使用一个简单的例子“Hello World”结束本篇,“Hello World” 使用红色背景 刷新屏幕。 1 2 3 4 5 6 7 8 9 10 11 12 13 public class HelloWorld extends Activity implements IOpenGLDemo{ /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.requestWindowFeature(Window.FEATURE_NO_TITLE) ; getWindow() .setFlags(WindowManager.LayoutParams.FLAG_FULLSCRE EN, WindowManager.LayoutParams.FLAG_FULLSCREEN); mGLSurfaceView = new GLSurfaceView(this); 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 mGLSurfaceView.setRenderer(new OpenGLRenderer(this)); setContentView(mGLSurfaceView); } public void DrawScene(GL10 gl) { gl.glClearColor(1.0f, 0.0f, 0.0f, 0.0f); // Clears the screen and depth buffer. gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); } @Override protected void onResume() { // Ideally a game should implement // onResume() and onPause() // to take appropriate action when the //activity looses focus super.onResume(); mGLSurfaceView.onResume(); } @Override protected void onPause() { // Ideally a game should implement onResume() 35 36 37 38 39 40 41 42 //and onPause() // to take appropriate action when the //activity looses focus super.onPause(); mGLSurfaceView.onPause(); } private GLSurfaceView mGLSurfaceView; } 其对应在 AndroidManifest.xml 中的定义如下: < action android:name=”android.intent.action.MAIN” /> < /intent-filter> < /activity> 本例下载 Android OpenGL ES 开发教程(8) 基本几何图形定义 在前面 Android OpenGL ES 开发教程(7):创建实例应用 OpenGLDemos 程序 框架 我们创建了示例程序的基本框架,并提供了一个“Hello World”示例,将屏 幕显示为红色。 本例介绍 OpenGL ES 3D 图形库支持的几种基本几何图形,本篇部分内容与 Android OpenGL ES 简明开发教程三:3D 绘图基本概念 类似。 通常二维图形库可以绘制点,线,多边形,圆弧,路径等等。OpenGL ES 支持 绘制的基本几何图形分为三类:点,线段,三角形。也就是说 OpenGL ES 只能 绘制这三种基本几何图形。任何复杂的 2D 或是 3D图形都是通过这三种几何图 形构造而成的。 比如下图复杂的 3D 图形,都有将其分割成细小的三角形面而构成的。然后通过 上色(Color),添加材质(Texture),再添加光照(lighting),构造 3D效果的图形: 点,线段,三角形都是通过顶点来定义的,也就是顶点数组来定义。对应平面上 的一系列顶点,可以看出一个个孤立的点(Point),也可以两个两个连接成线段 (Line Segment) ,也可以三个三个连成三角形(Triangle)。这些对一组顶点的不 同解释就定义了 Android OpenGL ES 可以绘制的基本几何图形,下面定义了 OpenGL ES 定义的几种模式: GL_POINTS 绘制独立的点。 GL_LINE_STRIP 绘制一系列线段。 GL_LINE_LOOP 类同上,但是首尾相连,构成一个封闭曲线。 GL_LINES 顶点两两连接,为多条线段构成。 GL_TRIANGLES 每隔三个顶点构成一个三角形,为多个三角形组成。 GL_TRIANGLE_STRIP 每相邻三个顶点组成一个三角形,为一系列相接三角形构成。 GL_TRIANGLE_FAN 以一个点为三角形公共顶点,组成一系列相邻的三角形。 以上模式对应到 Android 渲染方法: OpenGL ES 提供了两类方法来绘制一个空间几何图形: . public abstract void glDrawArrays(int mode, int first, int count) 使用 VetexBuffer 来绘制,顶点的顺序由 vertexBuffer 中的顺序指定。 . public abstract void glDrawElements(int mode, int count, int type, Buffer indices) ,可以重新定义顶点的顺序,顶点的顺序由 indices Buffer 指定。 其中 mode 为上述解释顶点的模式。 前面说过顶点一般使用数组来定义,并使用 Buffer 来存储以提高绘图性能,参 见 Android OpenGL ES 开发中的 Buffer 使用 如下面定义三个顶点坐标,并把它们存放在 FloatBuffer 中: . 1 . 2 . 3 . 4 . 5 . 6 . 7 . 8 . 9 . 10 . 11 . 12 . float[] vertexArray = new float[]{ . -0.8f , -0.4f * 1.732f , 0.0f , . 0.8f , -0.4f * 1.732f , 0.0f , . 0.0f , 0.4f * 1.732f , 0.0f , . }; . ByteBuffer vbb . = ByteBuffer.allocateDirect(vertexArray.length* 4); . vbb.order(ByteOrder.nativeOrder()); . FloatBuffer vertex = vbb.asFloatBuffer(); . vertex.put(vertexArray); . vertex.position(0); 有了顶点的定义,下面就可以通过打开 OpenGL ES 管道(Pipeline)的相应开关将 顶点参数传给 OpenGL 库: 打开顶点开关和关闭顶点开关的方法如下: 1 2 3 4 5 gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); ... gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); 在打开顶点开关后,将顶点坐标传给 OpenGL 管道的方法为:glVertexPointer: public void glVertexPointer(int size,int type,int stride,Buffer pointer) . size: 每个顶点坐标维数,可以为 2,3,4。 . type: 顶点的数据类型,可以为 GL_BYTE, GL_SHORT, GL_FIXED,或 GL_FLOAT,缺省为浮点类型 GL_FLOAT。 . stride: 每个相邻顶点之间在数组中的间隔(字节数),缺省为 0,表示顶 点存储之间无间隔。 . pointer: 存储顶点的数组。 应用用上可以般顶点的颜色值存放在对应顶点后面,如下图,RGB 采用 4 字节 表示,此时相邻顶点就不是连续存放的,stride 值为 4 对应顶点除了可以为其定义坐标外,还可以指定颜色,材质,法线(用于光照处 理)等。 glEnableClientState 和 glDisableClientState 可以控制的pipeline开关可以有: GL_COLOR_ARRAY (颜色) ,GL_NORMAL_ARRAY (法线), GL_TEXTURE_COORD_ARRAY (材质), GL_VERTEX_ARRAY(顶点), GL_POINT_SIZE_ARRAY_OES 等。 对应的传入颜色,顶点,材质,法线的方法如下: glColorPointer(int size,int type,int stride,Buffer pointer) glVertexPointer(int size, int type, int stride, Buffer pointer) glTexCoordPointer(int size, int type, int stride, Buffer pointer) glNormalPointer(int type, int stride, Buffer pointer) 如果需要使用三角形来构造复杂图形,可以使用 GL_TRIANGLE_STRIP 或 GL_TRIANGLE_FAN 模式,另外一种是通过定义顶点序列: 如下图定义了一个正方形: 对应的顶点和 buffer 定义代码: 1 2 3 4 5 6 7 8 private short[] indices = { 0, 1, 2, 0, 2, 3 }; //To gain some performance we also put this ones in a byte buffer. // short is 2 bytes, therefore we multiply the number if vertices with 2. ByteBuffer ibb = ByteBuffer.allocateDirect(indices.length * 2); ibb.order(ByteOrder.nativeOrder()); ShortBuffer indexBuffer = ibb.asShortBuffer(); indexBuffer.put(indices); indexBuffer.position(0); 定义三角形的顶点的顺序很重要 在拼接曲面的时候,用来定义面的顶点的顺序 非常重要,因为顶点的顺序定义了面的朝向(前向或是后向),为了获取绘制的 高性能,一般情况不会绘制面的前面和后面,只绘制面的“前面”。虽然“前面”“后 面”的定义可以应人而易,但一般为所有的“前面”定义统一的顶点顺序(顺时针或 是逆时针方向)。 下面代码设置逆时针方法为面的“前面”: 1 gl.glFrontFace(GL10.GL_CCW); 打开 忽略“后面”设置: 1 gl.glEnable(GL10.GL_CULL_FACE); 明确指明“忽略“哪个面的代码如下: 1 gl.glCullFace(GL10.GL_BACK); Android OpenGL ES 开发教程(9) 绘制点 Point 上一篇介绍了 OpenGL ES 能够绘制的几种基本几何图形:点,线,三角形。 将分别介绍这几种基本几何图形的例子。为方便起见,暂时在同一平面上绘制这 些几何图形,在后面介绍完 OpenGL ES 的坐标系统和坐标变换后,再介绍真正 的 3D 图形绘制方法。 在 Android OpenGL ES 开发教程(7):创建实例应用 OpenGLDemos 程序框架 创建了示例应用的程序框架,并提供了一个“Hello World”示例。 为避免一些重复代码,这里定义一个所有示例代码的基类 OpenGLESActivity, 其定义如下: 1 2 3 4 5 6 7 8 9 10 11 public class OpenGLESActivity extends Activity implements IOpenGLDemo{ /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags( WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); mGLSurfaceView = new GLSurfaceView(this); 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 mGLSurfaceView.setRenderer(new OpenGLRenderer(this)); setContentView(mGLSurfaceView); } public void DrawScene(GL10 gl) { gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // Clears the screen and depth buffer. gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); } @Override protected void onResume() { // Ideally a game should implement onResume() and onPause() // to take appropriate action when the activity looses focus super.onResume(); mGLSurfaceView.onResume(); } @Override protected void onPause() { // Ideally a game should implement onResume() and 31 32 33 34 35 36 37 onPause() // to take appropriate action when the activity looses focus super.onPause(); mGLSurfaceView.onPause(); } protected GLSurfaceView mGLSurfaceView; } . 在 onCreate 方法中创建一个 GLSurfaceView mGLSurfaceView,并将屏幕 设置为全屏。并为 mGLSurfaceView 设置 Render. . onResume ,onPause 处理 GLSurfaceView 的暂停和恢复。 . DrawScene 使用黑色清空屏幕。 OpenGL ES 内部存放图形数据的 Buffer 有 COLOR ,DEPTH (深度信息)等, 在绘制图形只前一般需要清空 COLOR 和 DEPTH Buffer。 本例在屏幕上使用红色绘制 3 个点。创建一个 DrawPoint 作为 OpenGLESActivity 的子类,并定义 3 个顶点的坐标: 1 2 3 4 5 6 public class DrawPoint extends OpenGLESActivity implements IOpenGLDemo{ float[] vertexArray = new float[]{ -0.8f , -0.4f * 1.732f , 0.0f , 0.8f , -0.4f * 1.732f , 0.0f , 0.0f , 0.4f * 1.732f , 0.0f , 7 8 9 10 11 12 13 14 }; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } ... } 下面为 DrawPoint 的 DrawScene 的实现: 1 2 3 4 5 6 7 8 public void DrawScene(GL10 gl) { super.DrawScene(gl); ByteBuffer vbb = ByteBuffer.allocateDirect(vertexArray.length*4); vbb.order(ByteOrder.nativeOrder()); FloatBuffer vertex = vbb.asFloatBuffer(); vertex.put(vertexArray); vertex.position(0); 9 10 11 12 13 14 15 16 17 gl.glColor4f(1.0f, 0.0f, 0.0f, 1.0f); gl.glPointSize(8f); gl.glLoadIdentity(); gl.glTranslatef(0, 0, -4); gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertex); gl.glDrawArrays(GL10.GL_POINTS, 0, 3); gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); } . 首先是使用 FloatBuffer 存放三个顶点坐标。 . 使用 glColor4f(float red, float green, float blue, float alpha) 将当前颜色 设为红色。 . glPointSize(float size) 可以用来设置绘制点的大小。 . 使用 glEnableClientState 打开 Pipeline 的 Vectex 顶点“开关” . 使用 glVertexPointer 通知 OpenGL ES 图形库顶点坐标。 . 使用 GL_POINTS 模式使用 glDrawArrays 绘制 3 个顶点。 本例下载 Android OpenGL ES 开发教程(10) 绘制线段 Line Segment 创建一个 DrawLine Activity,定义四个顶点: . 1 . 2 . 3 . 4 . 5 . 6 . float vertexArray[] = { . -0.8f, -0.4f * 1.732f, 0.0f, . -0.4f, 0.4f * 1.732f, 0.0f, . 0.0f, -0.4f * 1.732f, 0.0f, . 0.4f, 0.4f * 1.732f, 0.0f, . }; 分别以三种模式 GL_LINES,GL_LINE_STRIP,GL_LINE_LOOP 来绘制直 线: . 1 . 2 . 3 . 4 . 5 . 6 . 7 . 8 . 9 . 10 . 11 . 12 . 13 . 14 . 15 . 16 . 17 . 18 . 19 . 20 . 21 . 22 . public void DrawScene(GL10 gl) { . super.DrawScene(gl); . ByteBuffer vbb . = ByteBuffer.allocateDirect(vertexArray.length*4 ); . vbb.order(ByteOrder.nativeOrder()); . FloatBuffer vertex = vbb.asFloatBuffer(); . vertex.put(vertexArray); . vertex.position(0); . gl.glLoadIdentity(); . gl.glTranslatef(0, 0, -4); . gl.glEnableClientState(GL10.GL_VERTEX_AR RAY); . gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertex); . index++; . index%=10; . switch(index){ . case 0: . case 1: . case 2: . 23 . 24 . 25 . 26 . 27 . 28 . 29 . 30 . 31 . 32 . 33 . 34 . 35 . 36 . 37 . 38 . 39 . 40 . 41 . 42 . 43 . gl.glColor4f(1.0f, 0.0f, 0.0f, 1.0f); . gl.glDrawArrays(GL10.GL_LINES, 0, 4); . break; . case 3: . case 4: . case 5: . gl.glColor4f(0.0f, 1.0f, 0.0f, 1.0f); . gl.glDrawArrays(GL10.GL_LINE_STRIP, 0, 4); . break; . case 6: . case 7: . case 8: . case 9: . gl.glColor4f(0.0f, 0.0f, 1.0f, 1.0f); . gl.glDrawArrays(GL10.GL_LINE_LOOP, 0, 4); . break; . } . gl.glDisableClientState(GL10.GL_VERTEX_A RRAY); . } 这里 index 的目的是为了延迟一下显示(更好的做法是使用固定时间间隔)。 前面说过 GLSurfaceView 的渲染模式有两种,一种是连续不断的更新屏幕,另 一种为 on-demand ,只有在调用 requestRender() 在更新屏幕。 缺省为 RENDERMODE_CONTINUOUSLY 持续刷新屏幕。 OpenGLDemos 使用的是缺省的 RENDERMODE_CONTINUOUSLY 持续刷新 屏幕 ,因此 Activity 的 drawScene 会不断的执行。本例中屏幕上顺序以红,绿, 蓝色显示 LINES, LINE_STRIP,LINE_LOOP。 Android OpenGL ES 开发教程(11) 绘制三角形 Triangle 三角形为 OpenGL ES 支持的面,同样创建一个 DrawTriangle Activity,定义 6 个顶点使用三种不同模式来绘制三角形: 1 2 3 4 5 6 7 8 9 float vertexArray[] = { -0.8f, -0.4f * 1.732f, 0.0f, 0.0f, -0.4f * 1.732f, 0.0f, -0.4f, 0.4f * 1.732f, 0.0f, 0.0f, -0.0f * 1.732f, 0.0f, 0.8f, -0.0f * 1.732f, 0.0f, 0.4f, 0.4f * 1.732f, 0.0f, }; 本例绘制 1 2 3 4 5 6 7 8 public void DrawScene(GL10 gl) { super.DrawScene(gl); ByteBuffer vbb = ByteBuffer.allocateDirect(vertexArray.length*4); vbb.order(ByteOrder.nativeOrder()); FloatBuffer vertex = vbb.asFloatBuffer(); vertex.put(vertexArray); vertex.position(0); 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 gl.glLoadIdentity(); gl.glTranslatef(0, 0, -4); gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertex); index++; index%=10; switch(index){ case 0: case 1: case 2: gl.glColor4f(1.0f, 0.0f, 0.0f, 1.0f); gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 6); break; case 3: case 4: case 5: gl.glColor4f(0.0f, 1.0f, 0.0f, 1.0f); gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 6); break; case 6: case 7: 30 31 32 33 34 35 36 37 38 39 40 41 42 43 case 8: case 9: gl.glColor4f(0.0f, 0.0f, 1.0f, 1.0f); gl.glDrawArrays(GL10.GL_TRIANGLE_FAN, 0, 6); break; } gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); } 这里 index 的目的是为了延迟一下显示(更好的做法是使用固定时间间隔)。 前面说过 GLSurfaceView 的渲染模式有两种,一种是连续不断的更新屏幕,另 一种为 on-demand ,只有在调用 requestRender() 在更新屏幕。 缺省为 RENDERMODE_CONTINUOUSLY 持续刷新屏幕。 OpenGLDemos 使用的是缺省的 RENDERMODE_CONTINUOUSLY 持续刷新 屏幕 ,因此 Activity 的 drawScene 会不断的执行。本例中屏幕上顺序以红,绿, 蓝色显示 TRIANGLES, TRIANGLE_STRIP,TRIANGLE_FAN。 Android OpenGL ES 开发教程(12) 绘制一个 20 面体 前面介绍了 OpenGL ES 所有能够绘制的基本图形,点,线段和三角形。其它所 有复杂的 2D 或 3D 图形都是由这些基本图形构成。 本例介绍如何使用三角形构造一个正 20 面体。一个正 20 面体,有 12 个顶点, 20 个面,30 条边构成: 创建一个 DrawIcosahedron Activity, 定义 20 面体的 12 个顶 点,和 20 个面如下: 1 2 3 4 5 6 7 8 9 10 11 12 static final float X=.525731112119133606f; static final float Z=.850650808352039932f; static float vertices[] = new float[]{ -X, 0.0f, Z, X, 0.0f, Z, -X, 0.0f, -Z, X, 0.0f, -Z, 0.0f, Z, X, 0.0f, Z, -X, 0.0f, -Z, X, 0.0f, -Z, -X, Z, X, 0.0f, -Z, X, 0.0f, Z, -X, 0.0f, -Z, -X, 0.0f }; static short indices[] = new short[]{ 0,4,1, 0,9,4, 9,5,4, 4,5,8, 4,8,1, 8,10,1, 8,3,10, 5,3,8, 5,2,3, 2,7,3, 7,10,3, 7,6,10, 7,11,6, 11,0,6, 0,1,6, 6,1,10, 9,0,11, 9,11,2, 9,2,5, 7,2,11 }; OpenGL ES 缺省使用同一种颜色来绘制图形,为了能够更好的显示 3D 效果, 我们为每个顶点随机定义一些颜色如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 float[] colors = { 0f, 0f, 0f, 1f, 0f, 0f, 1f, 1f, 0f, 1f, 0f, 1f, 0f, 1f, 1f, 1f, 1f, 0f, 0f, 1f, 1f, 0f, 1f, 1f, 1f, 1f, 0f, 1f, 1f, 1f, 1f, 1f, 1f, 0f, 0f, 1f, 0f, 1f, 0f, 1f, 0f, 0f, 1f, 1f, 1f, 0f, 1f, 1f }; 添加颜色可以参见 Android OpenGL ES 简明开发教程五:添加颜色 以后也会 有详细说明。 将顶点,面,颜色存放到 Buffer 中以提高性能: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private FloatBuffer vertexBuffer; private FloatBuffer colorBuffer; private ShortBuffer indexBuffer; ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4); vbb.order(ByteOrder.nativeOrder()); vertexBuffer = vbb.asFloatBuffer(); vertexBuffer.put(vertices); vertexBuffer.position(0); ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length * 4); cbb.order(ByteOrder.nativeOrder()); colorBuffer = cbb.asFloatBuffer(); colorBuffer.put(colors); colorBuffer.position(0); ByteBuffer ibb = ByteBuffer.allocateDirect(indices.length * 2); ibb.order(ByteOrder.nativeOrder()); indexBuffer = ibb.asShortBuffer(); indexBuffer.put(indices); indexBuffer.position(0); 看一看 DrawScene 代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void DrawScene(GL10 gl) { super.DrawScene(gl); gl.glColor4f(1.0f, 0.0f, 0.0f, 1.0f); gl.glLoadIdentity(); gl.glTranslatef(0, 0, -4); gl.glRotatef(angle, 0, 1, 0); gl.glFrontFace(GL10.GL_CCW); gl.glEnable(GL10.GL_CULL_FACE); gl.glCullFace(GL10.GL_BACK); gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer); gl.glEnableClientState(GL10.GL_COLOR_ARRAY); gl.glColorPointer(4, GL10.GL_FLOAT, 0, colorBuffer); gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_SHORT, indexBuffer); gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); gl.glDisable(GL10.GL_CULL_FACE); angle++; } 用来绘制 20 面体使用了 gl.glDrawElements: public abstract void glDrawElements(int mode, int count, int type, Buffer indices) ,可以重新定义顶点的顺序,顶点的顺序由 indices Buffer 指定。 本例会显示一个绕 Y 轴不断旋转的 20 面体,如何旋转模型将在后面的坐标系统 和坐标变换中介绍. DrawLine, DrawTriangle,及本例下载 Android OpenGL ES 开发教程(13) 阶段小结 之前介绍了什么是 OpenGL ES ,OpenGL ES 管道的概念,什么是 EGL, Android 中 OpenGL ES 的开发包以及 GLSurfaceView,OpenGL ES 所支持的基 本几何图形:点,线,面,已及如何使用这些基本几何通过构成较复杂的图像(20 面体)。 . Android OpenGL ES 开发教程(1):导言 . Android OpenGL ES 开发教程(2):关于 OpenGL ES . Android OpenGL ES 开发教程(3):OpenGL ES 管道(Pipeline) . Android OpenGL ES 开发教程(4):OpenGL ES API 命名习惯 . Android OpenGL ES 开发教程(5):关于 EGL . Android OpenGL ES 开发教程(6):GLSurfaceView . Android OpenGL ES 开发教程(7):创建实例应用 OpenGLDemos 程序框 架 . Android OpenGL ES 开发教程(8):基本几何图形定义 . Android OpenGL ES 开发教程(9):绘制点 Point . Android OpenGL ES 开发教程(10):绘制线段 Line Segment . Android OpenGL ES 开发教程(11):绘制三角形 Triangle . Android OpenGL ES 开发教程(12):绘制一个 20 面体 但到目前为止还只是绘制 2D 图形,而忽略了一些重要的概念,3D 坐标系,和 3D 坐标变换,在介绍 OpenGL ES Demo 程序框架时,创建一个 OpenGLRenderer 实现 GLSurfaceView.Renderer 接口 1 2 3 4 5 6 7 public void onSurfaceChanged(GL10 gl, int width, int height) { // Sets the current view port to the new size. gl.glViewport(0, 0, width, height); // Select the projection matrix gl.glMatrixMode(GL10.GL_PROJECTION); // Reset the projection matrix gl.glLoadIdentity(); 8 9 10 11 12 13 14 15 16 17 // Calculate the aspect ratio of the window GLU.gluPerspective(gl, 45.0f, (float) width / (float) height, 0.1f, 100.0f); // Select the modelview matrix gl.glMatrixMode(GL10.GL_MODELVIEW); // Reset the modelview matrix gl.glLoadIdentity(); } } 我们忽略了对 glViewport,glMatrixMode,gluPerspective 以及 20 面体时 glLoadIdentity,glTranslatef,glRotatef 等方法的介绍,这些都涉及到如何来为 3D 图形构建模型,在下面的几篇将详细介绍 OpenGL ES 的坐标系和坐标变换 已经如何进行 3D 建模。 Android OpenGL ES 开发教程(14) 三维坐标系及坐标变换初步 OpenGL ES 图形库最终的结果是在二维平面上显示 3D物体(常称作模型 Model) 这是因为目前的打部分显示器还只能显示二维图形。但我们在构造 3D 模型时必 须要有空间现象能力,所有对模型的描述还是使用三维坐标。也就是使用 3D建 模,而有 OpenGL ES 库来完成从 3D 模型到二维屏幕上的显示。 这个过程可以分成三个部分: . 坐标变换,坐标变换通过使用变换矩阵来描述,因此学习 3D 绘图需要了 解一些空间几何,矩阵运算的知识。三维坐标通常使用齐次坐标来定义。 变换矩阵操作可以分为视角(Viewing),模型(Modeling)和投影 (Projection)操作,这些操作可以有选择,平移,缩放,正侧投影,透 视投影等。 . 由于最终的 3D 模型需要在一个矩形窗口中显示,因此在这个窗口之外的 部分需要裁剪掉以提高绘图效率,对应 3D 图形,裁剪是将处在剪切面之 外的部分扔掉。 . 在最终绘制到显示器(2D 屏幕),需要建立起变换后的坐标和屏幕像素 之间的对应关系,这通常称为“视窗”坐标变换(Viewport) transformation. 如果我们使用照相机拍照的过程做类比,可以更好的理解 3D 坐标变换的过程。 1. 拍照时第一步是架起三角架并把相机的镜头指向需要拍摄的场景,对应到 3D 变换为 viewing transformation (平移或是选择 Camera ) 2. 然后摄影师可能需要调整被拍场景中某个物体的角度,位置,比如摄影师 给架好三角架后给你拍照时,可以要让你调整站立姿势或是位置。对应到 3D 绘制就是 Modeling transformation (调整所绘模型的位置,角度或 是缩放比例)。 3. 之后摄影师可以需要调整镜头取景(拉近或是拍摄远景),相机取景框所 能拍摄的场景会随镜头的伸缩而变换,对应到 3D 绘图则为 Projection transformation(裁剪投影场景)。 4. 按下快门后,对于数码相机可以直接在屏幕上显示当前拍摄的照片,一般 可以充满整个屏幕(相当于将坐标做规范化处理 NDC),此时你可以使 用缩放放大功能显示照片的部分。对应到 3D 绘图相当于 viewport transformation (可以对最终的图像缩放显示等) 下图为 Android OpenGL ES 坐标变换的过程: . Object Coordinate System: 也称作 Local coordinate System,用来定义一 个模型本身的坐标系。 . World Coordinate System: 3d 虚拟世界中的绝对坐标系,定义好这个坐 标系的原点就可以用来描述模型的实现的位置,Camera 的位置,光源的 位置。 . View Coordinate System: 一般使用用来计算光照效果。 . Clip Coordinate System: 对 3D 场景使用投影变换裁剪视锥。 . Normalized device coordinate System (NDC): 规范后坐标系。 . Windows Coordinate System: 最后屏幕显示的 2D 坐标系统,一般原点 定义在屏幕左上角。 对于 Viewing transformation (平移,选择相机)和 Modeling transformation(平 移,选择模型)可以合并起来看,只是应为向左移动相机,和相机不同将模型右 移的效果是等效的。 所以在 OpenGL ES 中, . 使用 GL10.GL_MODELVIEW 来同时指定 viewing matrix 和 modeling matrix. . 使用 GL10.GL_PROJECTION 指定投影变换,OpenGL 支持透视投影和 正侧投影(一般用于工程制图)。 . 使用 glViewport 指定 Viewport 变换。 此时再看看下面的代码,就不是很难理解了,后面就逐步介绍各种坐标变换。 . 1 . 2 . 3 . 4 . 5 . 6 . 7 . 8 . public void onSurfaceChanged(GL10 gl, int width, int height) { . // Sets the current view port to the new size. . gl.glViewport(0, 0, width, height); . // Select the projection matrix . gl.glMatrixMode(GL10.GL_PROJECTION); . // Reset the projection matrix . gl.glLoadIdentity(); . 9 . 10 . 11 . 12 . 13 . 14 . // Calculate the aspect ratio of the window . GLU.gluPerspective(gl, 45.0f, (float) width / (float) height, 0.1f, 100.0f); . // Select the modelview matrix . gl.glMatrixMode(GL10.GL_MODELVIEW); . // Reset the modelview matrix . gl.glLoadIdentity(); . } Android OpenGL ES 开发教程(15) 通用的矩阵变换指令 Android OpenGL ES 对于不同坐标系下坐标变换,大都使用矩阵运算的方法来 定义和实现的。这里介绍对应指定的坐标系(比如 viewmodel, projection 或是 viewport) Android OpenGL ES 支持的一些矩阵运算及操作。 OpenGL ES 中使用四个分量(x,y,z,w)来定义空间一个点,使用 4 个分量来描述 3D 坐标称为齐次坐标 :所谓齐次坐标就是将一个原本是 n 维的向量用一个 n+1 维向量来表示。 它有什么优点呢? 许多图形应用涉及到几何变换,主要包 括平移、旋转、缩放。以矩阵表达式来计算这些变换时,平移是矩阵相加,旋转 和缩放则是矩阵相乘,综合起来可以表示为 p’ = m1*p + m2(m1 旋转缩放矩阵, m2 为平移矩阵, p 为原向量 ,p’为变换后的向量)。引入齐次坐标的目的主要 是合并矩阵运算中的乘法和加法,表示为 p’ = M*p 的形式。即它提供了用矩阵 运算把二维、三维甚至高维空间中的一个点集从一个坐标系变换到另一个坐标系 的有效方法。 它可以表示无穷远的点。n+1 维的齐次坐标中如果 h=0,实 际上就表示了 n 维空间的一个无穷远点。对于齐次坐标[a,b,h],保持 a,b 不变, |V|=(x1*x1,y1*y1,z1*z1)^1/2 的过程就表示了标准坐标系中的一个点沿直线 ax+by=0 逐渐走向无穷远处的过程。 为了实现 viewing, modeling, projection 坐标变换,需要构造一个 4X4 的矩阵 M,对应空间中任意一个顶点 vertex v , 经过坐标变换后的坐标 v’=Mv 矩阵本身可以支持加减乘除,对角线全为 1 的 4X4 矩阵成为单位矩阵 Identity Matrix 。 . 将当前矩阵设为单位矩阵的指令 为 glLoadIdentity(). . 矩阵相乘的指令 glMultMatrix*() 允许指定任意矩阵和当前矩阵相乘。 . 选择当前矩阵种类 glMatrixMode(). OpenGL ES 可以运行指定 GL_PROJECTION,GL_MODELVIEW 等坐标系,后续的矩阵操作将针 对选定的坐标。 . 将当前矩阵设置成任意指定矩阵 glLoadMatrix*() . 在栈中保存当前矩阵和从栈中恢复所存矩阵,可以使用 glPushMatrix() 和 glPopMatrix() . 特定的矩阵变换平移 glTranslatef(),旋转 glRotatef() 和缩放 glScalef() OpenGL 使用了右手坐标系统,右手坐标系判断方法:在空间直角坐标系中,让 右手拇指指向 x 轴的正方向,食指指向 y 轴的正方向,如果中指能指向 z 轴的正 方向,则称这个坐标系为右手直角坐标系。 Translate 平移变换 方法 public abstract void glTranslatef (float x, float y, float z) 用于坐标平移变 换。 在上个例子中我们把需要显示的正方形后移了 4 个单位,就是使用的坐标的平移 变换,可以进行多次平移变换,其结果为多个平移矩阵的累计结果,矩阵的顺序 不重要,可以互换。 Rotate 旋转 方法 public abstract void glRotatef(float angle, float x, float y, float z)用来实现 选择坐标变换,单位为角度。 (x,y,z)定义旋转的参照矢量方向。多次旋转的顺 序非常重要。 比如你选择一个骰子,首先按下列顺序 选择 3 次: 1 2 3 gl.glRotatef(90f, 1.0f, 0.0f, 0.0f); gl.glRotatef(90f, 0.0f, 1.0f, 0.0f); gl.glRotatef(90f, 0.0f, 0.0f, 1.0f); 然后打算逆向旋转回原先的初始状态,需要有如下旋转: 1 2 3 gl.glRotatef(90f, -1.0f, 0.0f, 0.0f); gl.glRotatef(90f, 0.0f, -1.0f, 0.0f); gl.glRotatef(90f, 0.0f, 0.0f, -1.0f); 或者如下旋转: 1 2 3 gl.glRotatef(90f, 0.0f, 0.0f, -1.0f); gl.glRotatef(90f, 0.0f, -1.0f, 0.0f); gl.glRotatef(90f, -1.0f, 0.0f, 0.0f); 旋转变换 glRotatef(angle, -x, -y, -z) 和 glRotatef(-angle, x, y, z)是等价的,但选 择变换的顺序直接影响最终坐标变换的结果。 角度为正时表示逆时针方向。 Translate & Rotate (平移和旋转组合变换) 在对 Mesh(网格,构成三维形体的基本单位)同时进行平移和选择变换时,坐 标变换的顺序也直接影响最终的结果。 比如:先平移后旋转, 旋转的中心为平移后的坐标。 先选择后平移: 平移在则相对于旋转后的坐标系: 一个基本原则是,坐标变换都是相对于变换的 Mesh 本身的坐标系而进行的。 Scale(缩放) 方法 public abstract void glScalef (float x, float y, float z)用于缩放变换。 下图为使用 gl.glScalef(2f, 2f, 2f) 变换后的基本,相当于把每个坐标值都乘以 2. Translate & Scale(平移和缩放组合变换) 同样当需要平移和缩放时,变换的顺序也会影响最终结果。 比如先平移后缩放: 1 2 gl.glTranslatef(2, 0, 0); gl.glScalef(0.5f, 0.5f, 0.5f); 如果调换一下顺序: 1 2 gl.glScalef(0.5f, 0.5f, 0.5f); gl.glTranslatef(2, 0, 0); 结果就有所不同: 矩阵操作,单位矩阵 在进行平移,旋转,缩放变换时,所有的变换都是针对当前的矩阵(与当前矩阵 相乘),如果需要将当前矩阵回复最初的无变换的矩阵,可以使用单位矩阵(无 平移,缩放,旋转)。 public abstract void glLoadIdentity()。 在栈中保存当前矩阵和从栈中恢复所存矩阵,可以使用 public abstract void glPushMatrix()和 public abstract void glPopMatrix()。 在进行坐标变换的一个好习惯是在变换前使用 glPushMatrix 保存当前矩阵,完 成坐标变换操作后,再调用 glPopMatrix 恢复原先的矩阵设置。 Android OpenGL ES 开发教程(16) Viewing 和 Modeling(MODELVIEW) 变换 Viewing 和 Modeling 变换关系紧密,对应到相机拍照为放置三角架和调整被拍 物体位置及角度,通常将这两个变换使用一个 modelview 变换矩阵来定义。对 于同一个坐标变换,可以使用不同的方法来想象这个变换,比如将相机向某个方 向平移一段距离,效果等同于将被拍摄的模型(model)向相反的方向平移同样的 距离(相对运动)。两个不同的空间想象方法对于理解坐标变换各有其优缺点。 你可以使用适合自己理解能力的方法来想象空间坐标变换。 下面我们使用一个由两个坐标变换组成的简单例子开始介绍 MODELVIEW 变 换:一个变换为逆时针绕 Z 轴旋转 45 度,另一个变换为为沿 X 轴平移。 假定 需要绘制的物体的尺寸和平移的距离相比要小的多从而你可以跟容易的看到平 移效果。这个物体初始位置在坐标系的原点。 如果先旋转物体然后再平移,旋转后的物体的位置在 X 轴上面,但如果先平移 后绕原点旋转物体,最终物体会出现在 y=x 的直线上: 可以看到坐标变换的次序直接影响到最终的变换结果。所有的 Viewing 和 Modeling 变换操作都可以使用一个 4X4 的矩阵来表示,所有后续的 glMultMatrix*() 或其它坐标变换指令 会使用一个新的变换矩阵 M 于当前 modelview 矩阵 C 相乘得到一个新的变换矩阵 CM。然后所有顶点坐标 v 都会 和这个新的变换矩阵相乘。 这个过程意味着最后发出的坐标变换指令实际上是 第一个应用到顶点上的:CMv 。因此一种来理解坐标变换次序的方法是:使用逆 序来指定坐标变换。 比如下面代码: 1 2 3 4 5 6 7 8 9 10 gl.glMatrixMode(GL_MODELVIEW); gl.glLoadIdentity(); //apply transformation N gl.glMultMatrixf(N); //apply transformation M glMultMatrixf(M); //apply transformation L gl.glMultMatrixf(L); //draw vertex ... 上面代码,modelview 矩阵依次为 I(单位矩阵),N,NM 最终为 NML ,最终坐 标变换的结果为 NMLv ,也就是 N(M(Lv)) ,v 首先与 L 相乘,结果 Lv 再和 M 相乘,所得结果 MLv 再和 N 相乘。可以看到坐标变换的次序和指令指定的 次序正好相反。而实际代码运行时,坐标无需进行三次变换运算,顶点 v 只需 和计算出的最终变换矩阵 NML 相乘一次就可以了。 因此如果你采用世界坐标系(原点和 X,Y,Z 轴方向固定)来思考坐标变换, 代码中坐标变换指令的次序和 顶点和矩阵相乘的次序相反。比如,还是上面的 例子,如果你想最终的物体出现在 X 轴上,此时必须是先旋转后平移,可以使 用如下代码(R 代表选择矩阵,T 代表平移矩阵) 1 2 gl.glMatrixMode(GL_MODELVIEW); gl.glLoadIdentity(); 3 4 5 6 7 8 //translation gl.glMultMatrixf(T); //rotation gl.glMultMatrixf(R); draw_the_object() 另外一种想象坐标变换的方法是忘记这种固定的坐标系统,而是使用物体本身的 局部坐标系统,这种局部坐标系和物体的相对位置是固定的。 所有的坐标变换 操作都是针对物体的局部坐标系。使用这种方法,代码中矩阵相乘的次序和相对 局部坐标系坐标变换的次序是一致的。(不关使用哪种方法来想象坐标变换,最 终同样变换结果代码都是一样的,只是理解的方法不同)。还是使用上面旋转平 移的例子。想象一个和物体连接一起的局部坐标系,如下图红色的坐标系,想象 所有的坐标变换都是相对这个局部坐标系的,要使物体最终出现在 y=x 上,可 以想象先转动物体及其局部坐标系(R),然后再平移物体及其局部坐标系(T),这 时代码的顺序和相对于物体局部坐标系的次序是相同的。 1 2 3 4 5 6 7 gl.glMatrixMode(GL_MODELVIEW); gl.glLoadIdentity(); //rotation gl.glMultMatrixf(R); //translation gl.glMultMatrixf(T); draw_the_object() 8 使用物体局部坐标系,可以更好的了 解理解如机械手和太阳系之类的图形系统。 Android OpenGL ES 的 GLU 包有一个辅助函数 gluLookAt 提供一个更直观的 方法来设置 modelview 变换矩阵: void gluLookAt(GL10 gl, float eyeX, float eyeY, float eyeZ, float centerX, float centerY, float centerZ, float upX, float upY, float upZ) . eyex,eyey,eyez 指定观测点的空间坐标。 . tarx,tary,tarz ,指定被观测物体的参考点的坐标。 . upx,upy,upz 指定观测点方向为“上”的向量。 注意: 这些坐标都采用世界坐标系。 Android OpenGL ES 开发教程(17) 投影变换 Projection 前面 ModelView 变换相当于拍照时放置相机和调整被拍物体的位置和角度。投 影变换则对应于调整相机镜头远近来取景。 下面代码设置当前 Matrix 模式为 Projection 投影矩阵: 1 2 gl.glMatrixMode(GL_PROJECTION); gl.glLoadIdentity(); 后续的坐标变换则针对投影矩阵。投影变换的目的是定义视锥(viewing volume), 视锥一方面定义了物体如何投影到屏幕(如透视投影或是正侧投影),另一方面 视锥也定义了裁剪场景的区域大小。 OpenGL ES 可以使用两种不同的投影变换:透视投影(Perspective Projection) 和正侧投影(Orthographic Projection)。 透视投影(Perspective Projection) 透视投影的特点是“近大远小”,也就是我们眼睛日常看到的世界。OpenGL ES 定义透视投影的函数为 glFrustum(): public void glFrustumf(float left,float right,float bottom,float top,float near,float far) 视锥由(left,bottom,-near) 和(right,top,-near) 定义了靠近观测点的裁剪 面,near 和 far 定义了观测点和两个创建面直接的近距离和远距离。 在实际写代码时,Android OpenGL ES 提供了一个辅助方法 gluPerspective() 可以更简单的来定义一个透视投影变换: public static void gluPerspective(GL10 gl, float fovy, float aspect, float zNear, float zFar) . fovy: 定义视锥的 view angle. . aspect: 定义视锥的宽高比。 . zNear: 定义裁剪面的近距离。 . zFar: 定义创建面的远距离。 正侧投影(Orthographic Projection) 正侧投影,它的视锥为一长方体,特点是物体的大小不随到观测点的距离而变化, 投影后可以保持物体之间的距离和夹角。它主要用在工程制图上。 定义正侧投影(也称作平移投影)的函数为: public void glOrthof(float left, float right,float bottom,float top,float near,float far) 裁剪 场景中的图形的顶点经过 modelview 和 projection 坐标变换后,所有处在 Viewing volumn 之外的顶点都会被裁剪掉,透视投影和正侧投影都有 6 个裁剪 面。所有处在裁剪面外部的顶点都需剪裁掉以提高绘图性能。 Android OpenGL ES 开发教程(18) Viewport 变换 摄影师调整好相机和被拍摄物体的位置角度(modelview) ,对好焦距 (projection)后,就可以按下快门拍照了,拍好的照片可以在计算机上使用照片 浏览器查看照片,放大,缩小,拉伸,并可以将照片显示窗口在屏幕上任意拖放。 对应到 3D 绘制就是 Viewport 变换,目前的显示器大多还是 2D 的,viewport (显示区域)为一个长方形区域,并且使用屏幕坐标系来定义: OpenGL ES 中使用 glViewport() 来定义显示视窗的大小和位置: glViewport(int x, int y, int width, int height) Android 缺省将 viewport 设置成和显示屏幕大小一致。 如果投影变换的宽度/高度比 (aspect) 和最后的 Viewport 的 width/height 比 不一致的话,最后显示的图形就可能需要拉伸以适应 Viewport,从而可能造成图 像变形。比如:现在的电视的显示模式有 4:3 和 16:9 或是其它模式,如果使用 16:9 的模式来显示原始宽高比为 4:3 的视频,图像就有变形。、 Z 坐标变换 前面提到的 modelview, projection 变换 同样应用于 Z 轴坐标,但和屏幕坐标 系中 x,y 坐标不同的时,在屏幕坐标系下 ,Android OpenGL ES 将 z 坐标重新 编码,它的值总会在 0.0 到 1.0 之间。作为深度 depth 测试的依据。 我们在示例代码的 OpenGLRenderer 的 onSurfaceChanged 使用和屏幕一样大 小的区域作为 Viewport,你也可以通过 glViewport 将视窗设成屏幕的局部某个 区域。 并且可以看到透视投影的 aspect 为 width/height ,因此最后的图形不会有变形: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void onSurfaceChanged(GL10 gl, int width, int height) { // Sets the current view port to the new size. gl.glViewport(0, 0, width, height); // Select the projection matrix gl.glMatrixMode(GL10.GL_PROJECTION); // Reset the projection matrix gl.glLoadIdentity(); // Calculate the aspect ratio of the window GLU.gluPerspective(gl, 45.0f, (float) width / (float) height, 0.1f, 100.0f); // Select the modelview matrix gl.glMatrixMode(GL10.GL_MODELVIEW); // Reset the modelview matrix gl.glLoadIdentity(); } Android OpenGL ES 开发教程(19) 绘制迷你太阳系 前面介绍了3D坐标系统和3D坐标变换以及在OpenGL ES中坐标变换的过程, 并与相机拍照片的过程做类比,以便更好的理解这 OpenGL 中构造 3D 模型的一 部步骤: 本例提供绘制一个迷你太阳系系统作为前面知识的总结,这个迷你太阳系,有一 个红色的太阳,一个蓝色的地图和一个白色的月亮构成: . 太阳居中,逆时针自转。 . 地球绕太阳顺时针公转,本身不自转。 . 月亮绕地球顺时针公转,自身逆时针自转。 为简单起见,使用一个 2D 五角星做为天体而没有使用球体(绘制球体在后面有 介绍),构造一个 Star 类: 1 2 3 4 public class Star { // Our vertices. protected float vertices[]; // Our vertex buffer. 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 protected FloatBuffer vertexBuffer; public Star() { float a=(float)(1.0f/(2.0f-2f*Math.cos(72f*Math.PI/180.f))); float bx=(float)(a*Math.cos(18*Math.PI/180.0f)); float by=(float)(a*Math.sin(18*Math.PI/180f)); float cy=(float)(-a * Math.cos(18*Math.PI/180f)); vertices=new float[]{ 0,a,0.5f,cy,-bx,by,bx,by,-0.5f,cy }; ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4); vbb.order(ByteOrder.nativeOrder()); vertexBuffer = vbb.asFloatBuffer(); vertexBuffer.put(vertices); vertexBuffer.position(0); } /** * This function draws our star on screen. * @param gl 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 */ public void draw(GL10 gl) { // Counter-clockwise winding. gl.glFrontFace(GL10.GL_CCW); // Enable face culling. gl.glEnable(GL10.GL_CULL_FACE); // What faces to remove with the face culling. gl.glCullFace(GL10.GL_BACK); // Enabled the vertices buffer for writing //and to be used during // rendering. gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); // Specifies the location and data format of //an array of vertex // coordinates to use when rendering. gl.glVertexPointer(2, GL10.GL_FLOAT, 0, vertexBuffer); gl.glDrawArrays(GL10.GL_LINE_LOOP, 0,5); // Disable the vertices buffer. 43 44 45 46 47 48 gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); // Disable face culling. gl.glDisable(GL10.GL_CULL_FACE); } } Star 定义了五角星的五个顶点,并使用 glDrawArrays 来绘制五角星,因此 vertices 顶点的顺序比较重要。 然后定义一个 DrawSolarSystem 来绘制这个迷你太阳系: 1 2 3 4 5 6 7 8 9 10 public class DrawSolarSystem extends OpenGLESActivity implements IOpenGLDemo{ private Star sun=new Star(); private Star earth=new Star(); private Star moon=new Star(); private int angle=0; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 } public void DrawScene(GL10 gl) { super.DrawScene(gl); gl.glLoadIdentity(); GLU.gluLookAt(gl,0.0f, 0.0f, 15.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f); // Star A // Save the current matrix. gl.glPushMatrix(); // Rotate Star A counter-clockwise. gl.glRotatef(angle, 0, 0, 1); gl.glColor4f(1.0f, 0.0f, 0.0f, 1.0f); // Draw Star A. sun.draw(gl); // Restore the last matrix. gl.glPopMatrix(); // Star B // Save the current matrix 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 gl.glPushMatrix(); // Rotate Star B before moving it, //making it rotate around A. gl.glRotatef(-angle, 0, 0, 1); // Move Star B. gl.glTranslatef(3, 0, 0); // Scale it to 50% of Star A gl.glScalef(.5f, .5f, .5f); gl.glColor4f(0.0f, 0.0f, 1.0f, 1.0f); // Draw Star B. earth.draw(gl); // Star C // Save the current matrix gl.glPushMatrix(); // Make the rotation around B gl.glRotatef(-angle, 0, 0, 1); gl.glTranslatef(2, 0, 0); // Scale it to 50% of Star B gl.glScalef(.5f, .5f, .5f); 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 // Rotate around it's own center. gl.glRotatef(angle*10, 0, 0, 1); gl.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); // Draw Star C. moon.draw(gl); // Restore to the matrix as it was before C. gl.glPopMatrix(); // Restore to the matrix as it was before B. gl.glPopMatrix(); // Increse the angle. angle++; } } 使用 GLU 的 gluLookAt 来定义 modelview Matrix ,把相机放在正对太阳中心 (0,0,0),距离 15 (0,0,15)。 使用 glPushMatrix 和 glPopMatrix 来将当前 Matrix 入栈或是出栈。 首先将当前 matrix 入栈,以红色绘制太阳,并逆向转动,将当前 matrix 入栈 的目的是在能够在绘制地球时恢复当前栈。 然后绘制地球,使用局部坐标系来想象地球和太阳之间的相对运动,地球离开一 距离绕太阳公转,相当于先旋转地球的局部坐标系,然后再平移地球的局部坐标 系。对应到代码为先 glRotatef ,然后 glTranslate. 最后是绘制月亮,使用类似的空间想象方法。 Android OpenGL ES 开发教程(21) 定义 3D 模型的前面和后面 OpenGL ES 使用也只能使用三角形来定义一个面(Face),为了获取绘制的高性 能,一般情况不会同时绘制面的前面和后面,只绘制面的“前面”。虽然“前面”“后 面”的定义可以应人而易,但一般为所有的“前面”定义统一的顶点顺序(顺时针或 是逆时针方向)。 只绘制“前面”的过程称为”Culling”。 下面代码设置逆时针方法为面的“前面”: 1 gl.glFrontFace(GL10.GL_CCW); 打开 忽略“后面”设置: 1 gl.glEnable(GL10.GL_CULL_FACE); 明确指明“忽略“哪个面的代码如下: 1 gl.glCullFace(GL10.GL_BACK); Android OpenGL ES 开发教程(22) 绘制一个球体 OpenGL ES 只能通过绘制三角形来构造几何图形,比如前面绘制的 20 面体 Android OpenGL ES 开发教程(12):绘制一个 20 面体,通过增加正多面体的边 数,就可以构造出一个球体: 在项目中创建一个 Sphere 类,它的 Draw 方法,通过绘制三角形来构造球体, 并且为其添加法线,法线主要用于光照效果,将在后面介绍。 1 2 3 4 5 6 7 8 9 10 11 public void draw(GL10 gl) { float theta, pai; float co, si; float r1, r2; float h1, h2; float step = 2.0f; float[][] v = new float[32][3]; ByteBuffer vbb; FloatBuffer vBuf; vbb = ByteBuffer.allocateDirect(v.length * v[0].length * 4); vbb.order(ByteOrder.nativeOrder()); 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 vBuf = vbb.asFloatBuffer(); gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glEnableClientState(GL10.GL_NORMAL_ARRAY); for (pai = -90.0f; pai < 90.0f; pai += step) { int n = 0; r1 = (float)Math.cos(pai * Math.PI / 180.0); r2 = (float)Math.cos((pai + step) * Math.PI / 180.0); h1 = (float)Math.sin(pai * Math.PI / 180.0); h2 = (float)Math.sin((pai + step) * Math.PI / 180.0); for (theta = 0.0f; theta <= 360.0f; theta += step) { co = (float)Math.cos(theta * Math.PI / 180.0); si = -(float)Math.sin(theta * Math.PI / 180.0); v[n][0] = (r2 * co); v[n][1] = (h2); v[n][2] = (r2 * si); v[n + 1][0] = (r1 * co); v[n + 1][1] = (h1); v[n + 1][2] = (r1 * si); vBuf.put(v[n]); vBuf.put(v[n + 1]); n += 2; 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 if(n>31){ vBuf.position(0); gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vBuf); gl.glNormalPointer(GL10.GL_FLOAT, 0, vBuf); gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, n); n = 0; theta -= step; } } vBuf.position(0); gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vBuf); gl.glNormalPointer(GL10.GL_FLOAT, 0, vBuf); gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, n); } gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); gl.glDisableClientState(GL10.GL_NORMAL_ARRAY); } 有了 Sphere 类, 创建一个 DrawSphere Activity 来绘制球体,为了能看出 3D 效果,给场景中添加光源(后面介绍) 1 2 3 4 5 public void DrawScene(GL10 gl) { super.DrawScene(gl); initScene(gl); sphere.draw(gl); } 本例下载 Android OpenGL ES 开发教程(23) FrameBuffer OpenGL ES 中的 FrameBuffer 指的是存储像素的内存空间。对应一个二维图像, 如果屏幕分辨率为 1280X1024 ,如果屏幕支持 24 位真彩色 (RGB),则存储这 个屏幕区域的内存至少需要 1024X1280X3 个字节。此外如果需要支持透明度 (Alpha),则一个像素需要 4 个字节。 对应 3D 图像来说,上面存储显示颜色的 Buffer 称为 Color Buffer,除 Color Buffer 之外,还需要存储每个像素和 View Point 之间的距离,OpenGL ES 中使 用 Depth Buffer 存储像素与眼睛(eye 或是 view point)的距离,Depth Buffer 也可称为 z Buffer. 此外 OpenGL ES 还定义了一个称为遮罩(Stencil) Buffer,可以将屏幕显示局限 在某个由 Stencil Buffer 定义的区域,在日常生活中常见的 Stencil Buffer 示例 时使用纸质模板在墙上或是 T 桖上印刷文字或是图像: 在 OpenGL ES 允许配置 Color Buffer 中 R,G,B,A 的颜色位数,Depth Buffer 的位数,以及 Stencil Buffer 的位数: 参数 含义 GL_RED_BITS, GL_GREEN_BITS, GL_BLUE_BITS, GL_ALPHA_BITS Number of bits per R, G, B, or A component in the color buffers GL_DEPTH_BITS Number of bits per pixel in the depth buffer GL_STENCIL_BITS Number of bits per pixel in the stencil buffer 在最终 OpenGL ES 写入这些 Buffer 时,OpenGL ES 提供一些 Mask 函数可以 控制 Color Buffer 中 RGBA 通道,是否允许写入 Depth Buffer 等,这些 Mask 函 数可以打开或是关闭某个通道,只有通道打开后,对应的分量才会写入指定 Buffer,比如你可以关闭红色通道,这样最后写道 Color Buffer 中就不含有红色。 这些函数有 glColorMask, glDepthMask, glStencilMask。这些在后面有更详细 的介绍。 Android OpenGL ES 开发教程(24) Depth Buffer OpenGL ES 中 Depth Buffer 保存了像素与观测点之间的距离信息,在绘制 3D 图形时,将只绘制可见的面而不去绘制隐藏的面,这个过程叫”Hidden surface removal” ,采用的算法为”The depth buffer algorithm”。 一般来说,填充的物体的顺序和其顺序是一致的,而要准确的显示绘制物体在 Z 轴的前后关系,就需要先绘制距离观测点(ViewPoint)最远的物体,再绘制离观 测点较远的物体,最后绘制离观测点最近的物体,因此需要对应所绘制物体进行 排序。OpenGL ES 中使用 Depth Buffer 存放需绘制物体的相对距离。 The depth buffer algorithm 在 OpenGL ES 3D 绘制的过程中这个算法是自动被采用的,但是了 解这个算法有助于理解 OpenGL ES 部分 API 的使用。 这个算法的基本步骤如下: 1. 将 Depth Buffer 中的值使用最大值清空整个 Depth Buffer,这个最大值缺 省为 1.0 ,为距离 viewPoint 最远的裁剪的距离。最小值为 0,表示距离 viewPoint 最近的裁剪面的距离。距离大小为相对值而非实际距离,这个 值越大表示与 Viewpoint 之间的距离越大。因此将初值这设为 1.0 相当于 清空 Depth Buffer。 2. 当 OpenGL 栅格化所绘制基本图形(Primitive),将计算该 Primitive 与 viewpoint 之间的距离,保存在 Depth Buffer 中。 3. 然后比较所要绘制的图形的距离和当前 Depth Buffer 中的值,如果这个距 离比 Depth Buffer 中的值小,表示这个物体离 viewPoint 较近,Open GL 则更像相应的 Color Buffer 并使用这个距离更新 Depth Buffer,否则,表 示当前要绘制的图形在已绘制的部分物体后面,则无需绘制该图形(删除)。 这个过程也称为”Depth Test” (深度测试)。 下面给出了 OpenGL ES 中与 Depth Buffer 相关的几个方法: . gl.Clear(GL10.GL_DEPTH_BUFFER_BIT) 清空 Depth Buffer (赋值为 1.0)通常清空 Depth Buffer 和 Color Buffer 同时进行。 . gl.glClearDepthf(float depth) 指定清空 Depth Buffer 是使用的值,缺省 为 1.0,通常无需改变这个值, . gl.glEnable(GL10.GL_DEPTH_TEST) 打开 depth Test . gl.glDisable(GL10.GL_DEPTH_TEST) 关闭 depth Test Android OpenGL ES 开发教程(25) OpenGL 光照模型 前面绘制球体时 Android OpenGL ES 开发教程(22):绘制一个球体 ,为了能 看出 3D 效果,给场景中添加光源。如果没有光照,绘出的球看上去和一个二维 平面上圆没什么差别,如下图,左边为有光照效果的球体,右边为同一个球体但 没有设置光源,看上去就没有立体效果,因此 OpenGL 光照效果对显示 3D 效 果非常明显。 在 OpenGL 光照模型中光源和光照效果可以细分为红,绿,蓝三个部分,光源由 红,绿,蓝强度来定义,而物体表面材料由其反射红,绿,蓝的程度和方向来定 义。OpenGL 光照模型使用的计算公式是对于现实世界光照的一个近似但效果 非常好并适合快速计算。 OpenGL 光照模型中定义的光源可以分别控制,打开或关闭,OpenGL ES 支持 最多八个光源。 OpenGL 光照模型中最终的光照效果可以分为四个组成部分:Emitted(光源), ambient(环境光),diffuse(漫射光)和 specular(镜面反射光),最终结果由这 四种光叠加而成。 Emitted : 一般只发光物体或者光源,这种光不受其它光源的影响。 ambient: 指光线经过多次反射后已经无法得知其方向(可以看作来自所有方向), 可以成为环境光,该光源如果射到某个平面,其反射方向为所有方向。Ambient 不依赖于光源的方向。 diffuse:当一束平行的入射光线射到粗糙的表面时,因面上凹凸不平,所以入射 线虽然互相平行,由于各点的法线方向不一致,造成反射光线向不同的方向无规 则地反射,这种反射称之为“漫反射”或“漫射”。这个反射的光则称为漫射光。漫 射光射到某个平面时,其反射方向也为所有方向。diffuse 只依赖于光源的方向 和法线的方向。 specular : 一般指物体被光源直射的高亮区域,也可以成为镜面反射区,如金 属。specular 依赖于光源的方向,法线的方向和视角的方向。 尽管光源可能只发送某一频率的光线,但 ambient,diffuse 和 specular 可能不 同。比如使用白光照射一堵红墙,散射的光线可能为红色。OpenGL 允许为光源 分别设置红,绿,蓝三个元素的值。 最终决定所看到物体的颜色除了光源的颜色和方向外,还取决于物体本身的颜色, 比如红色的光照在红色的物体和蓝色的物体,最终看到的物体一个还是红色,一 个为黑色。OpenGL 中对物体材料(Material)的颜色是通过其反射红,绿,蓝的 比例来定义的。 和光源一样,物体的颜色也可以有不同的 ambient,diffuse 和 specular,表现为反射这些光的比例。ambient,diffuse 反射通常为同样的颜色, 而 specular 常常表现为白色或灰色光,如使用白光照射一个红色的球,球的大 部分区域显示为红色,而高亮区域为白色。 Android OpenGL ES 开发教程(26) 设置光照效果 Set Lighting 上一篇简单介绍了 OpenGL 中使用的光照模型,本篇结合 OpenGL ES API 说明 如何使用光照效果: . 设置光源 . 定义法线 . 设置物体材料光学属性 光源 OpenGL ES 中可以最多同时使用八个光源,分别使用 0 到 7 表示。 OpenGL ES 光源可以分为 . 平行光源(Parallel light source), 代表由位于无限远处均匀发光体,太阳可 以近似控制平行光源。 . 点光源(Spot light source) 如灯泡就是一个点光源,发出的光可以指向 360 度,可以为点光源设置光衰减属性(attenuation)或者让点光源只能 射向某个方向(如射灯)。 . 可以为图形的不同部分设置不同的光源。 下面方法可以打开某个光源,使用光源首先要开光源的总开关: 1 gl.glEnable(GL10.GL_LIGHTING); 然后可以再打开某个光源如 0 号光源: 1 gl.glEnable(GL10.GL_LIGHTI0); 设置光源方法如下: . public void glLightfv(int light,int pname, FloatBuffer params) . public void glLightfv(int light,int pname,float[] params,int offset) . public void glLightf(int light,int pname,float param) . light 指光源的序号,OpenGL ES 可以设置从 0 到 7 共八个光源。 . pname: 光源参数名称,可以有如下:GL_SPOT_EXPONENT, GL_SPOT_CUTOFF, GL_CONSTANT_ATTENUATION, GL_LINEAR_ATTENUATION, GL_QUADRATIC_ATTENUATION, GL_AMBIENT, GL_DIFFUSE,GL_SPECULAR, GL_SPOT_DIRECTION, GL_POSITION . params 参数的值(数组或是 Buffer 类型)。 其中为光源设置颜色的参数类型为 GL_AMBIENT, GL_DIFFUSE,GL_SPECULAR,可以分别指定 R,G,B,A 的值。 指定光源的位置的参数为 GL_POSITION,值为(x,y,z,w): 平行光将 w 设为 0.0,(x,y,z)为平行光的方向: 对于点光源,将 w 设成非 0 值,通常设为 1.0. (x,y,z)为点光源的坐标位置。 将点光源设置成聚光灯,需要同时设置 GL_SPOT_DIRECTION,GL_SPOT_CUTOFF 等 参数,GL_POSITION 的设置 和点光源类似:将 w 设成非 0 值,通常设为 1.0. (x,y,z)为点光源的坐标位置。 而对于 GL_SPOT_DIRECTION 参数,设置聚光的方向(x,y,z) GL_SPOT_CUTOFF 参数设置聚光等发散角度(0 到 90 度) GL_SPOT_EXPONENT 给出了聚光灯光源汇聚光的程度,值越大,则聚光区 域越小(聚光能力更强)。 对应点光源(包括聚光灯),其它几个参数 GL_CONSTANT_ATTENUATION, GL_LINEAR_ATTENUATION, GL_QUADRATIC_ATTENUATION 为点光源 设置光线衰减参数,公式有如下形式,一般无需详细了解: 在场景中设置好光源后,下一步要为所绘制的图形设置法线(Normal),只有设 置了法线,光源才能在所会物体上出现光照效果。三维平面的法线是垂直于该平 面的三维向量。曲面在某点 P 处的法线为垂直于该点切平面的向量 和设置颜色类似,有两个方法可以为平面设置法线,一是 public void glNormal3f(float nx,float ny,float nz) 这个方法为后续所有平面设置同样的方向,直到重新设置新的法线为止。 为某个顶点设置法线: public void glNormalPointer(int type,int stride, Buffer pointer) . type 为 Buffer 的类型,可以为 GL_BYTE, GL_SHORT, GL_FIXED,或 GL_FLOAT . stride: 每个参数之间的间隔,通常为 0. . pointer: 法线值。 打开法线数组 1 gl.glEnableClientState(GL10.GL_NORMAL_ARRAY); 用法和 Color, Vertex 类似。参见 Android OpenGL ES 开发教程(8):基本几何 图形定义。 规范化法向量,比如使用坐标变换(缩放),如果三个方向缩放比例不同的话, 顶点或是平面的法线可能就有变好,此时需要打开规范化法线设置: 1 gl.glEnable(GL10.GL_NORMALIZE); 经过规范化后法向量为单位向量(长度为 1)。同时可以打开缩放法线设置 1 gl.glEnable(GL10.GL_RESCALE_NORMAL); 设置好法线后,需要设置物体表面材料(Material)的反光属性(颜色和材质)。 将在下篇介绍设置物体表面材料(Material)的反光属性(颜色和材质)并给出一 个光照的示例。 Android OpenGL ES 开发教程(27) 材质及光照示例 设置物体表面材料(Material)的反光属性(颜色和材质)的方法如下: public void glMaterialf(int face,int pname,float param) public void glMaterialfv(int face,int pname,float[] params,int offset) public void glMaterialfv(int face,int pname,FloatBuffer params) . face : 在 OpenGL ES 中只能使用 GL_FRONT_AND_BACK,表示修改物 体的前面和后面的材质光线属性。 . pname: 参数类型,可以有 GL_AMBIENT, GL_DIFFUSE, GL_SPECULAR, GL_EMISSION, GL_SHININESS。这些参数用在光照 方程。 . param: 参数的值。 其中 GL_AMBIENT,GL_DIFFUSE,GL_SPECULAR ,GL_EMISSION 为颜色 RGBA 值,GL_SHININESS 值可以从 0 到 128,值越大,光的散射越小: 此外,方法 glLightModleXX 给出了光照模型的参数 public void glLightModelf(int pname,float param) public void glLightModelfv(int pname,float[] params,int offset) public void glLightModelfv(int pname,FloatBuffer params) . pname: 参数类型,可以为 GL_LIGHT_MODEL_AMBIENT 和 GL_LIGHT_MODEL_TWO_SIDE . params: 参数的值。 最终顶点的颜色由这些参数(光源,材质光学属性,光照模型)综合决定(光照 方程计算出)。 下面例子在场景中设置一个白色光源: . 1 . 2 . 3 . 4 . 5 . 6 . 7 . 8 . 9 . 10 . 11 . 12 . 13 . 14 . 15 . 16 . 17 . 18 . 19 . 20 . 21 . 22 . 23 . 24 . 25 . 26 . 27 . 28 . 29 . 30 . public void initScene(GL10 gl){ . float[] amb = { 1.0f, 1.0f, 1.0f, 1.0f, }; . float[] diff = { 1.0f, 1.0f, 1.0f, 1.0f, }; . float[] spec = { 1.0f, 1.0f, 1.0f, 1.0f, }; . float[] pos = { 0.0f, 5.0f, 5.0f, 1.0f, }; . float[] spot_dir = { 0.0f, -1.0f, 0.0f, }; . gl.glEnable(GL10.GL_DEPTH_TEST); . gl.glEnable(GL10.GL_CULL_FACE); . gl.glEnable(GL10.GL_LIGHTING); . gl.glEnable(GL10.GL_LIGHT0); . ByteBuffer abb . = ByteBuffer.allocateDirect(amb.length*4); . abb.order(ByteOrder.nativeOrder()); . FloatBuffer ambBuf = abb.asFloatBuffer(); . ambBuf.put(amb); . ambBuf.position(0); . ByteBuffer dbb . = ByteBuffer.allocateDirect(diff.length*4); . dbb.order(ByteOrder.nativeOrder()); . FloatBuffer diffBuf = dbb.asFloatBuffer(); . diffBuf.put(diff); . diffBuf.position(0); . ByteBuffer sbb . = ByteBuffer.allocateDirect(spec.length*4); . sbb.order(ByteOrder.nativeOrder()); . FloatBuffer specBuf = sbb.asFloatBuffer(); . specBuf.put(spec); . specBuf.position(0); . ByteBuffer pbb . = ByteBuffer.allocateDirect(pos.length*4); . 31 . 32 . 33 . 34 . 35 . 36 . 37 . 38 . 39 . 40 . 41 . 42 . 43 . 44 . 45 . 46 . 47 . 48 . 49 . 50 . 51 . 52 . 53 . 54 . 55 . 56 . 57 . 58 . 59 . 60 . pbb.order(ByteOrder.nativeOrder()); . FloatBuffer posBuf = pbb.asFloatBuffer(); . posBuf.put(pos); . posBuf.position(0); . ByteBuffer spbb . = ByteBuffer.allocateDirect(spot_dir.length*4); . spbb.order(ByteOrder.nativeOrder()); . FloatBuffer spot_dirBuf = spbb.asFloatBuffer(); . spot_dirBuf.put(spot_dir); . spot_dirBuf.position(0); . gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_AMBIENT, ambBuf); . gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_DIFFUSE, diffBuf); . gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_SPECULAR, specBuf); . gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION, posBuf); . gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_SPOT_DIRECTION, . spot_dirBuf); . gl.glLightf(GL10.GL_LIGHT0, GL10.GL_SPOT_EXPONENT, 0.0f); . gl.glLightf(GL10.GL_LIGHT0, GL10.GL_SPOT_CUTOFF, 45.0f); . gl.glLoadIdentity(); . GLU.gluLookAt(gl,0.0f, 4.0f, 4.0f, 0.0f, 0.0f, 0.0f, . 0.0f, 1.0f, 0.0f); . } 绘制一个球,并使用蓝色材质: . 1 . 2 . 3 . 4 . 5 . 6 . 7 . 8 . 9 . 10 . 11 . 12 . 13 . 14 . 15 . 16 . 17 . 18 . 19 . 20 . 21 . 22 . 23 . 24 . 25 . 26 . 27 . 28 . 29 . 30 . 31 . 32 . 33 . 34 . 35 . 36 . 37 . public void drawScene(GL10 gl) { . super.drawScene(gl); . float[] mat_amb = {0.2f * 0.4f, 0.2f * 0.4f, . 0.2f * 1.0f, 1.0f,}; . float[] mat_diff = {0.4f, 0.4f, 1.0f, 1.0f,}; . float[] mat_spec = {1.0f, 1.0f, 1.0f, 1.0f,}; . ByteBuffer mabb . = ByteBuffer.allocateDirect(mat_amb.length*4); . mabb.order(ByteOrder.nativeOrder()); . FloatBuffer mat_ambBuf = mabb.asFloatBuffer(); . mat_ambBuf.put(mat_amb); . mat_ambBuf.position(0); . ByteBuffer mdbb . = ByteBuffer.allocateDirect(mat_diff.length*4); . mdbb.order(ByteOrder.nativeOrder()); . FloatBuffer mat_diffBuf = mdbb.asFloatBuffer(); . mat_diffBuf.put(mat_diff); . mat_diffBuf.position(0); . ByteBuffer msbb . = ByteBuffer.allocateDirect(mat_spec.length*4); . msbb.order(ByteOrder.nativeOrder()); . FloatBuffer mat_specBuf = msbb.asFloatBuffer(); . mat_specBuf.put(mat_spec); . mat_specBuf.position(0); . gl.glMaterialfv(GL10.GL_FRONT_AND_BAC K, . GL10.GL_AMBIENT, mat_ambBuf); . gl.glMaterialfv(GL10.GL_FRONT_AND_BAC K, . GL10.GL_DIFFUSE, mat_diffBuf); . gl.glMaterialfv(GL10.GL_FRONT_AND_BAC . 38 . 39 . 40 . 41 K, . GL10.GL_SPECULAR, mat_specBuf); . gl.glMaterialf(GL10.GL_FRONT_AND_BACK, . GL10.GL_SHININESS, 64.0f); . sphere.draw(gl); . }
还剩101页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

xiaoxixi61

贡献于2016-12-26

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf