Hello, OpenGL World!

很多語言的入門均是從Hello World開始,那開啟OpenGL的旅程我們也從Hello World開始。但OpenGL的Hello World相對(duì)于其它語言來說是具有一定難度的,因?yàn)樵谕瓿伤鼤r(shí)我們需要理解許多的概念,這讓它變得沒那么容易。本文通過一個(gè)在iOS設(shè)備上繪制一個(gè)三角形的案例來講解OpenGL中的一些概念。
在iPhone中我們知道其坐標(biāo)的原點(diǎn)是從左上角開始向下向右增加的坐標(biāo)系,異于我們數(shù)學(xué)中所熟知的坐標(biāo)系,而OpenGL中的坐標(biāo)系則與數(shù)學(xué)中的坐標(biāo)系相差無幾(暫時(shí)忽略Z軸),不同的是OpenGL的坐標(biāo)范圍無論是哪一個(gè)方向上的范圍均為-1至1。



在本案例中,我們?cè)趇Phone上繪制了一個(gè)具有a、b、c三個(gè)頂點(diǎn)的三角形,通過下圖坐標(biāo)系我們可以很容易地知道三角形的三個(gè)頂點(diǎn)的坐標(biāo)分別是a(-0.5, -0.5)、b(0.5, -0.5)、c(0, 0.5)。



OpenGL中的世界是一個(gè)3D世界,而屏幕中的世界是一個(gè)2D世界,所以O(shè)penGL大多數(shù)的工作是將3D世界的坐標(biāo)轉(zhuǎn)化為2D世界的坐標(biāo),并在相應(yīng)的屏幕上現(xiàn)實(shí)相關(guān)的物體。OpenGL中將3D轉(zhuǎn)化為2D的工作是有圖形渲染管線管理的,圖形渲染管線大致完成兩部分工作:
1.將3D坐標(biāo)轉(zhuǎn)換為2D坐標(biāo);
2.將2D坐標(biāo)轉(zhuǎn)換為有實(shí)際顏色的像素。
圖像渲染管線的工作流程(圖片來源于LearnOpenGL CN)

從上圖我們可以知道頂點(diǎn)著色器需要的輸入是頂點(diǎn)數(shù)據(jù),OpenGL的世界是一個(gè)3D世界,所以O(shè)penGL的坐標(biāo)均是3D坐標(biāo)(x, y, z),前面我們已經(jīng)得到了三角形的三個(gè)頂點(diǎn)的2D坐標(biāo),因?yàn)槲覀円秩镜娜切问且粋€(gè)2D三角形,所以我們?cè)O(shè)置三角形三個(gè)頂點(diǎn)的z坐標(biāo)的值均為0,所以我們定義的頂點(diǎn)數(shù)據(jù)可以使用一個(gè)float的數(shù)組。

const float vertices[] = {
-0.5, -0.5, 0,      // point a
 0.5, -0.5, 0,      // point b
 0.0,  0.5, 0       // point c
};

在得到頂點(diǎn)數(shù)據(jù)以后,我們進(jìn)入圖像渲染管線工作的頂點(diǎn)著色器階段。在頂點(diǎn)著色器階段會(huì)在GPU上創(chuàng)建一塊內(nèi)存用于存儲(chǔ)頂點(diǎn)數(shù)據(jù),并告訴OpenGL如何解析這些頂點(diǎn)數(shù)據(jù),最后將解析出來的數(shù)據(jù)發(fā)送到顯卡上。在GPU上創(chuàng)建的這塊內(nèi)存會(huì)通過VBO(Vertex Buffer Object)頂點(diǎn)緩沖對(duì)象進(jìn)行管理,使用VBO可以一次性將大量的頂點(diǎn)數(shù)據(jù)發(fā)送到顯卡內(nèi)存中,頂點(diǎn)著色器可以立即訪問頂點(diǎn),節(jié)省資源。

頂點(diǎn)輸入

1.頂點(diǎn)緩沖對(duì)象的生成

頂點(diǎn)緩沖對(duì)象的生成是通過glGenBuffers (GLsizei n, GLuint* buffers)函數(shù)生成。下面的代碼中我們生成了一個(gè)VBO對(duì)象,該VBO對(duì)象有一個(gè)唯一的ID(GLUint類型其實(shí)就是unsigned int類型)。

