学习了OpenGL ES 2.0的基本知识后,我们来完成一个小demo:使用OpenGL ES 2.0绘制一个简单的三角形。这个demo的目的只是了解使用OpenGL ES 2.0绘制一个三角形时需要执行的步骤。

主要包括以下知识点:

1. 使用EGL创建屏幕上渲染的表面Surface
2. 加载顶点和片元着色器
3. 创建program对象,连接顶点和片元着色器,链接program对象。
4. 设置视口(viewport)。
5. 清除颜色缓冲区。
6. 绘制一个简单的图元。
7. 使颜色缓冲区的内容显示到EGL窗口表面(屏幕)。
从上面的步骤来看,使用OpenGL ES 2.0绘制一个三角形还挺复杂的,因为OpenGL ES 2.0完全基于着色器,也就是说如果你不加载和绑定合适的着色器,将不能绘制任何东西,所以要比固定管线的OpenGL ES1.x复杂很多。这个demo中使用了OpenGL ES 2.0 Programming Guide书中提供的代码框架,这个框架是跨平台的,可以让我们专注的学习OpenGL ES 2.0相关的API。凡是es开头的函数都是框架封装的,比如:esInitialize()。框架源码地址:http://www.opengles-book.com/OpenGL_ES_Programming_Guide_v1.0.2.zip
demo需要运行到AMD’s OpenGL ES 2.0 emulator上,安装模拟器很简单,前提是你机器的显卡需要支持OpenGL 2.0。模拟器的安装可以参考这篇博客:win7下搭建opengl es 2.0开发环境。下载OpenGL ES 2.0 Programming Guide书中的例子代码解压,下面是例子中各目录的含义:
Common/*:包含框架源码和OpenGL ES 2.0所需的头文件。
Chapter_X/*:书中所有章节的例子源码。

下面看demo的代码,下面是main函数:

int main( void ) {
	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。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;

userData是一个void*,可以保存一些有用的数据。
UserData是demo里面封装的一个结构,内含program对象:

typedef struct{
	GLuint programObj;
}UserData;

esInitContext函数用来初始化ESContext结构。esCreateWindow函数创建一个指定宽、高和标题的窗口,最后一个参数是一个位标志,这里使用RGB framebuffer,具体含义等以后学习EGL时再讲。这个函数使用EGL在屏幕上创建一个渲染表面并附加到一个窗口上,EGL是跨平台的,它是OpenGL ES跟本地窗口系统的中介,可以用它来创建渲染表面和上下文。创建完窗口之后,就需要初始化着色器、program对象等OpenGL ES所需要的东东了。初始化完成后指定一个回调函数draw,系统会循环执行这个函数,渲染帧数据。最后执行esMainLoop,进入主消息循环,是一个无限循环,直到窗口被关闭为止。
下面重点来看看init函数:

int init( ESContext *esContext ) {
	UserData *userData = ( UserData* )esContext->userData;

	const char* vertexShaderSrc = 
	 "attribute vec4 vPosition;     \n"
      "void main()                  \n"
      "{                            \n"
      "   gl_Position = vPosition;  \n"
      "}                            \n";

	const char* fragmentShaderSrc = 
      "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 programObj;
	GLint linked;

	vertexShader = loadShader( GL_VERTEX_SHADER, vertexShaderSrc );
	fragmentShader = loadShader( GL_FRAGMENT_SHADER, fragmentShaderSrc );

	programObj = glCreateProgram();
	if( programObj == 0 ) {
		return 0;
	}

	glAttachShader( programObj, vertexShader );
	glAttachShader( programObj, fragmentShader );

	glBindAttribLocation( programObj, 0, "vPosition" );

	glLinkProgram( programObj );

	glGetProgramiv( programObj, GL_LINK_STATUS, &linked );

	if( !linked ) {
		GLuint infoLen = 0;
		glGetProgramiv( programObj, GL_INFO_LOG_LENGTH, &infoLen );

		if(infoLen > 1) {
			char *infoLog = ( char* )malloc( sizeof(char) * infoLen );

			glGetProgramInfoLog( programObj, infoLen, NULL, infoLog );
			esLogMessage( "Error linking program:\n%s\n", infoLog );

			free(infoLog);
		}
		glDeleteProgram( programObj );
		return FALSE;
	}

	userData->programObj = programObj;

	glClearColor( 0.0f, 0.0f, 0.0f, 1.0f );

	return TRUE;
}

首先定义了顶点着色器和片元着色器源码,因为在OpenGL ES 2.0中,不加载这两个东东是不能绘制的。顶点着色器代码vertexShaderSrc声明了一个vec4类型(包含4个float的向量)的输入属性vPosition,main函数是着色器的入口函数,在main函数中把vPosition赋值给内置输出变量 gl_Position,这个变量会传给图元装配阶段。片元着色器代码fragmentShaderSrc 首先为float变量指定了一个默认的精度mediump,然后在main函数中给内置输出变量gl_FragColor赋值为vec4 ( 1.0, 0.0, 0.0, 1.0 ),这里指定所有的片元输出颜色为红色。一般来说,在开发中着色器代码不会以字符串常量的形式出现在代码中,会放到文件中,这里是为了简单方便。接着调用loadShader函数加载着色器代码,loadShader函数源码:

GLuint loadShader( GLenum type, const char* shaderSrc ) {
	GLuint shader;
	GLint compiled;

	shader = glCreateShader( type );
	if(shader == 0) {
		return 0;
	}

	glShaderSource( shader, 1, &shaderSrc, NULL );
	glCompileShader( shader );

	glGetShaderiv( shader, GL_COMPILE_STATUS, &compiled );

	if( !compiled ) {
		GLint infoLen = 0;
		glGetShaderiv( shader, GL_INFO_LOG_LENGTH, &infoLen );

		if( infoLen > 1 ) {
			char *infoLog = ( char* )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;
}

首先调用glCreateShader创建指定类型(顶点或片元)的着色器对象,接着调用glShaderSource加载着色器代码,然后调用glCompileShader编译着色器。glGetShaderiv函数是获得着色器编译状态的,判断是否发生了错误。如果一切正常,则返回创建的着色器对象。
继续回到init函数,创建了顶点着色器和片元着色器之后需要创建program对象(着色器对象跟program对象的关系可以理解成编译器和链接器)。需要把着色器对象连接到program对象,一个program对象必须连接一个顶点着色器和一个片元着色器。接着设置顶点着色器属性vPosition的位置为0,即绑定到位置0,在稍后设置顶点数据时有用:

 glBindAttribLocation( programObj, 0, "vPosition" );

之后链接program对象,获取链接状态,检查是否出现错误,最后把创建的program对象保存到UserData中。

glClearColor( 0.0f, 0.0f, 0.0f, 1.0f );

上面这行代码是设置glClear时的背景颜色。

现在回到main函数:

 esRegisterDrawFunc( &esContext, draw );

这行代码是指定回调的draw函数,这个函数会一直循环调用的,draw函数源码如下:

void draw( ESContext *esContext ) {
	UserData *userData = ( UserData* )esContext->userData;
	GLfloat vertices[] = { 0.0f,  0.5f, 0.0f, 
                           -0.5f, -0.5f, 0.0f,
                            0.5f, -0.5f, 0.0f };
	glViewport( 0, 0, esContext->width, esContext->height );
	glClear( GL_COLOR_BUFFER_BIT );

	glUseProgram( userData->programObj );

	glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 0, vertices );
	glEnableVertexAttribArray( 0 );

	glDrawArrays( GL_TRIANGLES, 0, 3 );

	eglSwapBuffers( esContext->eglDisplay, esContext->eglSurface );
}

draw函数实现了绘制帧数据,在draw函数中首先定义了顶点数据坐标,然后调用了glViewport,通知OpenGL ES将要绘制的2D渲染表面的起点(x, y)、宽和高,就是设置最终绘制区域(可视区域)。glClear( GL_COLOR_BUFFER_BIT )是以指定的颜色清除颜色缓冲区,就是清屏,这里将使用glClearColor设置的颜色,glClearColor需要在glClear之前执行。为了使用program对象进行渲染,需要调用glUseProgram函数,这样接下来的所有渲染都会使用连接到program对象的着色器。接下来完成加载几何图形和绘制图元,我们指定几何图形为三角形,指定三角形的顶点坐标数组vertices,然后把顶点位置加装到OpenGL ES,并连接顶点着色器定义的属性vPosition。还记得之前把vPosition变量绑定到顶点着色器属性0位置吗?因为顶点着色器中的每个属性都有一个唯一的位置(可以理解为索引,是一个unsigned int值),我们使用glVertexAttribPointer函数把顶点数据加载到着色器中索引为0的属性。glEnableVertexAttribArray是启用通用顶点数组属性。glDrawArrays函数是告诉OpenGL ES将要绘制的图元是三角形。最后执行eglSwapBuffers把缓存中的数据显示到绑定的屏幕上, 这里使用了双缓冲技术:前台缓冲区(屏幕上可见)和后台缓冲区(不可见),等到后台缓冲区渲染完成后再跟前台缓冲区交换,实现平滑过渡,如果直接更新前台缓冲区数据,会出现一闪闪的效果。最终看到的效果如图:

OpenGL ES 2.0渲染管线

Opengl es 2.0实现了可编程的图形管线,比起1.x的固定管线要复杂和灵活很多,由两部分规范组成:Opengl es 2.0 API规范和Opengl es着色语言规范。下图是Openg...

阅读全文

win7下搭建opengl es 2.0开发环境

1. 下载AMD的OpenGL ES2.0的模拟器,下载地址: http://www.opengles-book.com/ESEmulator.2009-04-28-v1.4.APRIL_2009_RELEASE.msi 2. 下载《OpenGL ES2.0 P...

阅读全文

1 条评论

欢迎留言