前言:
从上一篇文章中,我们在win7下面搭建OpenGL ES 2.0开发环境的时候,成功运行了官方编程指导中提供的Hello_Triangle这个例子,最后得到的结果就是在窗口中绘制出一个红色的三角形,接下来我们就开始来解读这个demo的代码和实现过程。
在此之前,我们先大致了解整个绘制过程的基本步骤:
从工程的结构树我们可以看到esUtil是一个API工程,其中Common目录下包含了框架源码和OpenGL ES 2.0所需的头文件,关于这部分源码的具体内容我们在后面的学习中再做了解。这里我们要分析的是demo工程的源码,其实源码就只有Hello_Triangle.c这个文件。
一、main函数:
阅读源码的习惯当然是从main函数入口开始,那么我们来看看main函数:
int main ( int argc, char *argv[] )
{
ESContext esContext;
UserData userData;
esInitContext ( &esContext );
esContext.userData = &userData;
esCreateWindow ( &esContext, "Hello Triangle", 320, 240, ES_WINDOW_RGB );
if ( !Init ( &esContext ) )
return 0;
esRegisterDrawFunc ( &esContext, Draw );
esMainLoop ( &esContext );
}
这里我们首先声明了一个ESContext结构,这个是框架提供的一个结构体,用于存储一些公用的数据,这样就可以不自行去定义全局变量,当然这对于多平台的可移植性来说是必须的,因为在某些平台中是不支持全局变量的,例如:Symbian。这里我们可以直接在Common/esUtil.c中找到ESContext的定义:
typedef struct
{
/// Put your user data here...
void* userData;
/// Window width
GLint width;
/// Window height
GLint height;
/// Window handle
EGLNativeWindowType hWnd;
/// EGL display
EGLDisplay eglDisplay;
/// EGL context
EGLContext eglContext;
/// EGL surface
EGLSurface eglSurface;
/// Callbacks
void (ESCALLBACK *drawFunc) ( void* );
void (ESCALLBACK *keyFunc) ( void*, unsigned char, int, int );
void (ESCALLBACK *updateFunc) ( void*, float deltaTime );
} ESContext;
然后main中又声明了一个UserData结构,这个其实是我们在当前脚本中自行定义的结构体,用于存放一些用户自行定义的数据,这里我们可以看到结构体中只是封装了一个GLunit数据,这个其实就是OpenGL中的无符号整型(正整型),相当于C语言中的unsigned int。
typedef struct
{
// Handle to a program object
GLuint programObject;
} UserData;
这里以es为前缀的函数都是ES框架封装的一些方法:
esInitContext: 也是框架封装好的一个方法,用于对最开始声明的 ESContext结构进行初始化;Init:我们自行定义的初始化函数,主要用于初始化着色器、programObject等;
Draw:控制初始化后加载进来的顶点着色器和片元着色器,绘制最后窗口真正要显示出来的内容;
esRegisterDrawFunc:指定绘制回调的方法为此脚本中我们自行定义的Draw函数,如此系统每次绘制都会调用这个函数,渲染帧数据;
esMainLoop:进入主消息循环,此处是无限循环,直到窗口关闭为止。
二、Init函数:
在看完入口函数之后,我们接下来再看看入口函数中调用到的我们自定义的初始化函数Init:
///
// Initialize the shader and program object
//
int Init ( ESContext *esContext )
{
UserData *userData = esContext->userData;
GLbyte vShaderStr[] =
"attribute vec4 vPosition; \n"
"void main() \n"
"{ \n"
" gl_Position = vPosition; \n"
"} \n";
GLbyte fShaderStr[] =
"precision mediump float;\n"\
"void main() \n"
"{ \n"
" gl_FragColor = vec4 ( 1.0, 0.0, 0.0, 1.0 );\n"
"} \n";
GLuint vertexShader;
GLuint fragmentShader;
GLuint programObject;
GLint linked;
// Load the vertex/fragment shaders
vertexShader = LoadShader ( GL_VERTEX_SHADER, vShaderStr );
fragmentShader = LoadShader ( GL_FRAGMENT_SHADER, fShaderStr );
// Create the program object
programObject = glCreateProgram ( );
if ( programObject == 0 )
return 0;
glAttachShader ( programObject, vertexShader );
glAttachShader ( programObject, fragmentShader );
// Bind vPosition to attribute 0
glBindAttribLocation ( programObject, 0, "vPosition" );
// Link the program
glLinkProgram ( programObject );
// Check the link status
glGetProgramiv ( programObject, GL_LINK_STATUS, &linked );
if ( !linked )
{
GLint infoLen = 0;
glGetProgramiv ( programObject, GL_INFO_LOG_LENGTH, &infoLen );
if ( infoLen > 1 )
{
char* infoLog = malloc (sizeof(char) * infoLen );
glGetProgramInfoLog ( programObject, infoLen, NULL, infoLog );
esLogMessage ( "Error linking program:\n%s\n", infoLog );
free ( infoLog );
}
glDeleteProgram ( programObject );
return FALSE;
}
// Store the program object
userData->programObject = programObject;
glClearColor ( 0.0f, 0.0f, 0.0f, 0.0f );
return TRUE;
}
在这个函数中,首先我们定义了顶点着色器和片元着色器源码,这是OpenGL ES 2.0进行绘制必不可少的两个对象。
vShaderStr:顶点着色器源码——声明一个vec4类型(包含四个float的向量)的输入属性vPosition;设置着色器的入口函数main函数并且在main函数中把vPosition赋值给内置输出变量gl_Position,这个变量会传给图元装配阶段。
fShaderStr:片元着色器源码——为float类型变量指定一个默认的精度为mediump;在main函数中给内置输出变量gl_FragColor复制为vec4(1.0,0.0,0.0,1.0),这里就是用于指定片元输出的颜色为红色。
(注:虽然在这里我们将着色器源码以字符串变量的形式放在代码中,但在实际开发中,我们通常将着色器源码放在文件中,那样更方便代码管理。)
用LoadShader创建了顶点着色器和片元着色器之后,需要创建programObject对象(着色器对象跟programObject的关系就相当于编译器和连接器)。需要把着色器对象连接到programObject对象,一个programObject对象必须连接一个顶点着色器和一个片元着色器。
glCreateProgram:创建一个programObject对象
glAttachShader:着色器与programObject对象连接
glBindAttribLocation:设置顶点着色器属性vPosition的位置
glGetProgramiv:获取连接状态
glClearColor:设置glClear时的背景颜色
连接正常后把programObject对象存放到UserData中。
三、LoadShader函数:
///
// Create a shader object, load the shader source, and
// compile the shader.
//
GLuint LoadShader ( GLenum type, const char *shaderSrc )
{
GLuint shader;
GLint compiled;
// Create the shader object
shader = glCreateShader ( type );
if ( shader == 0 )
return 0;
// Load the shader source
glShaderSource ( shader, 1, &shaderSrc, NULL );
// Compile the shader
glCompileShader ( shader );
// Check the compile status
glGetShaderiv ( shader, GL_COMPILE_STATUS, &compiled );
if ( !compiled )
{
GLint infoLen = 0;
glGetShaderiv ( shader, GL_INFO_LOG_LENGTH, &infoLen );
if ( infoLen > 1 )
{
char* infoLog = malloc (sizeof(char) * infoLen );
glGetShaderInfoLog ( shader, infoLen, NULL, infoLog );
esLogMessage ( "Error compiling shader:\n%s\n", infoLog );
free ( infoLog );
}
glDeleteShader ( shader );
return 0;
}
return shader;
}
这是着色器的加载函数,加载步骤:
1.glCreateShader:创建指定类型(顶点或片元)的着色器对象
2.glShaderSource:加载着色器的源码
3.glCompileShader:编译着色器
4.glGetShaderiv:获得着色器编译状态(用于判断编译是否成功,若成功则返回着色器对象)
四、Draw函数:
///
// Draw a triangle using the shader pair created in Init()
//
void Draw ( ESContext *esContext )
{
UserData *userData = esContext->userData;
GLfloat vVertices[] = { 0.0f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f };
// Set the viewport
glViewport ( 0, 0, esContext->width, esContext->height );
// Clear the color buffer
glClear ( GL_COLOR_BUFFER_BIT );
// Use the program object
glUseProgram ( userData->programObject );
// Load the vertex data
glVertexAttribPointer ( 0, 3, GL_FLOAT, GL_FALSE, 0, vVertices );
glEnableVertexAttribArray ( 0 );
glDrawArrays ( GL_TRIANGLES, 0, 3 );
eglSwapBuffers ( esContext->eglDisplay, esContext->eglSurface );
}
Draw实现了绘制帧数据,具体步骤:
1.定义顶点数据坐标
2.glViewport:通过OpenGL ES设置绘制的2D渲染表面的起点(x,y)、宽和高,也就是最终的绘制区域(可视区域)
3.glClear:以指定的颜色清除颜色缓冲区,也就是清屏,这里使用的颜色就是Init初始化函数中glClearColor设置的颜色(故glClearColor需要在glClear之前执行)
4.glUseProgram:获取渲染所要用到的program对象,这样接下来的所有渲染都会使用连接到program对象的着色器
5.glVertexAttribPointer:加载几何图形和绘制图元,此demo中我们指定的图形是三角形,vVertices中定义了三角形的顶点坐标组,然后把顶点位置加载到OpenGL ES,并连接顶点着色器定义的属性vPosition。顶点着色器中的每个属性都有一个唯一的位置(或者说是索引,是一个unsigned int值)。所以glVertexAttribPointer的作用就是把顶点数据加载到着色器中指定索引(例如索引我为0的属性为vPosition)的属性
6.glEnableVertexAttribArray:启动通用顶点数组属性
7.glDrawArrays:告诉OpenGL ES要绘制的图元是三角形
8.eglSwapBuffers:把缓存中的数据显示到绑定的屏幕上(双缓冲技术:前台缓冲区(屏幕可见)和后台缓存区(不可见),等到后台缓冲区渲染完成后再与前台缓冲区交换,如此便可实现平滑过渡)。
eglSwapBuffers(esContext->eglDisplay, esContext->eglSurface);这是一个EGL函数,其输入参数为EGL显示区和窗口,这两个参数代表了实际的物理显示区和渲染区。
附加:
关于EGL,EGL 是 Khronos 组织创造的渲染 API(OpenGL ES)和操作系统窗口之间的接口,应用程序中引入EGL需要添加链接库:libEGL.lib,任何OpenGL ES在使用EGL渲染前需要做的事情:
1.查询设备上可用的显示初始化它;
2.创建一个渲染屏幕(隐藏平面或显示屏平面);
3.创建渲染上下文。