原文地址:http://dev.uphoneapp.com/doc/view.xhtml?id=52260
前言
OpenGL ES是Khronos Group创建的一系列API中的一种(官方组织是:http://www.khronos.org/)。在桌面计算机上有两套标准的3DAPI:Direct3D和OpenGL。Direct3D实际上是运行在windows操作系统上的标准3DAPI,而OpenGL则是跨平台的,适用于Linux、多种UNIX、MAC OS X和windows。由于OpenGL得到了广范围的认可,所以,基于嵌入式的3DAPI---OpenGL ES也就应运而生。
沃Phone使用的芯片高通7227,它能很好的提供对OpenGL ES的支持,了解OpenGL ES的种种特性,不仅能开发出很好的适用于沃Phone的3D游戏、3D应用等。借助于OpenGL ES的平台无关性,只要稍微修改EGL,理论上就可以将开发的3D游戏、3D应用移植到任何支持OpenGL ES的平台上去。
本篇文档就从零开始,深入简出,跟大家介绍一下OpenGL ES的原理和开发。
OpenGL ES简介
什么是OpenGL ES
OpenGL ES是一套适用于手持嵌入式设备的3DAPI。比如手机、PDA、汽车、航空等等上面都可以使用到OpenGL ES。OpenGL ES是免授权费的、跨平台的、功能完善的2D和3D图形应用程序接口API,它是桌面OpenGL的子集,是从OpenGL裁剪定制而来的。由于手持设备的相关局限性,OpenGL ES相对于OpenGL不可避免的进行了相关的精简。去除了OpenGL中比如glBegin/glEnd,四边形(GL_QUADS)、多边形(GL_POLYGONS)等复杂图元等许多非绝对必要的特性。但是OpenGL方面的很多知识,OpenGL ES都是可以借鉴的。
OpenGL ES其实是一个状态机(State machine),它保存一种状态直至其改变。每个状态都有本身默认的缺省值,可以通过相关的查询和设置函数进行相关的查询和设置。
大多数的OpenGL ES的应用都采用的是相同的操作顺序,这一系列的处理阶段被称作OpenGL ES的渲染管线(pipeline)。
OpenGL ES 2.0
看一下OpenGL ES的发展历程,如下图:
从图中我们可见,现在常用的OpenGL ES有两个版本,OpenGL ES1.1和OpenGL ES2.0。其中OpenGL ES1.1是固定渲染管线的,而OpenGL ES2.0是可编程渲染管线的。1.1版本现在多用于低成本的设备中,而2.0的版本的设备通常是兼容1.1的。这里说的渲染,就是根据模型创建图像的一个过程。而模型是根据几何图元创建的,图元包括点、直线、多边形,他们是通过顶点(Vertex)定义的。
OpenGL ES的处理流程
1. 固定渲染管线流程:
上图为固定渲染管线流程图,这个渲染管线是OpenGL ES1.1使用的。所处理的流程如下
1) Primitive Processing:
这一步是图元运算过程,所谓图元,其实就是一个点集。在OpenGL ES中,所有的物体,几何元素最终都是以顶点的形式表述的。一般来说,这些顶点将会产生三角形、直线或点。它做的工作就是将顶点提供给顶点处理器进行处理。顶点的数据包括顶点的位置(空间坐标)、大小、颜色、顶点的法向量(用于光照计算)、纹理坐标(可能有多个)等等。
2) Transform and Lighting:
这一步是转换和光照过程。其中Transform是通过模型、视图、投影变换矩阵,将所有的顶点坐标变换成人眼坐标系下的一致坐标。变换矩阵同样会改变物体的顶点法向量。如果激活了纹理,还可以进行纹理坐标转换,以及自动纹理坐标的生产。Lighting处理的就是光照部分,它会利用光源、材质、转换后的顶点位置和法向量计算每个顶点的颜色值。
3) Primitive Assembly:
图元装配过程。管线中这个流程是对所有的点数据进行点线面等基础图元的组装。这个过程会对所有的图元进行剪切和筛选。对于不在视区空间中的部分进行剪切,对于不可见的面进行筛选。
4) Resterizar:
光栅化。光栅化的过程就是对所有的经过Primitive Assembly图元转换成屏幕上可以显示的二维Fragment(片元)。片元和将要显示的像素一一对应。
5) Texture Environment:
纹理处理。利用纹理坐标来进行纹理的相关处理。
6) Colour Sum:
颜色叠加。根据纹理颜色等相关属性确定最终的顶点颜色。
7) Fog:
雾。雾化处理。
8) Alpha Test:
Alpha测试。判断某些片元是否抛弃。比如可以规定Alpha小于0.2的片元就需要抛弃。
9) Depth Stencil:
深度测试和模板测试。深度测试需要一个深度缓冲区,是在后面的会被在前的遮盖,需要抛弃。模板测试需要一个模板缓冲区,也就是模板缓冲区中为每个像素保存了一个“模板值”,当像素需要进行模板测试时,将设定的模板参考值与该像素的“模板值”进行比较,符合条件的通过测试,不符合条件的则被丢弃,不进行绘制。条件的设置与Alpha测试中的条件设置相似。但注意Alpha测试中是用浮点数来进行比较,而模板测试则是用整数来进行比较。
10) Color Buffer blend:
跟颜色缓冲区进行混合。最终生成的片元颜色需要跟颜色缓冲区中本来的进行混合(也可以理解成为跟背景混合),以生成最终的颜色。
11) Dither:
抖动。在可用颜色数量较少的系统中,可能需要对颜色值进行抖动,在适当损失颜色质量的情况下增加可使用的颜色数量。
12) Frame Buffer:
最终结果就写进了Frame Buffer。一个流程就算结束了。
2. 可编程渲染管线流程:
对比固定渲染管线流程图和可编程渲染管线流程图,可以看出来,大部分的流程都是一样的,只是可编程将固定中的几个功能合并了,新增了两个新的流程,Vertex Shader和Fragment Shader。也就是着色器(shader)
作为OpenGL ES2.0的一部分,着色器允许应用程序显式地指定处理顶点和片断时候所执行的操作。
1) Shader language简介:
编写OpenGL ES程序使用的着色器类似于使用基于编译器的语言(比如C语言)编写程序。我们需要用编译器来分析程序,检查它所存在的错误,并把它转换成为目标代码。接着,在链接阶段,链接器把一组目标文件组合在一起,形成一个可执行的程序。着色器的创建流程如下:
对于每个着色器对象:
1. 创建一个着色器对象。
2. 把着色器源代码编译为目标代码。
3. 验证这个着色器已经成功通过编译。
然后,为了把多个着色器对象链接到一个着色器程序中,需要:
1. 创建一个着色器程序。
2. 把适当的着色器对象连接到这个着色器程序中。
3. 链接着色器程序。
4. 验证着色器程序链接成功。
5. 使用着色器进行顶点或片段处理。
1) Vertex Shader:
包含了固定渲染管线中的Transform and lighting的所有操作。
2) Fragment Shader:
包含了固定渲染管线中的纹理处理、颜色叠加、雾、alpha测试等内容。
具体着色器语言和两个着色器的使用,可见后续的sample分析。
EGL简介
OpenGL实现跨平台的功能,在不同的操作系统上需要不同的类似适配层的内容,比如在Windows操作系统上需要WGL。同样的,OpenGL ES是一个平台中立的图形库,在它能够工作前,需要与一个实际的窗口关联起来,但是,与OpenGL不一样的是,OpenGL是每个窗口系统需要一个与之对应的适配层,Windows需要WGL,X-Window需要xgl,Mac OS需要agl。而OpenGL ES的这层,是统一的一个标准。这个标准就是EGL。
(一) 初识EGL
EGL是介于RenderAPI(比如OpenGL ES和OpenVG)和本地基础系统的一套接口。里面涉及了OpenGL ES和OpenVG的一些相关描述,所以需要和OpenGL ES和OpenVG文档一起阅读。EGL使用OpenGLES的命名习惯来命名函数入口和宏定义。具体的接口和相关宏定义可以参见egl.h。
(二) EGL的使用
1. 获取Display:
Display代表的是显示器,有的系统上有多个显示器,也就会有多个display。获得Display需要调用EGLDisplay eglGetDisplay(EGLNativeDisplayType display_id);,参数一般为EGL_DEFAULT_DISPLAY。该参数的实际意义是平台相关的,比如在windows平台上,一般返回的就是DC。沃Phone上就是TDC。
2. 初始化egl:
获得了Display后,调用EGLBoolean eglInitialize(EGLDisplay dpy, EGLint *major, EGLint *minor);该函数会进行一些相关的内部初始化工作。我们可以通过这个函数获得egl的版本号。
3. 选择Config:
Config实际就是FrameBuffer的参数,在Windows下对应于PixelFormat,在X-Window下对应Visual。可以用函数EGLBoolean eglChooseConfig(EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size, EGLint *num_config);,其中attrib_list 是以EGL_NONE结束的参数数组,通常以id,value依次存放,对于个别标识性的属性可以只有id,没有value。另一个办法是用EGLBoolean eglGetConfigs(EGLDisplay dpy, EGLConfig *configs, EGLint config_size, EGLint *num_config); 来获得所有config。这两个函数都会返回不多于config_size个Config,结果保存在configs[]中,系统的总Config个数保存在num_config中。可以利用eglGetConfig()中间两个参数为0来查询系统支持的Config总个数。Config有众多的Attribute,这些Attribute决定FrameBuffer的格式和能力,通过eglGetConfigAttrib ()来读取,但不能修改。
4. 构造Surface:
有了Config,就可以开始构造Surface了。Surface实际上就是一个FrameBuffer。通过函数EGLSurface eglCreateWindowSurface(EGLDisplay dpy, EGLConfig config,EGLNativeWindowType win, const EGLint *attrib_list)可以创建一个Surface。系统通常还支持另外两种Surface:PixmapSurface和PBufferSurface,这两种都不是可显示的Surface,PixmapSurface是保存在系统内存中的位图,PBuffer则是保存在显存中的帧。Surface也有一些attribute,基本上都可以故名思意,EGL_HEIGHT EGL_WIDTH EGL_LARGEST_PBUFFER EGL_TEXTURE_FORMAT EGL_TEXTURE_TARGET EGL_MIPMAP_TEXTURE EGL_MIPMAP_LEVEL,通过eglSurfaceAttrib()设置、eglQuerySurface()读取。
5. 创建Context:
OpenGL ES的pipeline从程序的角度看就是一个状态机,有当前的颜色、纹理坐标、变换矩阵、渲染模式等一大堆状态,这些状态作用于程序提交的顶点坐标等图元从而形成帧缓冲内的像素。在OpenGL ES的编程接口中,Context就代表这个状态机,程序的主要工作就是向Context提供图元、设置状态,偶尔也从Context里获取一些信息。用EGLContext eglCreateContext(EGLDisplay dpy, EGLSurface write, EGLSurface read, EGLContext * share_list)来创建一个Context。
6. 绘制:
应用程序通过OpenGL API进行绘制,一帧完成之后,调用eglSwapBuffers(EGLDisplay dpy, EGLContext ctx)来显示。
OpenGL ES的HelloWorld---Hello********
- // 头文件包含
- #include "TG3.h"
- #include
- #include
- #include "OGLES2Hello********.h"
- // 宏定义
- #define VERTEX_ARRAY 0
- // 定义Display、config、surface、context
- EGLDisplay eglDisplay = 0;
- EGLConfig eglConfig = 0;
- EGLSurface eglSurface = 0;
- EGLContext eglContext = 0;
- EGLNativeWindowType eglWindow = 0;
- // 沃Phone窗口指针
- extern TWindow *g_pThis;
- bool TestEGLError()
- {
- //eglGetError返回上一个egl中的错误,用户在每个egl函数调用结束都需要调用这个函数。
- EGLint iErr = eglGetError();
- if (iErr != EGL_SUCCESS)
- {
- return false;
- }
- return true;
- }
- bool CreateEGLContext()
- {
- // 第一步:获得或者创建一个可以用于OpenGL ES输出的EGLNativeWindowType
- eglWindow = (EGLNativeWindowType)g_pThis;
- //第二步:获得默认的Display。通常我们只有一块屏幕,参数传EGL_DEFAULT_DISPLAY就可以了。
- eglDisplay = eglGetDisplay((EGLNativeDisplayType) EGL_DEFAULT_DISPLAY);
- //第三步:初始化EGL,如果我们不想要版本号,后两个参数也可以传NULL进去。
- EGLint iMajorVersion, iMinorVersion;
- if (!eglInitialize(eglDisplay, &iMajorVersion, &iMinorVersion))
- {
- return false;
- }
- //第四步:指定需要的配置属性,一个EGL的配置描述了Surfaces上像素的格式信息。当前我们要的是Windows的surface,在屏幕上是可见的,以EGL_NONE结尾。
- const EGLint pi32ConfigAttribs[] =
- {
- EGL_LEVEL, 0,
- EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
- EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
- EGL_NATIVE_RENDERABLE, EGL_FALSE,
- EGL_DEPTH_SIZE, EGL_DONT_CARE,
- EGL_NONE
- };
- //第五步:寻找一个符合所有要求的配置,我们需要的只是其中一个,所以可以限制config的个数为1。
- int iConfigs;
- if (!eglChooseConfig(eglDisplay, pi32ConfigAttribs, &eglConfig, 1, &iConfigs) || (iConfigs != 1))
- {
- return false;
- }
- //第六步:创建一个可画的surface。这里创建时可见的windows surface。Pixmaps和pbuffers都是不可见的。
- eglSurface = eglCreateWindowSurface(eglDisplay, eglConfig, eglWindow, NULL);
- if(eglSurface == EGL_NO_SURFACE)
- {
- eglGetError(); // Clear error
- eglSurface = eglCreateWindowSurface(eglDisplay, eglConfig, NULL, NULL);
- }
- if (!TestEGLError())
- {
- return false;
- }
- //第七步:创建Context。我们OpenGL ES的资源,比如纹理只有在这个context里是可见的。
- // 绑定API (可以是OpenGLES或者 OpenVG)
- eglBindAPI(EGL_OPENGL_ES_API);
- EGLint ai32ContextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE };
- eglContext = eglCreateContext(eglDisplay, eglConfig, NULL, ai32ContextAttribs);
- if (!TestEGLError())
- {
- return false;
- }
- //第八步:将创建的context绑定到当前的线程,使用我们创建的surface进行读和写。Context绑定到线程上,你就可以不用担心其他的进程影响你的OpenGL ES应用程序。
- eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);
- if (!TestEGLError())
- {
- return false;
- }
- return true;
- }
- bool Render()
- {
- //第九步:使用OpenGL ES API画一些东西。到这里,所有的东西都已准备就绪,我们做好了往屏幕上画东西的准备。
- bool bRet = false;
- //单元矩阵,用于投影和模型变换
- float pfIdentity[] =
- {
- 1.0f,0.0f,0.0f,0.0f,
- 0.0f,1.0f,0.0f,0.0f,
- 0.0f,0.0f,1.0f,0.0f,
- 0.0f,0.0f,0.0f,1.0f
- };
- // Vertex和Fragment的shader
- // gl_FragColor指定了最终的像素颜色。
- // gl_position则指定了最终的点在人眼坐标中的位置。
- char szFragShaderSrc[] = {"\
- void main (void)\
- {\
- gl_FragColor = vec4(1.0, 1.0, 0.66 ,1.0);\
- }"};
- char szVertShaderSrc[] = {"\
- attribute highp vec4 myVertex;\
- uniform mediump mat4 myPMVMatrix;\
- void main(void)\
- {\
- gl_Position = myPMVMatrix * myVertex;\
- }"};
- char * pszFragShader = (char *)szFragShaderSrc;
- char * pszVertShader = (char *)szVertShaderSrc;
- GLuint uiFragShader = 0;
- GLuint uiVertShader = 0; /* 用来存放Vertex和Fragment shader的句柄 */
- GLuint uiProgramObject = 0; /* 用来存放创建的program的句柄*/
- GLint bShaderCompiled;
- GLint bLinked;
- // 我们要画一个三角形,所以,我们先创建一个顶点缓冲区
- GLuint ui32Vbo = 0; // 顶点缓冲区对象句柄
- //顶点数据 这9个数据分别为3个点的X、Y、Z坐标
- GLfloat afVertices[] = { -0.4f,-0.4f,0.0f, // Position
- 0.4f ,-0.4f,0.0f,
- 0.0f ,0.4f ,0.0f};
- int i32InfoLogLength, i32CharsWritten;
- char* pszInfoLog = NULL;
- int i32Location = 0;
- unsigned int uiSize = 0;
- //创建Fragment着色器对象
- uiFragShader = glCreateShader(GL_FRAGMENT_SHADER);
- // 将代码加载进来
- glShaderSource(uiFragShader, 1, (const char**)&pszFragShader, NULL);
- //编译代码
- glCompileShader(uiFragShader);
- //看编译是否成功进行
- glGetShaderiv(uiFragShader, GL_COMPILE_STATUS, &bShaderCompiled);
- if (!bShaderCompiled)
- {
- // 错误发生,首先获取错误的长度
- glGetShaderiv(uiFragShader, GL_INFO_LOG_LENGTH, &i32InfoLogLength);
- //开辟足够的空间来存储错误信息
- pszInfoLog = new char[i32InfoLogLength];
- glGetShaderInfoLog(uiFragShader, i32InfoLogLength, &i32CharsWritten, pszInfoLog);
- delete[] pszInfoLog;
- goto cleanup;
- }
- // 使用同样的方法加载Vertex Shader
- uiVertShader = glCreateShader(GL_VERTEX_SHADER);
- glShaderSource(uiVertShader, 1, (const char**)&pszVertShader, NULL);
- glCompileShader(uiVertShader);
- glGetShaderiv(uiVertShader, GL_COMPILE_STATUS, &bShaderCompiled);
- if (!bShaderCompiled)
- {
- glGetShaderiv(uiVertShader, GL_INFO_LOG_LENGTH, &i32InfoLogLength);
- pszInfoLog = new char[i32InfoLogLength];
- glGetShaderInfoLog(uiVertShader, i32InfoLogLength, &i32CharsWritten, pszInfoLog);
- delete[] pszInfoLog;
- goto cleanup;
- }
- // 创建着色器程序
- uiProgramObject = glCreateProgram();
- // 将Vertex和Fragment Shader绑定进去。
- glAttachShader(uiProgramObject, uiFragShader);
- glAttachShader(uiProgramObject, uiVertShader);
- //将用户自定义的顶点属性myVertex绑定到VERTEX_ARRAY。
- glBindAttribLocation(uiProgramObject, VERTEX_ARRAY, "myVertex");
- // 链接着色器程序
- glLinkProgram(uiProgramObject);
- // 判断链接是否成功的操作
- glGetProgramiv(uiProgramObject, GL_LINK_STATUS, &bLinked);
- if (!bLinked)
- {
- glGetProgramiv(uiProgramObject, GL_INFO_LOG_LENGTH, &i32InfoLogLength);
- pszInfoLog = new char[i32InfoLogLength];
- glGetProgramInfoLog(uiProgramObject, i32InfoLogLength, &i32CharsWritten, pszInfoLog);
- delete[] pszInfoLog;
- goto cleanup;
- }
- // 使用着色器程序
- glUseProgram(uiProgramObject);
- // 设置清除颜色,以RGBA的模式,每个分量的值从0.0到1.0
- glClearColor(0.6f, 0.8f, 1.0f, 1.0f);
- //生成一个顶点缓冲区对象
- glGenBuffers(1, &ui32Vbo);
- //绑定生成的缓冲区对象到GL_ARRAY_BUFFER
- glBindBuffer(GL_ARRAY_BUFFER, ui32Vbo);
- // 加载顶点数据
- uiSize = 3 * (sizeof(GLfloat) * 3); // Calc afVertices size (3 vertices * stride (3 GLfloats per vertex))
- glBufferData(GL_ARRAY_BUFFER, uiSize, afVertices, GL_STATIC_DRAW);
- // 画三角形
- {
- //清除颜色缓冲区。glClear同样也能清除深度缓冲区(GL_DEPTH_BUFFER)和模板缓冲区(GL_STENCIL_BUFFER_BIT)
- glClear(GL_COLOR_BUFFER_BIT);
- //获取myPMVMatrix在shader中的位置
- i32Location = glGetUniformLocation(uiProgramObject, "myPMVMatrix");
- //传值给获取到的位置,也就是将pfIdentity传给myPMVMatrix
- glUniformMatrix4fv( i32Location, 1, GL_FALSE, pfIdentity);
- //将VERTEX_ARRAY置为有效。
- glEnableVertexAttribArray(VERTEX_ARRAY);
- // 将定点数据传到VERTEX_ARRAY
- glVertexAttribPointer(VERTEX_ARRAY, 3, GL_FLOAT, GL_FALSE, 0, 0);
- //画一个三角形。
- glDrawArrays(GL_TRIANGLES, 0, 3);
- //SwapBuffers。就可以将三角形显示出来
- eglSwapBuffers(eglDisplay, eglSurface);
- }
- bRet = true;
- cleanup:
- // 释放资源
- if (uiProgramObject)
- glDeleteProgram(uiProgramObject);
- if (uiFragShader)
- glDeleteShader(uiFragShader);
- if (uiVertShader)
- glDeleteShader(uiVertShader);
- // Delete the VBO as it is no longer needed
- if (ui32Vbo)
- glDeleteBuffers(1, &ui32Vbo);
- return bRet;
- }
- bool DestroyEGLContext()
- {
- //第十步:结束OpenGL ES并删除创建的Windows(如果存在的话).eglminate已经负责清除context和surface。所以调用了eglTerminate后就无需再调用eglDestroySurface和eglDestroyContext了。
- if (eglDisplay != EGL_NO_DISPLAY)
- {
- eglMakeCurrent(eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
- eglDestroyContext(eglDisplay, eglContext );
- eglDestroySurface(eglDisplay, eglSurface );
- eglTerminate(eglDisplay);
- eglDisplay = EGL_NO_DISPLAY;
- }
- //第十一步:删除已经创建的eglWindow。这个是跟具体平台有关的。
- eglWindow = NULL;
- return true;
- }
- //这样,有关OpenGL ES画一个三角形的步骤就走完了,剩下的,只是在沃Phone的平台上进行相关额外的操作,比如窗口的创建,消息循环的处理等。
- //根据工程向导新建一个新工程,工程新建完,在**MainForm.cpp中
- //新增定义:
- TWindow *g_pThis = NULL;
- //在TMainForm::TMainForm(TApplication * pApp):TWindow(pApp)中加入
- m_bGLESInit = FALSE;
- g_pThis = this;
- //在case EVENT_WinInit:中加入:
- if (CreateEGLContext())
- {
- m_bGLESInit = TRUE;
- Sys_PostMessage(MESSAGE_PRIOR_LOWEST, 0, &PID_SELF, EVENT_FirstUser, GetWindowHwndId(), 0, NULL, 0);
- }
- bHandled = TRUE;
- //新增:
- case EVENT_FirstUser:
- {
- if(m_bGLESInit)
- {
- Render();
- Sys_PostMessage(MESSAGE_PRIOR_LOWEST, 0, &PID_SELF, EVENT_FirstUser, GetWindowHwndId(), 0, NULL, 0);
- }
- bHandled = TRUE;
- }
- break;
- case EVENT_GlesUpdateNotify:
- {
- if(m_bGLESInit)
- {
- TRectangle rt;
- GetClientBounds(&rt);
- MarkUpdateRectangle(&rt);
- }
- bHandled = TRUE;
- }
- break;
- //在case EVENT_WinClose:中加入:
- if (m_bGLESInit)
- {
- DestroyEGLContext();
- m_bGLESInit = FALSE;
- }
- // Stop the application since the main form has been closed
- pApp->SendStopEvent();
这个是在模拟器上跑的时候的代码,如果是在真机下跑,同在模拟器上跑,有以下区别: