摘 要:
本篇文档从零开始,深入简出,跟大家介绍一下OpenGL ES的原理和开发。
前言
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
-
- EGLDisplay eglDisplay = 0;
- EGLConfig eglConfig = 0;
- EGLSurface eglSurface = 0;
- EGLContext eglContext = 0;
- EGLNativeWindowType eglWindow = 0;
-
- extern TWindow *g_pThis;
-
- bool TestEGLError()
- {
-
- EGLint iErr = eglGetError();
- if (iErr != EGL_SUCCESS)
- {
- return false;
- }
-
- return true;
- }
-
- bool CreateEGLContext()
- {
-
- eglWindow = (EGLNativeWindowType)g_pThis;
-
-
- eglDisplay = eglGetDisplay((EGLNativeDisplayType) EGL_DEFAULT_DISPLAY);
-
-
- EGLint iMajorVersion, iMinorVersion;
- if (!eglInitialize(eglDisplay, &iMajorVersion, &iMinorVersion))
- {
- return false;
- }
-
-
- 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
- };
-
-
- int iConfigs;
- if (!eglChooseConfig(eglDisplay, pi32ConfigAttribs, &eglConfig, 1, &iConfigs) || (iConfigs != 1))
- {
- return false;
- }
-
-
- eglSurface = eglCreateWindowSurface(eglDisplay, eglConfig, eglWindow, NULL);
-
- if(eglSurface == EGL_NO_SURFACE)
- {
- eglGetError();
- eglSurface = eglCreateWindowSurface(eglDisplay, eglConfig, NULL, NULL);
- }
-
- if (!TestEGLError())
- {
- return false;
- }
-
-
-
- eglBindAPI(EGL_OPENGL_ES_API);
- EGLint ai32ContextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE };
- eglContext = eglCreateContext(eglDisplay, eglConfig, NULL, ai32ContextAttribs);
-
- if (!TestEGLError())
- {
- return false;
- }
-
-
- eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);
- if (!TestEGLError())
- {
- return false;
- }
- return true;
- }
-
- bool Render()
- {
-
- 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
- };
-
-
-
-
- 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;
- GLuint uiProgramObject = 0;
-
- GLint bShaderCompiled;
- GLint bLinked;
-
-
- GLuint ui32Vbo = 0;
-
-
- GLfloat afVertices[] = { -0.4f,-0.4f,0.0f,
- 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;
-
-
- 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;
- }
-
-
- 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();
-
-
- glAttachShader(uiProgramObject, uiFragShader);
- glAttachShader(uiProgramObject, uiVertShader);
-
-
- 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);
-
-
- glClearColor(0.6f, 0.8f, 1.0f, 1.0f);
-
-
- glGenBuffers(1, &ui32Vbo);
-
-
- glBindBuffer(GL_ARRAY_BUFFER, ui32Vbo);
-
-
- uiSize = 3 * (sizeof(GLfloat) * 3);
- glBufferData(GL_ARRAY_BUFFER, uiSize, afVertices, GL_STATIC_DRAW);
-
-
- {
-
- glClear(GL_COLOR_BUFFER_BIT);
-
-
-
- i32Location = glGetUniformLocation(uiProgramObject, "myPMVMatrix");
-
-
- glUniformMatrix4fv( i32Location, 1, GL_FALSE, pfIdentity);
-
-
- glEnableVertexAttribArray(VERTEX_ARRAY);
-
-
- glVertexAttribPointer(VERTEX_ARRAY, 3, GL_FLOAT, GL_FALSE, 0, 0);
-
-
- glDrawArrays(GL_TRIANGLES, 0, 3);
-
-
- eglSwapBuffers(eglDisplay, eglSurface);
- }
- bRet = true;
-
- cleanup:
-
- if (uiProgramObject)
- glDeleteProgram(uiProgramObject);
- if (uiFragShader)
- glDeleteShader(uiFragShader);
- if (uiVertShader)
- glDeleteShader(uiVertShader);
-
-
- if (ui32Vbo)
- glDeleteBuffers(1, &ui32Vbo);
-
- return bRet;
- }
-
- bool DestroyEGLContext()
- {
-
- 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 = NULL;
- return true;
- }
-
- TWindow *g_pThis = NULL;
-
- m_bGLESInit = FALSE;
- g_pThis = this;
-
- 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;
-
- if (m_bGLESInit)
- {
- DestroyEGLContext();
- m_bGLESInit = FALSE;
- }
-
- pApp->SendStopEvent();
这个是在模拟器上跑的时候的代码,如果是在真机下跑,同在模拟器上跑,有以下区别:
1.
模拟器上使用的是自己创建的
windows
窗口,使用
ms window
的消息机制。但在真机上传的是
沃Phone
的窗口句柄。
2.
在调用
eglGetDisplay
的时候,模拟器上传入的是
hdc
,真机上传入的是
(EGLNativeDisplayType)EGL_DEFAULT_DISPLAY)
。
3.
调用
eglCreateWindowSurface
的时候,模拟器上使用的是
hWnd
,但是真机上传入的是
TWindows
4.
模拟器上的消息循环使用
ms
的机制,需要自己控制。真机上则需要加入
EVENT_FirstUser
、
EVENT_GlesUpdateNotify
等消息。具体可参见范例。