1. 前言
理解紋理之前,需要理解兩個(gè)概念:
- Fragment;
- Fragment 的片段插值;
官方描述如下:


總結(jié):
- 一個(gè) Fragment 對應(yīng)一個(gè)像素,只不過 Fragment 是一個(gè)數(shù)據(jù)模型,其中的數(shù)據(jù)提供給 Fragment Shader 最終渲染出這個(gè) Pixel 的 RGBA;
- 片段插值對每個(gè)屬性都其作用,不僅僅是顏色。這個(gè)功能可以幫助開發(fā)者節(jié)省大量的工作,比如減少輸入的頂點(diǎn)數(shù)量、減少輸入的顏色等;
- 紋理坐標(biāo)的個(gè)數(shù)最開始只和頂點(diǎn)數(shù)量一致,但是片段插值之后,每個(gè) Fragment 都對應(yīng)著有一個(gè)紋理坐標(biāo),從而可以在紋理中取出對應(yīng)的像素色值(后文會(huì)講);
2. 紋理的概念
我們已經(jīng)了解到,我們可以為每個(gè)頂點(diǎn)添加顏色來增加圖形的細(xì)節(jié),從而創(chuàng)建出有趣的圖像。但是,如果想讓圖形看起來更真實(shí),我們就必須有足夠多的頂點(diǎn),從而指定足夠多的顏色。雖然片段著色器會(huì)根據(jù)頂點(diǎn)和頂點(diǎn)輸入的屬性進(jìn)行片段插值的操作,但是仍然滿足不了很多場景。

此時(shí),紋理的概念就出現(xiàn)了。
紋理是一個(gè)2D圖片(甚至也有1D和3D的紋理),它可以用來添加物體的細(xì)節(jié),紋理類似于一張貼紙一樣,可以直接貼在圖元上。因?yàn)槲覀兛梢栽谝粡垐D片上插入非常多的細(xì)節(jié),這樣就可以讓物體非常精細(xì)而不用指定額外的頂點(diǎn)。

除了圖像以外,紋理也可以被用來儲(chǔ)存大量的數(shù)據(jù),這些數(shù)據(jù)可以發(fā)送到著色器上,但是這不是我們現(xiàn)在的主題。
3. 紋理坐標(biāo)
紋理坐標(biāo)是針對紋理而言的,對于 2D 紋理而言,紋理坐標(biāo)在 x 和 y 軸上,范圍為 0 到 1 之間,紋理坐標(biāo)起始于(0, 0),也就是紋理圖片的左下角,終于(1, 1),即紋理圖片的右上角。其坐標(biāo)系如下:

為了能夠把紋理映射(Map)到三角形上(頂點(diǎn)/圖元),我們需要指定三角形的每個(gè)頂點(diǎn)各自對應(yīng)紋理的哪個(gè)部分。這樣每個(gè)頂點(diǎn)就會(huì)關(guān)聯(lián)著一個(gè)紋理坐標(biāo)(Texture Coordinate),用來標(biāo)明該從紋理圖像的哪個(gè)部分采集片段顏色(采樣,后文會(huì)將)。
三個(gè)頂點(diǎn)對應(yīng)的紋理坐標(biāo)確定之后,因?yàn)榻?jīng)過光柵化階段之后會(huì)產(chǎn)生很多片段(Fragment)供片段著色器來渲染像素點(diǎn)最終的顏色。此時(shí),片段著色器會(huì)根據(jù)三個(gè)頂點(diǎn)關(guān)聯(lián)的紋理坐標(biāo)進(jìn)行采樣。同時(shí),還會(huì)根據(jù)三個(gè)頂點(diǎn)關(guān)聯(lián)的紋理坐標(biāo)來對每個(gè) Fragment 的紋理坐標(biāo)屬性進(jìn)行片段插值(Fragment Interpolation)。也就是為每個(gè) Fragment 都計(jì)算并關(guān)聯(lián)一個(gè)紋理坐標(biāo),再根據(jù)這個(gè)紋理坐標(biāo)去紋理中根據(jù)一定的規(guī)則獲取顏色(采樣),最終計(jì)算出這個(gè) Fragment 對應(yīng)的 pixel 的 RGBA;
這個(gè)步驟的理解很重要,關(guān)乎到能否理解后面的線性采樣和鄰近采樣為什么會(huì)產(chǎn)生不同的效果。
4. 環(huán)繞方式
環(huán)繞方式和我們平常接觸到的平鋪方式關(guān)聯(lián)性很大。
紋理坐標(biāo)的范圍通常是從(0, 0)到(1, 1),那如果我們把紋理坐標(biāo)設(shè)置在范圍之外會(huì)發(fā)生什么?OpenGL 默認(rèn)的行為是重復(fù)這個(gè)紋理圖像(,但OpenGL提供了更多的選擇:
-
GL_REPEAT:對紋理的默認(rèn)行為。重復(fù)紋理圖像; -
GL_MIRRORED_REPEAT:和GL_REPEAT一樣,但每次重復(fù)圖片是鏡像放置的; -
GL_CLAMP_TO_EDGE:紋理坐標(biāo)會(huì)被約束在 0 到 1 之間,超出的部分會(huì)重復(fù)紋理坐標(biāo)的邊緣,產(chǎn)生一種邊緣被拉伸的效果; -
GL_CLAMP_TO_BORDER:超出的坐標(biāo)為用戶指定的邊緣顏色;
不同行為的效果如下:

這是方式如下:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
- 第一個(gè)參數(shù):紋理目標(biāo)
我們使用2D紋理,所以是GL_TEXTURE_2D - 第二個(gè)參數(shù):紋理軸
- 第三個(gè)參數(shù):該軸上的環(huán)繞方式
如果我們選擇 GL_CLAMP_TO_BORDER 選項(xiàng),我們還需要指定一個(gè)邊緣的顏色。這需要使用 glTexParameter 函數(shù)的 fv 后綴形式,用 GL_TEXTURE_BORDER_COLOR 作為它的選項(xiàng),并且傳遞一個(gè)float數(shù)組作為邊緣的顏色值:
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
5. 紋理過濾
紋理坐標(biāo)相當(dāng)于臺(tái)球桌上框球的三腳架。當(dāng)紋理坐標(biāo)確定之后,這張圖片上哪些像素點(diǎn)需要被映射到 OpenGL(或者說Buffer)上就確定了;
此時(shí)考慮,從選取像素?cái)?shù)量的多少大概可以分為三種情況:
- 頂點(diǎn)坐標(biāo)選取了很多像素點(diǎn),也就是圖片縮放比例很?。?/li>

- 頂點(diǎn)坐標(biāo)選取了較多像素點(diǎn),圖片被稍微放大,展現(xiàn)部分更偏向細(xì)節(jié):

- 頂點(diǎn)坐標(biāo)選取了很少的像素點(diǎn),圖片被放的很大,展現(xiàn)部分更加粒子化:


針對第三種情況,如果直接將范圍內(nèi)的像素點(diǎn)進(jìn)行映射,那么效果就和實(shí)際效果一樣,粒子化(鋸齒化)很嚴(yán)重。
openGL 提供了兩種紋理過濾方案:
-
GL_NEAREST:鄰近過濾
鄰近過濾是 openGL 默認(rèn)的過濾方式,也就是會(huì)展示實(shí)際效果,其原理如下:

-
GL_LINEAR線性過濾
它會(huì)基于紋理坐標(biāo)附近的紋理像素,計(jì)算出一個(gè)插值,近似出這些紋理像素之間的顏色。一個(gè)紋理像素的中心距離紋理坐標(biāo)越近,那么這個(gè)紋理像素的顏色對最終的樣本顏色的貢獻(xiàn)越大。

在分辨率較低時(shí),兩種過濾方式的差別如下:

上圖可以看出,線性過濾的鋸齒化很輕,鄰近過濾的像素畫很嚴(yán)重。
一開始會(huì)無法理解為什么線性過濾時(shí),內(nèi)部像素點(diǎn)的鋸齒化也很低。按理來講,只有 4 個(gè)頂點(diǎn),那么最多也就是影響 4 個(gè)頂點(diǎn)的鋸齒化程度。這也是為什么文章一開始著重理解片段插值的原因。
雖然一開始只傳入三個(gè)或者四個(gè)頂點(diǎn),也就只關(guān)聯(lián)了和頂點(diǎn)數(shù)量一致的紋理坐標(biāo)。但是光柵化之后的片段著色器階段,會(huì)為每個(gè) Fragment 的紋理坐標(biāo)進(jìn)行片段插值,此時(shí):
Fragment.count == Pixel.count == TextureCoordinate.counts
所以,內(nèi)部的像素點(diǎn)也會(huì)有一個(gè)紋理坐標(biāo)作用在紋理過濾這個(gè)過程中,最終導(dǎo)致所有像素點(diǎn)都會(huì)被處理。
6. 多級漸遠(yuǎn)紋理
多級漸遠(yuǎn)紋理用于處理深度不一時(shí)的紋理貼圖的性能問題。
想象一下,假設(shè)我們有一個(gè)包含著上千物體的大房間,每個(gè)物體上都有紋理。有些物體會(huì)很遠(yuǎn),由于遠(yuǎn)處的物體可能只產(chǎn)生很少的片段,OpenGL 從高分辨率紋理中為這些片段獲取正確的顏色值就很困難,因?yàn)樗枰獙σ粋€(gè)跨過紋理很大部分的片段只拾取一個(gè)紋理顏色。
來看一個(gè)例子:
上圖中,假設(shè)兩個(gè)矩形的紋理坐標(biāo)都是 (0,1)、(1,1)、(1,0)、(0,0),也就是全量取紋理中的像素。
因?yàn)閮蓚€(gè)矩形深度不一樣,那么光柵化之后產(chǎn)生的 Fragment 數(shù)量也不一樣。假設(shè) Fragment 的數(shù)量一個(gè)是 100,一個(gè)是 10 ,紋理的像素點(diǎn)是 1000 個(gè)。較小的矩形如果也是全量取紋理中的數(shù)據(jù),根據(jù)線性過濾的計(jì)算方式, 1 個(gè)點(diǎn)就需要取周圍大概 100 個(gè)像素點(diǎn)來計(jì)算該點(diǎn)的色值進(jìn)行。而較大的矩形是 100 個(gè),大概只需要取周圍 10 個(gè)像素點(diǎn)來計(jì)算最終的色值。
上述的計(jì)算過程很不嚴(yán)謹(jǐn),單純的只是為了理解多級漸遠(yuǎn)紋理的概念而 YY 出來的;
雖然總計(jì)算量可能是一樣的,但是因?yàn)檫h(yuǎn)處的物體所占像素點(diǎn)很少,即使圖像質(zhì)量很細(xì)膩,人眼也無法區(qū)分。而近處的物體“很大”,人眼能夠區(qū)分,所以這個(gè)計(jì)算的消耗是值得的。
因此,推出了多級漸遠(yuǎn)紋理的概念,它簡單來說就是一系列的紋理圖像,后一個(gè)紋理圖像是前一個(gè)的二分之一。
多級漸遠(yuǎn)紋理背后的理念很簡單:距觀察者的距離超過一定的閾值,OpenGL會(huì)使用不同的多級漸遠(yuǎn)紋理,即最適合物體的距離的那個(gè)。
大概原理如下:

多級漸遠(yuǎn)紋理的類型有:
-
GL_NEAREST_MIPMAP_NEAREST:使用最鄰近的多級漸遠(yuǎn)紋理來匹配像素大小,并使用鄰近插值進(jìn)行紋理采樣 -
GL_LINEAR_MIPMAP_NEAREST:使用最鄰近的多級漸遠(yuǎn)紋理級別,并使用線性插值進(jìn)行采樣 -
GL_NEAREST_MIPMAP_LINEAR:在兩個(gè)最匹配像素大小的多級漸遠(yuǎn)紋理之間進(jìn)行線性插值,使用鄰近插值進(jìn)行采樣
GL_LINEAR_MIPMAP_LINEAR:在兩個(gè)鄰近的多級漸遠(yuǎn)紋理之間使用線性插值,并使用線性插值進(jìn)行采樣
其計(jì)算過程和距離選擇不需要開發(fā)者操太多心,只需要在創(chuàng)建完一個(gè)紋理后調(diào)用 glGenerateMipmaps,OpenGL 就會(huì)承擔(dān)接下來的所有工作了。后面的教程中你會(huì)看到該如何使用它。
就像紋理過濾一樣,我們可以使用glTexParameteri將過濾方式設(shè)置為前面四種提到的方法之一:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
另外,多級漸遠(yuǎn)紋理的枚舉其實(shí)和上文提到的兩種過濾方式是同一個(gè)類型的枚舉:

7. 紋理的創(chuàng)建和配置
上面講述了紋理相關(guān)的基本概念,現(xiàn)在需要使用紋理來繪圖了,需要做這幾件事:
- 紋理的創(chuàng)建
這一步和之前的 VBO、VAO 等創(chuàng)建過程類似,很簡單:
// 創(chuàng)建紋理
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
- 初始化紋理配置
紋理創(chuàng)建完成之后就需要對上面講述的概念進(jìn)行配置:
/** 環(huán)繞方式
* 第一個(gè)參數(shù):紋理目標(biāo)。我們使用2D紋理,所以是GL_TEXTURE_2D
* 第二個(gè)參數(shù):紋理軸
* 第三個(gè)參數(shù):該軸上的環(huán)繞方式
*/
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
// 紋理過濾
// 多級漸遠(yuǎn)紋理
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- 紋理的映射
見后文
- 繪圖
見后文
8. glTexImage2D 方法解析
紋理的映射就是需要把紋理的來源數(shù)據(jù)(如圖片)化到紋理對象上,一般而言是把一張圖片映射到紋理上。
映射紋理之前需要調(diào)用 glTexImage2D 來設(shè)置紋理的處理方式、數(shù)據(jù)源等信息。其入?yún)⑷缦拢?/p>
- 第一個(gè)參數(shù):指定了紋理目標(biāo)(Target)。
設(shè)置為 GL_TEXTURE_2D 意味著會(huì)生成與當(dāng)前綁定的紋理對象在同一個(gè)目標(biāo)上的紋理(任何綁定到 GL_TEXTURE_1D和GL_TEXTURE_3D 的紋理不會(huì)受到影響)。
- 第二個(gè)參數(shù):為紋理指定多級漸遠(yuǎn)紋理的級別
如果你希望單獨(dú)手動(dòng)設(shè)置每個(gè)多級漸遠(yuǎn)紋理的級別的話。這里我們填0,也就是基本級別。
- 第三個(gè)參數(shù):告訴OpenGL我們希望把紋理儲(chǔ)存為何種格式
我們的圖像只有RGB值,因此我們也把紋理儲(chǔ)存為RGB值。
- 第四個(gè)、五個(gè)參數(shù):設(shè)置最終的紋理的寬度和高度
我們之前加載圖像的時(shí)候儲(chǔ)存了它們,所以我們使用對應(yīng)的變量。
下個(gè)參數(shù)應(yīng)該總是被設(shè)為0(歷史遺留的問題)。
- 第七、八個(gè)參數(shù):定義了源圖的格式和數(shù)據(jù)類型。
我們使用RGB值加載這個(gè)圖像,并把它們儲(chǔ)存為char(byte)數(shù)組,我們將會(huì)傳入對應(yīng)值。
- 第九個(gè)參數(shù):真正的圖像數(shù)據(jù)
這里需要一個(gè)指針,指向圖片被解壓到內(nèi)存之后的存儲(chǔ)位置;
調(diào)用示例:
// 設(shè)置紋理數(shù)據(jù)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, cwidth, cheight, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
// 映射紋理
glGenerateMipmap(GL_TEXTURE_2D);
9. 使用 stb_image 庫加載圖片
映射紋理,首先需要?jiǎng)?chuàng)建并解碼圖片,官網(wǎng)教程中使用的是 stb_image 這個(gè)三方庫,其使用方法也比較簡單,下載源碼之后做如下配置即可:
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
源碼算上注釋有將近 8000 行,代碼就不贅述了:

而上面的配置就是根據(jù)當(dāng)前的平臺(tái)省略一些代碼,編譯時(shí)就不會(huì)有那么多源碼了。使用 stb_image 映射圖元的代碼如下:
NSString *path = [[NSBundle mainBundle] pathForResource:@"container" ofType:@"jpeg"];
char* cPath = (char*) [path cStringUsingEncoding:NSUTF8StringEncoding];
int cwidth, cheight, cnrChannels;
unsigned char *cdata = stbi_load(cPath, &cwidth, &cheight, &cnrChannels, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, cwidth, cheight, 0, GL_RGB, GL_UNSIGNED_BYTE, cdata);
glGenerateMipmap(GL_TEXTURE_2D);
stb_image 中的 stbi_load 本質(zhì)上是從這個(gè) path 上來解析圖片。這屬于圖形學(xué)中圖片格式、圖片解壓縮等相關(guān)領(lǐng)域的知識。
大體過一下相關(guān)概念:
- 物理世界中的人眼所見視圖可以無限精細(xì),但是電腦世界圖片由一定的像素點(diǎn)組成。像素的多少和人眼能識別的粒度有關(guān),這也是不同分辨率看上去細(xì)膩程度不同的本質(zhì)原因;
- 每個(gè)像素點(diǎn)由色值來表示,深度為 8 的 RGBA_8888 的圖片大小中,一個(gè)像素點(diǎn)所占字節(jié)為 8 bit * 4 = 32 bit = 4 byte。因此分辨率為 1024 * 1024 時(shí),圖片你的大小為 1024 * 1024 * 8byte = 4M,如果不壓縮,圖片在磁盤上占用的內(nèi)存就很大;
- 圖片壓縮格式分為有損壓縮和無損壓縮。有損壓縮的本質(zhì)是刪除附近的像素點(diǎn),加載到內(nèi)存后也無法恢復(fù),代表格式 JPG。無損壓縮的本質(zhì)是相同色值只存儲(chǔ)一次,加載到內(nèi)存時(shí)再恢復(fù),代表格式 JPEG。WBP 是集大成者,支持有損和無損,還支持 gif,但是本質(zhì)是通過解壓縮時(shí)間來換取功能或圖片質(zhì)量,解壓縮時(shí)間相對前兩者較長;
- 圖片加載到內(nèi)存之后,其所占內(nèi)存和圖片在磁盤上的大小無關(guān),而是和分辨率、圖片大小、顏色通道大小有關(guān);
- 圖片的加載過程就是解析磁盤上的源文件,然后通過解壓源文件中的被壓縮過的 bitmap 信息得出實(shí)際的 bitmap,比如無損壓縮需要根據(jù)信息進(jìn)行復(fù)原;
總之:stbi_load 方法的本質(zhì)就是獲取圖片信息、同時(shí)將圖片從磁盤加載到內(nèi)存;
10. 使用 iOS 中的方法加載圖片
glTexImage2D 所需要的紋理數(shù)據(jù)本質(zhì)上就是一個(gè)指針,指向解壓完成后的 bitmap;
弄清楚了本質(zhì)之后,其實(shí)使用 iOS 中的相關(guān)方法同樣可以完成圖片的加載操作:
// 獲取磁盤中的圖片數(shù)據(jù)
NSString *path = [[NSBundle mainBundle] pathForResource:@"pika" ofType:@"jpg"];
NSData *imageData = [NSData dataWithContentsOfFile:path];
UIImage *image = [UIImage imageWithData:imageData];
// 解壓圖片
CGDataProviderRef provider = CGImageGetDataProvider(image.CGImage);
CFDataRef cfdata = CGDataProviderCopyData(provider);
const UInt8 *p8 = CFDataGetBytePtr(cfdata);
// 映射
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.size.width, image.size.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, p8);
glGenerateMipmap(GL_TEXTURE_2D);
這里有兩個(gè)地方需要說明。
- NSData 的 bytes 方法
這個(gè)方法官方文檔描述是指向 NSData 的數(shù)據(jù)區(qū)域。但是在使用時(shí),如果直接把這個(gè)數(shù)據(jù)傳遞給 glTexImage2D,結(jié)果是怎么也加載不出圖片
其實(shí)官方文檔解釋也沒有錯(cuò),這個(gè)方法確實(shí)是指向 NSData 在內(nèi)存中的數(shù)據(jù)區(qū)。但是,這個(gè)數(shù)據(jù)是沒有經(jīng)過解壓的圖片數(shù)據(jù)??梢圆榭磳?yīng)的內(nèi)存:

從上圖可以看出,這個(gè)數(shù)據(jù)還有 Photoshop 和圖片的時(shí)間,很明顯,如果是 bitmap,里面全都是像素點(diǎn)的色值,怎么可能有時(shí)間信息?
所以,這里需要有一個(gè)基本認(rèn)知:
- 磁盤中的圖片文件包含圖片的基本信息 + 壓縮后的像素信息
因此,圖片上才可以看到很多信息:

- UIImage 的意義
UIImage 只是一個(gè)數(shù)據(jù)模型,映射的是磁盤上的圖片文件對應(yīng)的信息,所有才會(huì)有 width、height 等信息。
圖片的渲染是由 UIImageView 來完成,圖片的解壓是由 Provider 來完成,所以上面使用 CGDataProviderCopyData() 來獲取真實(shí)的 bitmap,然后再通過 CFDataGetBytePtr() 獲取到內(nèi)存中 bitmap 的指針,最終傳遞 glTexImage2D;
其實(shí)可以看到,UIImage 的大小也是圖片在磁盤上的大小,而并不是圖片被夾在到內(nèi)存之后的大小:

- 一個(gè)注意點(diǎn)
使用第三方庫 stb_image 解析時(shí),像素通道用的是 RGB,但是使用 UIImage時(shí),需要改成 RGBA,否則加載出來的圖片和正常圖片對比如下:

猜測 iOS 中的 ImageProvider 默認(rèn)使用 RGBA 來解壓并生成 bitmap;
11. 其他代碼
紋理映射完成了,還需要一個(gè)矩形圖片來進(jìn)行貼圖,頂點(diǎn)設(shè)置、屬性設(shè)置、VAO、EBO 等相關(guān)代碼如下:
float vertices[] = {
// ---- 位置 ---- ---- 顏色 ---- - 紋理坐標(biāo) -
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上
};
unsigned int indices[] = { // 注意索引從0開始!
0, 1, 3, // 第一個(gè)三角形
1, 2, 3 // 第二個(gè)三角形
};
GLuint VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
GLuint VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
GLuint EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 第一個(gè)屬性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)0);
glEnableVertexAttribArray(0);
// 第二個(gè)屬性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// 第三個(gè)屬性
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
[self configTexture];
[self useProgram];
頂點(diǎn)著色器代碼如下:
const char *vertexShaderString = "#version 300 es\n"
"layout (location = 0) in vec3 aPos;\n"
"layout (location = 1) in vec3 aColor;\n"
"layout (location = 2) in vec2 aTexCoord;\n"
"out vec3 ourColor;\n"
"out vec2 TexCoord;\n"
"void main(){\n"
"gl_Position = vec4(aPos, 1.0);\n"
"ourColor = aColor;\n"
"TexCoord = aTexCoord;\n"
"}\0";
GLint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderString,NULL);
glCompileShader(vertexShader);
片段著色器代碼如下:
const char *fragmentShaderString = "#version 300 es\n"
"out lowp vec4 FragColor;\n"
"in lowp vec3 ourColor;\n"
"in lowp vec2 TexCoord;\n"
"uniform sampler2D ourTexture;\n"
"void main(){\n"
"FragColor = texture(ourTexture, TexCoord);\n"
"}\0";
GLint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderString, NULL);
glCompileShader(fragmentShader);
繪制代碼:
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
glClearColor(0.3f, 0.6f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
}
12. 皮一下
原本的效果:

混個(gè)顏色:
FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);
結(jié)果:

13. EBO 和 uniform
這里在說幾個(gè)點(diǎn):
- EBO
本質(zhì)上是通過 EBO 來告訴著色器應(yīng)該如何連接頂點(diǎn),從而節(jié)省輸入的頂點(diǎn),進(jìn)而提高效率。
比如畫一個(gè)矩形需要兩個(gè)三角形,正常邏輯是使用 6 個(gè)頂點(diǎn):
float vertices[] = {
// 第一個(gè)三角形
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, 0.5f, 0.0f, // 左上角
// 第二個(gè)三角形
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
此時(shí)有 6 個(gè)頂點(diǎn),進(jìn)行了 6 次連線才組成了一個(gè)矩形。但是有兩個(gè)頂點(diǎn)是重合的,如果是上千個(gè)三角形,則浪費(fèi)了 50% 的效率。另外,圖元裝配階段多了一次連線。
所以,使用索引緩沖對象(EBO)來告訴著色器程序應(yīng)該如何組織圖形:
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
unsigned int indices[] = { // 注意索引從0開始!
0, 1, 3, // 第一個(gè)三角形
1, 2, 3 // 第二個(gè)三角形
};
EBO 的創(chuàng)建和使用和 VAO 大體相似,只不過需要注意的是,此時(shí)如果使用 glDrawCall() 進(jìn)行繪制,因?yàn)閮?nèi)部沒有 EBO 的邏輯,加上少了兩個(gè)頂點(diǎn),所以會(huì)導(dǎo)致繪制錯(cuò)誤。
使用 EBO 之后需要調(diào)用 glDrawElements() 來進(jìn)行繪制,該方法內(nèi)部實(shí)現(xiàn)了 EBO 指向 VAO/VBO 的索引邏輯,大概邏輯如下:

