iOS OpenGL ES 學(xué)習(xí)筆記(一) 介紹了OpenGL的基本工作流程,知道了這些基礎(chǔ)知識(shí),就可以開(kāi)始寫(xiě)Demo了,第一個(gè)demo跟教程上的一樣,繪制一個(gè)三角形,不過(guò)它是在PC上用C++寫(xiě),這里是在iOS平臺(tái)上寫(xiě)。
完成的代碼在這里:代碼
先用GLKit完成一個(gè)最簡(jiǎn)單的版本,之后再用純OpenGL的方法完成一個(gè)一模一樣的效果。
1)我們需要一個(gè)OpenGL的content(上下文),前面已經(jīng)說(shuō)過(guò)了,OpenGL自身是一個(gè)巨大的狀態(tài)機(jī),OpenGL的狀態(tài)稱為content,所以第一步就是要?jiǎng)?chuàng)建這個(gè)context
//1
self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:self.context];
創(chuàng)建好了還需要將其設(shè)置為 currentContext。
2)我們需要一個(gè)的view,用來(lái)顯示我們繪制的結(jié)果,
//2
self.glkView = [[GLKView alloc] initWithFrame:self.view.bounds context:self.context];
self.glkView.delegate = self;
[self.view addSubview:self.glkView];
GLKView是蘋(píng)果封裝的專門(mén)用來(lái)顯示OpenGL繪制結(jié)果的view,現(xiàn)在知道這個(gè)就夠了;
它的初始化需要用到第一步創(chuàng)建的context,然后將其delegate設(shè)置為self,等一下再實(shí)現(xiàn)它的delegate方法;
3)輸入頂點(diǎn)數(shù)據(jù)
這一部分直接看https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/ 的 頂點(diǎn)輸入 部分即可。摘抄如下:
開(kāi)始繪制圖形之前,我們必須先給OpenGL輸入一些頂點(diǎn)數(shù)據(jù)。OpenGL是一個(gè)3D圖形庫(kù),所以我們?cè)贠penGL中指定的所有坐標(biāo)都是3D坐標(biāo)(x、y和z)。OpenGL不是簡(jiǎn)單地把所有的3D坐標(biāo)變換為屏幕上的2D像素;OpenGL僅當(dāng)3D坐標(biāo)在3個(gè)軸(x、y和z)上都為-1.0到1.0的范圍內(nèi)時(shí)才處理它。所有在所謂的標(biāo)準(zhǔn)化設(shè)備坐標(biāo)(Normalized Device Coordinates)范圍內(nèi)的坐標(biāo)才會(huì)最終呈現(xiàn)在屏幕上(在這個(gè)范圍以外的坐標(biāo)都不會(huì)顯示)。
由于我們希望渲染一個(gè)三角形,我們一共要指定三個(gè)頂點(diǎn),每個(gè)頂點(diǎn)都有一個(gè)3D位置。我們會(huì)將它們以標(biāo)準(zhǔn)化設(shè)備坐標(biāo)的形式(OpenGL的可見(jiàn)區(qū)域)定義為一個(gè)float數(shù)組。
float vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f };由于OpenGL是在3D空間中工作的,而我們渲染的是一個(gè)2D三角形,我們將它頂點(diǎn)的z坐標(biāo)設(shè)置為0.0。這樣子的話三角形每一點(diǎn)的深度(通常深度可以理解為z坐標(biāo),它代表一個(gè)像素在空間中和你的距離,如果離你遠(yuǎn)就可能被別的像素遮擋,你就看不到它了,它會(huì)被丟棄,以節(jié)省資源。)都是一樣的,從而使它看上去像是2D的。
標(biāo)準(zhǔn)化設(shè)備坐標(biāo)(Normalized Device Coordinates, NDC)
一旦你的頂點(diǎn)坐標(biāo)已經(jīng)在頂點(diǎn)著色器中處理過(guò),它們就應(yīng)該是標(biāo)準(zhǔn)化設(shè)備坐標(biāo)了,標(biāo)準(zhǔn)化設(shè)備坐標(biāo)是一個(gè)x、y和z值在-1.0到1.0的一小段空間。任何落在范圍外的坐標(biāo)都會(huì)被丟棄/裁剪,不會(huì)顯示在你的屏幕上。下面你會(huì)看到我們定義的在標(biāo)準(zhǔn)化設(shè)備坐標(biāo)中的三角形(忽略z軸):
image.png
與通常的屏幕坐標(biāo)不同,y軸正方向?yàn)橄蛏希?0, 0)坐標(biāo)是這個(gè)圖像的中心,而不是左上角。最終你希望所有(變換過(guò)的)坐標(biāo)都在這個(gè)坐標(biāo)空間中,否則它們就不可見(jiàn)了。
你的標(biāo)準(zhǔn)化設(shè)備坐標(biāo)接著會(huì)變換為屏幕空間坐標(biāo)(Screen-space Coordinates),這是使用你通過(guò)glViewport函數(shù)提供的數(shù)據(jù),進(jìn)行視口變換(Viewport Transform)完成的。所得的屏幕空間坐標(biāo)又會(huì)被變換為片段輸入到片段著色器中。定義這樣的頂點(diǎn)數(shù)據(jù)以后,我們會(huì)把它作為輸入發(fā)送給圖形渲染管線的第一個(gè)處理階段:頂點(diǎn)著色器。它會(huì)在GPU上創(chuàng)建內(nèi)存用于儲(chǔ)存我們的頂點(diǎn)數(shù)據(jù),還要配置OpenGL如何解釋這些內(nèi)存,并且指定其如何發(fā)送給顯卡。頂點(diǎn)著色器接著會(huì)處理我們?cè)趦?nèi)存中指定數(shù)量的頂點(diǎn)。
我們通過(guò)頂點(diǎn)緩沖對(duì)象(Vertex Buffer Objects, VBO)管理這個(gè)內(nèi)存,它會(huì)在GPU內(nèi)存(通常被稱為顯存)中儲(chǔ)存大量頂點(diǎn)。使用這些緩沖對(duì)象的好處是我們可以一次性的發(fā)送一大批數(shù)據(jù)到顯卡上,而不是每個(gè)頂點(diǎn)發(fā)送一次。從CPU把數(shù)據(jù)發(fā)送到顯卡相對(duì)較慢,所以只要可能我們都要嘗試盡量一次性發(fā)送盡可能多的數(shù)據(jù)。當(dāng)數(shù)據(jù)發(fā)送至顯卡的內(nèi)存中后,頂點(diǎn)著色器幾乎能立即訪問(wèn)頂點(diǎn),這是個(gè)非??斓倪^(guò)程。
頂點(diǎn)緩沖對(duì)象是我們?cè)贠penGL教程中第一個(gè)出現(xiàn)的OpenGL對(duì)象。就像OpenGL中的其它對(duì)象一樣,這個(gè)緩沖有一個(gè)獨(dú)一無(wú)二的ID,所以我們可以使用glGenBuffers函數(shù)和一個(gè)緩沖ID生成一個(gè)VBO對(duì)象:
unsigned int VBO; glGenBuffers(1, &VBO);OpenGL有很多緩沖對(duì)象類型,頂點(diǎn)緩沖對(duì)象的緩沖類型是GL_ARRAY_BUFFER。OpenGL允許我們同時(shí)綁定多個(gè)緩沖,只要它們是不同的緩沖類型。我們可以使用glBindBuffer函數(shù)把新創(chuàng)建的緩沖綁定到GL_ARRAY_BUFFER目標(biāo)上:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
從這一刻起,我們使用的任何(在GL_ARRAY_BUFFER目標(biāo)上的)緩沖調(diào)用都會(huì)用來(lái)配置當(dāng)前綁定的緩沖(VBO)。然后我們可以調(diào)用glBufferData函數(shù),它會(huì)把之前定義的頂點(diǎn)數(shù)據(jù)復(fù)制到緩沖的內(nèi)存中:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData是一個(gè)專門(mén)用來(lái)把用戶定義的數(shù)據(jù)復(fù)制到當(dāng)前綁定緩沖的函數(shù)。它的第一個(gè)參數(shù)是目標(biāo)緩沖的類型:頂點(diǎn)緩沖對(duì)象當(dāng)前綁定到GL_ARRAY_BUFFER目標(biāo)上。第二個(gè)參數(shù)指定傳輸數(shù)據(jù)的大小(以字節(jié)為單位);用一個(gè)簡(jiǎn)單的sizeof計(jì)算出頂點(diǎn)數(shù)據(jù)大小就行。第三個(gè)參數(shù)是我們希望發(fā)送的實(shí)際數(shù)據(jù)。第四個(gè)參數(shù)指定了我們希望顯卡如何管理給定的數(shù)據(jù)。它有三種形式:
- GL_STATIC_DRAW :數(shù)據(jù)不會(huì)或幾乎不會(huì)改變。
- GL_DYNAMIC_DRAW:數(shù)據(jù)會(huì)被改變很多。
- GL_STREAM_DRAW :數(shù)據(jù)每次繪制時(shí)都會(huì)改變。
三角形的位置數(shù)據(jù)不會(huì)改變,每次渲染調(diào)用時(shí)都保持原樣,所以它的使用類型最好是GL_STATIC_DRAW。如果,比如說(shuō)一個(gè)緩沖中的數(shù)據(jù)將頻繁被改變,那么使用的類型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW,這樣就能確保顯卡把數(shù)據(jù)放在能夠高速寫(xiě)入的內(nèi)存部分。
再iOS中代碼幾乎完全一樣
//3
const GLfloat vertexArray[] = {
0.0, 0.5, 0.0,
-0.5, -0.5, 0,
0.5, -0.5, 0
};
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertexArray), vertexArray, GL_STATIC_DRAW);
4)配置頂點(diǎn)著色器與片段著色器
前面說(shuō)過(guò)了OpenGL沒(méi)有默認(rèn)的頂點(diǎn)著色器與片段著色器,但是GLKit為我們封裝了GLKBaseEffect來(lái)實(shí)現(xiàn)最簡(jiǎn)單的頂點(diǎn)著色器和片段著色器,
//4
self.effect = [[GLKBaseEffect alloc] init];
// self.effect.constantColor = GLKVector4Make(0, 0, 1, 1);
OpenGL使用思維向量表示顏色,GLKBaseEffect默認(rèn)使用純白色,默認(rèn)采用RGBA的格式,如果想用藍(lán)色,用GLKVector4Make(0, 0, 1, 1)即可。
5)鏈接頂點(diǎn)屬性
這一部分也是直接看https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/ 的 鏈接頂點(diǎn)屬性 部分即可,摘抄如下:
頂點(diǎn)著色器允許我們指定任何以頂點(diǎn)屬性為形式的輸入。這使其具有很強(qiáng)的靈活性的同時(shí),它還的確意味著我們必須手動(dòng)指定輸入數(shù)據(jù)的哪一個(gè)部分對(duì)應(yīng)頂點(diǎn)著色器的哪一個(gè)頂點(diǎn)屬性。所以,我們必須在渲染前指定OpenGL該如何解釋頂點(diǎn)數(shù)據(jù)。
我們的頂點(diǎn)緩沖數(shù)據(jù)會(huì)被解析為下面這樣子:
image.png
- 位置數(shù)據(jù)被儲(chǔ)存為32位(4字節(jié))浮點(diǎn)值。
- 每個(gè)位置包含3個(gè)這樣的值。
- 在這3個(gè)值之間沒(méi)有空隙(或其他值)。這幾個(gè)值在數(shù)組中緊密排列(Tightly Packed)。
- 數(shù)據(jù)中第一個(gè)值在緩沖開(kāi)始的位置。
有了這些信息我們就可以使用glVertexAttribPointer函數(shù)告訴OpenGL該如何解析頂點(diǎn)數(shù)據(jù)(應(yīng)用到逐個(gè)頂點(diǎn)屬性上)了:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer函數(shù)的參數(shù)非常多,所以我會(huì)逐一介紹它們:
第一個(gè)參數(shù)指定我們要配置的頂點(diǎn)屬性。還記得我們?cè)陧旤c(diǎn)著色器中使用layout(location = 0)定義了position頂點(diǎn)屬性的位置值(Location)嗎?它可以把頂點(diǎn)屬性的位置值設(shè)置為0。因?yàn)槲覀兿M褦?shù)據(jù)傳遞到這一個(gè)頂點(diǎn)屬性中,所以這里我們傳入0。
第二個(gè)參數(shù)指定頂點(diǎn)屬性的大小。頂點(diǎn)屬性是一個(gè)vec3,它由3個(gè)值組成,所以大小是3。
第三個(gè)參數(shù)指定數(shù)據(jù)的類型,這里是GL_FLOAT(GLSL中vec*都是由浮點(diǎn)數(shù)值組成的)。
下個(gè)參數(shù)定義我們是否希望數(shù)據(jù)被標(biāo)準(zhǔn)化(Normalize)。如果我們?cè)O(shè)置為GL_TRUE,所有數(shù)據(jù)都會(huì)被映射到0(對(duì)于有符號(hào)型signed數(shù)據(jù)是-1)到1之間。我們把它設(shè)置為GL_FALSE。
第五個(gè)參數(shù)叫做步長(zhǎng)(Stride),它告訴我們?cè)谶B續(xù)的頂點(diǎn)屬性組之間的間隔。由于下個(gè)組位置數(shù)據(jù)在3個(gè)float之后,我們把步長(zhǎng)設(shè)置為3 * sizeof(float)。要注意的是由于我們知道這個(gè)數(shù)組是緊密排列的(在兩個(gè)頂點(diǎn)屬性之間沒(méi)有空隙)我們也可以設(shè)置為0來(lái)讓OpenGL決定具體步長(zhǎng)是多少(只有當(dāng)數(shù)值是緊密排列時(shí)才可用)。一旦我們有更多的頂點(diǎn)屬性,我們就必須更小心地定義每個(gè)頂點(diǎn)屬性之間的間隔,我們?cè)诤竺鏁?huì)看到更多的例子(譯注: 這個(gè)參數(shù)的意思簡(jiǎn)單說(shuō)就是從這個(gè)屬性第二次出現(xiàn)的地方到整個(gè)數(shù)組0位置之間有多少字節(jié))。
最后一個(gè)參數(shù)的類型是void*,所以需要我們進(jìn)行這個(gè)奇怪的強(qiáng)制類型轉(zhuǎn)換。它表示位置數(shù)據(jù)在緩沖中起始位置的偏移量(Offset)。由于位置數(shù)據(jù)在數(shù)組的開(kāi)頭,所以這里是0。我們會(huì)在后面詳細(xì)解釋這個(gè)參數(shù)。
現(xiàn)在我們已經(jīng)定義了OpenGL該如何解釋頂點(diǎn)數(shù)據(jù),我們現(xiàn)在應(yīng)該使用glEnableVertexAttribArray,以頂點(diǎn)屬性位置值作為參數(shù),啟用頂點(diǎn)屬性;頂點(diǎn)屬性默認(rèn)是禁用的。
在iOS中代碼也是一模一樣的
//5
glEnableVertexAttribArray(GLKVertexAttribPosition);
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 3, (GLfloat *)NULL);
6)繪制
實(shí)現(xiàn)GLKView的代理方法即可,
#pragma mark - GLKViewDelegate
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
//這兩句是設(shè)置背景顏色
glClearColor(0.5, 0, 0.5, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
//6
[self.effect prepareToDraw];
glDrawArrays(GL_TRIANGLES, 0, 3);
}
Run,期待的三角形出現(xiàn)了!??!