GLuint  VBO;
// 參數(shù)含義:
// GLsizei n: 生成多少個(gè)VBO對(duì)象
// GLuint* buffers: 緩沖ID
glGenBuffers(1, &VBO);
2.頂點(diǎn)對(duì)象的綁定

使用glBindBuffer函數(shù)把VBO對(duì)象綁定到指定的目標(biāo)上,使用較多的是GL_ARRAY_BUFFER和GL_ELEMENT_ARRAY_BUFFER。

glBindBuffer(GL_ARRAY_BUFFER, VBO);
3.復(fù)制緩沖數(shù)據(jù)至頂點(diǎn)內(nèi)存

綁定緩沖后,在所綁定的目標(biāo)上的所有緩沖都會(huì)用來配置所綁定的VBO。這是可以使用glBufferData函數(shù)將緩沖數(shù)據(jù)復(fù)制到頂點(diǎn)內(nèi)存。

// 參數(shù)含義:
// target: 目標(biāo)緩沖類型
// size: 需要傳輸數(shù)據(jù)的大小
// data: 需要復(fù)制的數(shù)據(jù)
// usage: 顯卡管理給定數(shù)據(jù)的方式
// GL_STREAM_DRAW: 數(shù)據(jù)會(huì)改變較多
// GL_STATIC_DRAW: 數(shù)據(jù)不會(huì)或幾乎不會(huì)改變(因三角形的三個(gè)頂點(diǎn)固定不會(huì)改變,所以使用該類型)
// GL_DYNAMIC_DRAW: 數(shù)據(jù)會(huì)每次繪制改變
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

著色器(shader)

在OpenGL中如果我們需要渲染的話,我們必須至少實(shí)現(xiàn)一個(gè)頂點(diǎn)著色器和一個(gè)片段著色器。頂點(diǎn)著色器、幾何著色器、片段著色器都是可編程的。而著色器程序的編寫是使用著色器語言(glsl)進(jìn)行編寫的。

1.頂點(diǎn)著色器、片段著色器的編寫

在本文中我們不過多的去介紹著色器相關(guān)的知識(shí),在以后的文章中會(huì)詳細(xì)介紹著色器的內(nèi)容,前面我們提到,OpenGL進(jìn)行渲染至少需要一個(gè)頂點(diǎn)著色器和一個(gè)片段著色器,我們?cè)谶@里就貼出兩個(gè)著色器的相關(guān)glsl的代碼。

// 頂點(diǎn)著色器
char vShaderStr[] =
"#version 300 es                                \n"
"layout (location = 0) in vec4 vPosition;       \n"
"out vec4 fragColor;                            \n"
"void main()                                    \n"
"{                                              \n"
"   gl_Position = vPosition;                    \n"
"}                                              \n";

// 片段著色器
char fShaderStr[] =
"#version 300 es                                \n"
"precision mediump float;                       \n"
"out vec4 fragColor;                            \n"
"void main()                                    \n"
"{                                              \n"
"   fragColor = vec4(1.0, 0, 0, 1.0);           \n"
"}
2.著色器的創(chuàng)建和編譯
a.著色器的創(chuàng)建

創(chuàng)建著色器我們使用glCreateShader函數(shù),其返回的是一個(gè)GLuint類型ID,參數(shù)是需要?jiǎng)?chuàng)建的著色器的類型,可以使用GL_FRAGMENT_SHADER、GL_VERTEX_SHADER等值。當(dāng)我們創(chuàng)建頂點(diǎn)著色器的時(shí)候我們使用GL_VERTEX_SHADER類型,需要?jiǎng)?chuàng)建片段著色器的時(shí)候使用GL_FRAGMENT_SHADER類型。

GLuint vShader = glCreateShader(GL_VERTEX_SHADER);           // 創(chuàng)建頂點(diǎn)著色器
GLuint fShader = glCreateShader(GL_FRAGMENT_SHADER);         // 創(chuàng)建片段著色器
b.將著色器源碼附加至著色器對(duì)象

將著色器源碼附加至著色器對(duì)象上,我們使用glShaderSource函數(shù)。

// 綁定shader源碼
// 參數(shù)含義
// shader: 指定將源碼附加至哪個(gè)著色器
// count:  字符串源碼數(shù)組的個(gè)數(shù)
// string: 字符串源碼
// length: 字符串源碼的長(zhǎng)度
glShaderSource(vshader, 1, &vShaderStr, NULL);
glShaderSource(fshader, 1, &fShaderStr, NULL);
c.著色器編譯

著色器編譯使用glCompileShader函數(shù),傳入的值是需要編譯的著色器。

// 編譯著色器
glCompileShader(vShader);   // 編譯頂點(diǎn)著色器
glCompileShader(fshader);   // 編譯片段著色器

在編譯期間可能會(huì)出現(xiàn)一些錯(cuò)誤,我們要獲得對(duì)應(yīng)的錯(cuò)誤信息可以結(jié)合glGetShaderiv、glGetShaderInfoLog函數(shù)獲得相關(guān)的信息。

// 獲取編譯著色器失敗的相關(guān)消息
int result;
// 參數(shù)含義
// shader: 需要查詢的著色器
// pname: 查詢類別:GL_COMPILE_STATUS、GL_SHADER_TYPE、GL_DELETE_STATUS、GL_INFO_LOG_LENGTH、GL_SHADER_SOURCE_LENGTH
// params: 返回查詢對(duì)象的結(jié)果值
glGetShaderiv(shader, GL_COMPILE_STATUS, &result);
if (!result) {
    GLint infoLen = 0;
    glGetShaderiv(shaderType, GL_INFO_LOG_LENGTH, &infoLen);
    
    if (infoLen) {
        char *infoLog = malloc(sizeof(char) * infoLen);
        glGetShaderInfoLog(shaderType, infoLen, NULL, infoLog);
        NSLog(@"Error compiling shader:%@", [NSString stringWithUTF8String:infoLog]);
        free(infoLog);
    }
    
    glDeleteShader(shader);
}

上面的代碼是已經(jīng)封裝好的代碼其中的shaderType是從外界傳入,即創(chuàng)建著色器時(shí)所用的類型,shader值得是前文的vShader或者fShader,因著色器的創(chuàng)建和編譯的過程是相同的,不同的只是著色器的源碼以及著色器的類型,所以,將該整個(gè)過程封裝為一個(gè)函數(shù)。

- (GLuint)createShader:(GLenum)shaderType source:(const char *)source {

    // 創(chuàng)建shader
    GLuint shader = glCreateShader(shaderType);

    // 綁定shader源碼
    // 參數(shù)含義
    // shader: 指定將源碼附加至哪個(gè)著色器
    // count:  字符串源碼數(shù)組的個(gè)數(shù)
    // string: 字符串源碼
    // length: 字符串源碼的長(zhǎng)度
    glShaderSource(shader, 1, &source, NULL);
    // 編譯著色器
    glCompileShader(shader);

    // 獲取編譯著色器失敗的相關(guān)消息
    int result;
    // 參數(shù)含義
    // shader: 需要查詢的著色器
    // pname: 查詢類別:GL_COMPILE_STATUS、GL_SHADER_TYPE、GL_DELETE_STATUS、GL_INFO_LOG_LENGTH、GL_SHADER_SOURCE_LENGTH
    // params: 返回查詢對(duì)象的結(jié)果值
    glGetShaderiv(shader, GL_COMPILE_STATUS, &result);
    if (!result) {
        GLint infoLen = 0;
        glGetShaderiv(shaderType, GL_INFO_LOG_LENGTH, &infoLen);
    
        if (infoLen) {
            char *infoLog = malloc(sizeof(char) * infoLen);
            glGetShaderInfoLog(shaderType, infoLen, NULL, infoLog);
            NSLog(@"Error compiling shader:%@", [NSString stringWithUTF8String:infoLog]);
            free(infoLog);
        }
    
        glDeleteShader(shader);
        return 0;
    }

    return shader;
}
d.著色器程序

多個(gè)著色器合并得到最終的輸出需要依賴著色器程序?qū)ο?。首先?chuàng)建著色器程序?qū)ο?,接著將需要合并的著色器attach(附加)至著色器程序?qū)ο笊希詈笸ㄟ^著色器程序?qū)ο髮ttach的著色器鏈接起來。