- uniform
作用:在著色器內(nèi)部聲明一個(gè)變量,CPU 可以向這個(gè)變量傳遞數(shù)據(jù),以此將數(shù)據(jù)傳遞到 GPU 供著色器使用;
使用方法,首先在著色器內(nèi)部聲明一個(gè)變量:
#version 330 core
out vec4 FragColor;
uniform vec4 ourColor; // 在OpenGL程序代碼中設(shè)定這個(gè)變量
void main()
{
FragColor = ourColor;
}
向 uniform 變量傳遞數(shù)據(jù):
float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
上述例子的大概效果就是三角形的顏色會(huì)隨著時(shí)間推移而變化,Demo 就不演示了,具體可以去官網(wǎng)看:https://learnopengl-cn.github.io/01%20Getting%20started/05%20Shaders/#uniform
- uniform sampler2D ourTexture;
sampler2D 是默認(rèn)紋理處理器;
14. 兩個(gè)紋理的混合
混個(gè)皮神吧~~
首先在片段著色器中聲明兩個(gè)采樣器:
#version 300 es
uniform sampler2D texture1;
uniform sampler2D texture2;
void main()
{
FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.5);
}
sampler2D 是紋理采樣器。如果采樣器不指定紋理單元的位置,默認(rèn)對第一個(gè)紋理單元進(jìn)行采樣;
什么是紋理單元?說白了就是個(gè)坑位,讓開發(fā)者有位置可以放置處理好的紋理,一般 OpenGL 會(huì)至少提供 16 個(gè)紋理單元,iOS 中提供了 32 個(gè):

上述片段著色器代碼表示:
- 使用 texture1 和 texture2 兩個(gè)采樣器進(jìn)行采樣;
- 兩個(gè)采樣器會(huì)根據(jù)自己內(nèi)部的紋理單元位置對指定紋理進(jìn)行采樣,默認(rèn)紋理位置是 0;
- 按照 0.5 進(jìn)行混合;
接下來就需要設(shè)置紋理采樣器中的紋理單元位置了:
- (void)configTexture {
NSString *path = [[NSBundle mainBundle] pathForResource:@"container" ofType:@"jpeg"];
// 1.創(chuàng)建
GLuint texture;
glGenTextures(1, &texture);
// 2.激活第一個(gè)紋理單元
glActiveTexture(GL_TEXTURE0);
// 3.綁定
glBindTexture(GL_TEXTURE_2D, texture);
// 4.映射
[self genTexture:texture path:path];
NSString *path2 = [[NSBundle mainBundle] pathForResource:@"pika" ofType:@"jpg"];
GLuint texture2;
glGenTextures(1, &texture2);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture);
[self genTexture:texture2 path:path2];
}
上述代碼中,首先創(chuàng)建了 2 個(gè)紋理。
然后調(diào)用 glActiveTexture 激活了紋理單元,默認(rèn)第一個(gè)紋理單元是激活的。激活的意思是,接下來我要往第一個(gè)紋理單元這個(gè)坑位里面放數(shù)據(jù)了。
緊接著綁定了第一個(gè)紋理。綁定的意思就是接下來的紋理設(shè)置、映射等函數(shù)都是針對這個(gè)紋理的;
最后是調(diào)用紋理映射函數(shù)進(jìn)行數(shù)據(jù)映射,上面已經(jīng)寫很多了,就不展示代碼了;
至此,紋理單元 1 和 紋理單元 2 中各有了兩個(gè)紋理數(shù)據(jù)了,接下來向著色器程序中傳遞 uniform 數(shù)據(jù)并繪制了:
// 傳遞數(shù)據(jù)之前先要激活著色器程序
glUseProgram(shaderProgram);
glUniform1i(glGetUniformLocation(shaderProgram, "texture1"), 0);
glUniform1i(glGetUniformLocation(shaderProgram, "texture2"), 1);
// drawRect中調(diào)用
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
向著色器程序中傳遞數(shù)據(jù)之前,需要先激活這個(gè)著色器程序;
另外還需要注意的是,glUniform1i 中的第二個(gè)參數(shù)是 Value,這里是直接設(shè)置采樣器屬于哪個(gè)紋理單元,用 0 、1 、2 這樣的值,不能傳 GL_TEXTURE0、GL_TEXTURE1 這種枚舉,因?yàn)槊杜e的值是這樣的:

這個(gè)值具體怎么用的不是很清楚,但是感覺是個(gè)指針一樣的東西,而 glUniform1i 第二個(gè)參數(shù)是 int 類型,所以這里不能傳遞指針;

最后,圖片翻轉(zhuǎn)的原理和操作就不贅述了~~~