【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;
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值