【OpenGL ES】Hello Triangle
像Hello World一样,Hello Triangle是OpenGL ES的一个入门级例子,OpenGL ES 3.0完全基于着色器,如果没有绑定和加载合适的着色器,就无法绘制任何几何形状。下面介绍Hello Triangle的一般步骤,如何用OpenGL ES绘制一个三角形,需要做哪些事情,而每个步骤的详细原理则不作介绍,具体源码可参考https://github.com/geminy/aidear/tree/master/graphics/mu/examples/opengles3。
1、main
首先从main函数开始,main函数在esUtil.c文件中定义,代码如下所示:
// esUtil.c
int main ( int argc, char *argv[] )
{
// 1
// ESContext是一个很重要的struct
// 用于OpenGL ES的上下文管理
// 贯穿程序始终
// 稍后详细介绍ESContext
ESContext esContext;
// 2
// 初始化ESContext
memset ( &esContext, 0, sizeof( esContext ) );
// 3
// 通过esMain函数设置ESContext
// esMain函数还作了其它的许多工作
// 稍后详细介绍esMain
if ( esMain ( &esContext ) != GL_TRUE )
return 1;
// 4
// 进入程序主循环WinLoop函数
// 稍后详细介绍WinLoop
WinLoop ( &esContext );
// 5
// shutdown callback
if ( esContext.shutdownFunc != NULL )
esContext.shutdownFunc ( &esContext );
// 6
// 释放内存
if ( esContext.userData != NULL )
free ( esContext.userData );
// 7
// 程序结束
return 0;
}
程序运行起来时,初始界面如下:
2、ESContext
// esUtil.h
struct ESContext
{
void *platformData; // 平台数据
void *userData; // 用户数据
GLint width; // 窗口宽度
GLint height; // 窗口高度
EGLNativeDisplayType eglNativeDisplay; // egl display handle
EGLNativeWindowType eglNativeWindow; // egl window handle
EGLDisplay eglDisplay; // egl display
EGLContext eglContext; // egl context
EGLSurface eglSurface; // egl surface
void ( *drawFunc ) ( ESContext * ); // draw callback
void ( *shutdownFunc ) ( ESContext * ); // shutdown callback
void ( *keyFunc ) ( ESContext *, unsigned char, int, int ); // key callback
void ( *updateFunc ) ( ESContext *, float deltaTime ); // update callback
};
ESContext的几个callback函数通过如下函数进行注册:
// esUtil.c
// 注册draw callback
void esRegisterDrawFunc ( ESContext *esContext, void ( *drawFunc ) ( ESContext * ) )
{
esContext->drawFunc = drawFunc;
}
// 注册shutdown callback
void esRegisterShutdownFunc ( ESContext *esContext, void ( *shutdownFunc ) ( ESContext * ) )
{
esContext->shutdownFunc = shutdownFunc;
}
// 注册update callback
void esRegisterUpdateFunc ( ESContext *esContext, void ( *updateFunc ) ( ESContext *, float ) )
{
esContext->updateFunc = updateFunc;
}
// 注册key callback
void ESUTIL_API esRegisterKeyFunc ( ESContext *esContext,
void ( *keyFunc ) ( ESContext *, unsigned char, int, int ) )
{
esContext->keyFunc = keyFunc;
}
3、HelloTriangle
esMain——
HelloTriangle从上面提到的esMain函数开始,代码如下所示:
// Hello_Triangle.c
typedef struct
{
GLuint programObject; // opengl es program object handle
} UserData;
int esMain ( ESContext *esContext )
{
// 1
// 给用户数据分配内存
esContext->userData = malloc ( sizeof ( UserData ) );
// 2
// 创建窗口
// 窗口标题为Hello Triangle
// 窗口宽x高为320x240
// 窗口颜色缓冲区使用RGB通道
// 同时还更新了ESContext
// 稍后详细介绍esCreateWindow函数
esCreateWindow ( esContext, "Hello Triangle", 320, 240, ES_WINDOW_RGB );
// 3
// 初始化绘制三角形所需的opengl es shader和program
if ( !Init ( esContext ) )
{
return GL_FALSE;
}
// 4 注册shutdown和draw callback
esRegisterShutdownFunc ( esContext, Shutdown );
esRegisterDrawFunc ( esContext, Draw );
return GL_TRUE;
}
Init——
初始化绘制三角形所需的opengl es shader和program的Init函数主要包括四个步骤,LoadShader、glCreateProgram、glAttachShader和glLinkProgram,如下:
// Hello_Triangle.c
int Init ( ESContext *esContext )
{
// 在此之前已经给用户数据分配了内存
UserData *userData = esContext->userData;
// 顶点着色器
char vShaderStr[] =
"#version 300 es \n"
"layout(location = 0) in vec4 vPosition; \n"
"void main() \n"
"{ \n"
" gl_Position = vPosition; \n"
"} \n";
// 片段着色器
// fragColor = vec4 ( 1.0, 0.0, 0.0, 1.0 );表示三角形颜色为黄色
char fShaderStr[] =
"#version 300 es \n"
"precision mediump float; \n"
"out vec4 fragColor; \n"
"void main() \n"
"{ \n"
" fragColor = vec4 ( 1.0, 0.0, 0.0, 1.0 ); \n"
"} \n";
GLuint vertexShader;
GLuint fragmentShader;
GLuint programObject;
GLint linked;
// 加载顶点着色器和片段着色器
// 稍后详细介绍LoadShader函数
vertexShader = LoadShader ( GL_VERTEX_SHADER, vShaderStr );
fragmentShader = LoadShader ( GL_FRAGMENT_SHADER, fShaderStr );
// 创建程序对象
programObject = glCreateProgram ( );
if ( programObject == 0 )
{
return 0;
}
// 把加载好的顶点着色器和片段着色器与刚创建的程序对象绑定起来
glAttachShader ( programObject, vertexShader );
glAttachShader ( programObject, fragmentShader );
// 链接程序对象
glLinkProgram ( programObject );
// 检查程序对象链接状态
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;
}
// 存储程序对象
userData->programObject = programObject;
// 设置背景颜色为蓝色
glClearColor ( 0.0f, 1.0f, 0.0f, 0.0f );
return TRUE;
}
LoadShader——
LoadShader函数用于加载指定的着色器,主要包括三个步骤,glCreateShader、glShaderSource和glCompileShader,如下:
// Hello_Triangle.c
GLuint LoadShader ( GLenum type, const char *shaderSrc )
{
GLuint shader;
GLint compiled;
// 创建指定类型的着色器
// type为GL_VERTEX_SHADER或GL_FRAGMENT_SHADER
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 = 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;
}
Draw——
在Draw回调函数中,glViewPort设置一个矩形观察区域,glClear用之前在片段着色器中设置的黄色清除三角形的颜色缓冲区(这是必需的),glUseProgram使用之前在Init函数中创建的程序对象,vVertices定义一个顶点数组,用于设置三角形的三个顶点的坐标,坐标原点在屏幕*,水平X轴正方向向右(可视坐标从-1.0到正1.0),竖直Y轴正方向向上(可视坐标从-1.0到正1.0),glVertexAttribPointer和glEnableVertexAttribArray对顶点数组进行处理,最后通过glDrawArrays绘制三角形,代码如下所示:
// Hello_Triangle.c
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
};
glViewport ( 0, 0, esContext->width, esContext->height );
glClear ( GL_COLOR_BUFFER_BIT );
glUseProgram ( userData->programObject );
glVertexAttribPointer ( 0, 3, GL_FLOAT, GL_FALSE, 0, vVertices );
glEnableVertexAttribArray ( 0 );
glDrawArrays ( GL_TRIANGLES, 0, 3 );
}
Shutdown——
Shutdown回调只是通过glDeleteProgram删除之前创建的程序对象,避免内存泄漏,代码如下所示:
// Hello_Triangle.c
void Shutdown ( ESContext *esContext )
{
UserData *userData = esContext->userData;
glDeleteProgram ( userData->programObject );
}
4、CreateWindow
esCreateWindow——
下面介绍上面esMain函数中使用的esCreateWindow函数,主要就是通过libEGL和libX11中的API创建窗口,代码如下:
// esUtil.c
GLboolean esCreateWindow ( ESContext *esContext, const char *title, GLint width, GLint height, GLuint flags )
{
EGLConfig config;
EGLint majorVersion;
EGLint minorVersion;
// eglCreateContext函数用到的属性
// 通过EGL_CONTEXT_CLIENT_VERSION指定了opengl es版本为3
EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE };
if ( esContext == NULL )
{
return GL_FALSE;
}
// 设置宽和高
esContext->width = width;
esContext->height = height;
// 通过WinCreate函数创建窗口
// 稍后详细介绍WinCreate
if ( !WinCreate ( esContext, title ) )
{
return GL_FALSE;
}
// 获取egl display
// egl native display在上面的WinCreate函数中设置
esContext->eglDisplay = eglGetDisplay( esContext->eglNativeDisplay );
if ( esContext->eglDisplay == EGL_NO_DISPLAY )
{
return GL_FALSE;
}
// 初始化egl
if ( !eglInitialize ( esContext->eglDisplay, &majorVersion, &minorVersion ) )
{
return GL_FALSE;
}
{
// 配置egl
EGLint numConfigs = 0;
EGLint attribList[] =
{
EGL_RED_SIZE, 5,
EGL_GREEN_SIZE, 6,
EGL_BLUE_SIZE, 5,
EGL_ALPHA_SIZE, ( flags & ES_WINDOW_ALPHA ) ? 8 : EGL_DONT_CARE,
EGL_DEPTH_SIZE, ( flags & ES_WINDOW_DEPTH ) ? 8 : EGL_DONT_CARE,
EGL_STENCIL_SIZE, ( flags & ES_WINDOW_STENCIL ) ? 8 : EGL_DONT_CARE,
EGL_SAMPLE_BUFFERS, ( flags & ES_WINDOW_MULTISAMPLE ) ? 1 : 0,
EGL_RENDERABLE_TYPE, GetContextRenderableType ( esContext->eglDisplay ),
EGL_NONE
};
if ( !eglChooseConfig ( esContext->eglDisplay, attribList, &config, 1, &numConfigs ) )
{
return GL_FALSE;
}
if ( numConfigs < 1 )
{
return GL_FALSE;
}
}
// 创建egl window surface
// egl native window在上面的WinCreate函数中设置
esContext->eglSurface = eglCreateWindowSurface ( esContext->eglDisplay, config,
esContext->eglNativeWindow, NULL );
if ( esContext->eglSurface == EGL_NO_SURFACE )
{
return GL_FALSE;
}
// 创建egl context
esContext->eglContext = eglCreateContext ( esContext->eglDisplay, config,
EGL_NO_CONTEXT, contextAttribs );
if ( esContext->eglContext == EGL_NO_CONTEXT )
{
return GL_FALSE;
}
// make current
if ( !eglMakeCurrent ( esContext->eglDisplay, esContext->eglSurface,
esContext->eglSurface, esContext->eglContext ) )
{
return GL_FALSE;
}
return GL_TRUE;
}
WinCreate——
WinCreate函数创建X11窗口,同时设置egl native display和window,代码如下:
// esUtil_X11.c
static Display *x_display = NULL;
static Atom s_wmDeleteMessage;
EGLBoolean WinCreate(ESContext *esContext, const char *title)
{
Window root;
XSetWindowAttributes swa;
XSetWindowAttributes xattr;
Atom wm_state;
XWMHints hints;
XEvent xev;
Window win;
/*
* X11 native display initialization
*/
x_display = XOpenDisplay(NULL);
if ( x_display == NULL )
{
return EGL_FALSE;
}
root = DefaultRootWindow(x_display);
swa.event_mask = ExposureMask | PointerMotionMask | KeyPressMask;
win = XCreateWindow(
x_display, root,
0, 0, esContext->width, esContext->height, 0,
CopyFromParent, InputOutput,
CopyFromParent, CWEventMask,
&swa );
s_wmDeleteMessage = XInternAtom(x_display, "WM_DELETE_WINDOW", False);
XSetWMProtocols(x_display, win, &s_wmDeleteMessage, 1);
xattr.override_redirect = FALSE;
XChangeWindowAttributes ( x_display, win, CWOverrideRedirect, &xattr );
hints.input = TRUE;
hints.flags = InputHint;
XSetWMHints(x_display, win, &hints);
// make the window visible on the screen
XMapWindow (x_display, win);
XStoreName (x_display, win, title);
// get identifiers for the provided atom name strings
wm_state = XInternAtom (x_display, "_NET_WM_STATE", FALSE);
memset ( &xev, 0, sizeof(xev) );
xev.type = ClientMessage;
xev.xclient.window = win;
xev.xclient.message_type = wm_state;
xev.xclient.format = 32;
xev.xclient.data.l[0] = 1;
xev.xclient.data.l[1] = FALSE;
XSendEvent (
x_display,
DefaultRootWindow ( x_display ),
FALSE,
SubstructureNotifyMask,
&xev );
esContext->eglNativeWindow = (EGLNativeWindowType) win;
esContext->eglNativeDisplay = (EGLNativeDisplayType) x_display;
return EGL_TRUE;
}
GetContextRenderableType——
EGLint GetContextRenderableType ( EGLDisplay eglDisplay )
{
#ifdef EGL_KHR_create_context
const char *extensions = eglQueryString ( eglDisplay, EGL_EXTENSIONS );
if ( extensions != NULL && strstr( extensions, "EGL_KHR_create_context" ) )
{
return EGL_OPENGL_ES3_BIT_KHR;
}
#endif
return EGL_OPENGL_ES2_BIT;
}
5、WinLoop
前面在main函数中提到了程序主循环WinLoop函数,在这个函数中,循环实现基于libX11的事件系统,在循环中执行我们注册的回调函数和swap buffer,代码如下:
// esUtil_X11.c
void WinLoop ( ESContext *esContext )
{
struct timeval t1, t2;
struct timezone tz;
float deltatime;
gettimeofday ( &t1 , &tz );
while(userInterrupt(esContext) == GL_FALSE)
{
gettimeofday(&t2, &tz);
deltatime = (float)(t2.tv_sec - t1.tv_sec + (t2.tv_usec - t1.tv_usec) * 1e-6);
t1 = t2;
if (esContext->updateFunc != NULL)
esContext->updateFunc(esContext, deltatime);
if (esContext->drawFunc != NULL)
esContext->drawFunc(esContext);
eglSwapBuffers(esContext->eglDisplay, esContext->eglSurface);
}
}
GLboolean userInterrupt(ESContext *esContext)
{
XEvent xev;
KeySym key;
GLboolean userinterrupt = GL_FALSE;
char text;
// Pump all messages from X server. Keypresses are directed to keyfunc (if defined)
while ( XPending ( x_display ) )
{
XNextEvent( x_display, &xev );
if ( xev.type == KeyPress )
{
if (XLookupString(&xev.xkey,&text,1,&key,0)==1)
{
if (esContext->keyFunc != NULL)
esContext->keyFunc(esContext, text, 0, 0);
}
}
if (xev.type == ClientMessage) {
if (xev.xclient.data.l[0] == s_wmDeleteMessage) {
userinterrupt = GL_TRUE;
}
}
if ( xev.type == DestroyNotify )
userinterrupt = GL_TRUE;
}
return userinterrupt;
}