很多語言的入門均是從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í)際顏色的像素。

從上圖我們可以知道頂點(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