- (void)setupProgram {
    // 著色器程序?qū)ο蟮纳?    self.program = glCreateProgram();
    // 將頂點(diǎn)著色器和片段著色器附加至著色器程序?qū)ο笊?    glAttachShader(self.program, self.vShader);
    glAttachShader(self.program, self.fShader);
    // 開始講著色器程序上的著色器鏈接
    glLinkProgram(self.program);

    int linkResult;
    // 獲取著色器程序鏈接的狀態(tài)
    glGetProgramiv(self.program, GL_LINK_STATUS, &linkResult);

    if (linkResult == GL_FALSE) {
        GLchar message[256];
        glGetProgramInfoLog(self.program, sizeof(message), 0, message);
        NSLog(@"Program link failure:%@", [NSString stringWithUTF8String:message]);
        exit(1);
    }
}

將著色器attach至著色器程序?qū)ο笊虾?,刪除相應(yīng)的著色器。

- (void)deleteShaders {
    glDeleteShader(self.vShader);
    glDeleteShader(self.fShader);
}

在上文我們已經(jīng)說過,在圖像渲染管線的頂點(diǎn)著色器階段除了會(huì)在GPU上創(chuàng)建一塊內(nèi)存存儲(chǔ)頂點(diǎn)數(shù)據(jù)外,還需要告訴OpenGL如何解析這些頂點(diǎn)數(shù)據(jù)。下面我們來看一下如何解析頂點(diǎn)數(shù)據(jù)。
解析頂點(diǎn)數(shù)據(jù)使用glVertexAttribPointer函數(shù)。

// 參數(shù)含義
// indx: 頂點(diǎn)屬性的位置
// size: 頂點(diǎn)屬性的大小
// type: 頂點(diǎn)屬性數(shù)據(jù)的類型
// normalized: 是否希望數(shù)據(jù)被標(biāo)準(zhǔn)化 GL_TRUE會(huì)把所有數(shù)據(jù)映射為0至1, GL_FALSE將所有數(shù)據(jù)映射為-1至1
// stride: 連續(xù)兩個(gè)頂點(diǎn)屬性之間的間隔
// ptr: 數(shù)據(jù)在緩沖中起始位置的偏移量
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);

告訴OpenGL如何解析頂點(diǎn)數(shù)據(jù)以后,啟用頂點(diǎn)屬性。

 // 啟用頂點(diǎn)屬性
glEnableVertexAttribArray(0);

最后,對(duì)物體進(jìn)行渲染。

// 開始渲染
glUseProgram(self.program);

// 參數(shù)含義
// mode: 繪制的圖元類型
// first: 起始索引
// count: 渲染的頂點(diǎn)數(shù)量
glDrawArrays(GL_TRIANGLES, 0, 3);

至此,OpenGL的渲染過程我們已經(jīng)完成了,在iOS中,我們借用GLKit(Apple 對(duì)OpenGL的一些封裝)的相關(guān)API區(qū)顯示繪制的圖形(為什么不直接講GLKit的使用?OpenGL是跨平臺(tái)的,不只針對(duì)于iOS)。這方面的使用較為簡(jiǎn)單,就是創(chuàng)建上下文,設(shè)置代理,實(shí)現(xiàn)代理方法等,我們最后的物體渲染于代理方法中實(shí)現(xiàn)。具體代碼如下。

- (void)setupOpenGLContext {
    self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];

    GLKView *view = (GLKView *)self.view; // 需要將storyboard中的view的class改為GLKView
    view.context = self.context;
    view.drawableDepthFormat = GLKViewDrawableColorFormatRGBA8888;
    view.delegate = self;
    [EAGLContext setCurrentContext:self.context];
}

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
    [self setupRenderBuffers];
}

至此,運(yùn)行Xcode可以在設(shè)備上看到一個(gè)紅色的三角形。在OpenGL的渲染過程中,如有不明白之處,可以結(jié)合圖像渲染管道的工作流程去理解,在這里我們只是簡(jiǎn)單的實(shí)現(xiàn)了將頂點(diǎn)數(shù)據(jù)輸入,經(jīng)頂點(diǎn)著色器,片段著色器處理,通過program鏈接著色器,最終渲染出圖形。

本文集的所有代碼均上傳至Github。

學(xué)習(xí)參考鏈接:
LearnOpenGL CN
OpenGL ES 3.0 Programming Guide

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容