我們可以為每個(gè)頂點(diǎn)添加顏色來增加圖形的細(xì)節(jié)枕荞,從而創(chuàng)建出有趣的圖像柜候。但是搞动,如果想讓圖形看起來更真實(shí),我們就必須有足夠多的頂點(diǎn)渣刷,從而指定足夠多的顏色鹦肿。這將會(huì)產(chǎn)生很多額外開銷,因?yàn)槊總€(gè)模型都會(huì)需求更多的頂點(diǎn)辅柴,每個(gè)頂點(diǎn)又需求一個(gè)顏色屬性箩溃。
紋理
這時(shí)候就需要用到紋理,紋理是一個(gè)2D圖片(甚至也有1D和3D的紋理)碌嘀,它可以用來添加物體的細(xì)節(jié)涣旨;
你可以想象紋理是一張繪有磚塊的紙,無縫折疊貼合到你的3D的房子上股冗,這樣你的房子看起來就像有磚墻外表了开泽。因?yàn)槲覀兛梢栽谝粡垐D片上插入非常多的細(xì)節(jié),這樣就可以讓物體非常精細(xì)而不用指定額外的頂點(diǎn)魁瞪。
紋理坐標(biāo)
為了能夠把紋理映射(Map)到三角形上穆律,我們需要指定三角形的每個(gè)頂點(diǎn)各自對(duì)應(yīng)紋理的哪個(gè)部分。這樣每個(gè)頂點(diǎn)就會(huì)關(guān)聯(lián)著一個(gè)紋理坐標(biāo)(Texture Coordinate)导俘。
紋理坐標(biāo)在x和y軸上峦耘,范圍為0到1之間(注意我們使用的是2D紋理圖像)。使用紋理坐標(biāo)獲取紋理顏色叫做采樣(Sampling)旅薄。紋理坐標(biāo)起始于(0, 0)辅髓,也就是紋理圖片的左下角,終始于(1, 1)少梁,即紋理圖片的右上角洛口。
注意:紋理坐標(biāo)和頂點(diǎn)坐標(biāo)不同,沒有負(fù)數(shù)凯沪。是在圖片的左下角開始為原點(diǎn)第焰。
頂點(diǎn)坐標(biāo)是在中間位置為原點(diǎn)。
紋理坐標(biāo)的看起來像這樣的:
float texCoords[] = {
0.0f, 0.0f, // 左下角
1.0f, 0.0f, // 右下角
0.5f, 1.0f // 上中
};
紋理環(huán)繞
因?yàn)榧y理都是從(0妨马,0)到(1挺举,1),那如果超過這個(gè)坐標(biāo)的之外的會(huì)發(fā)生什么烘跺?OpenGL默認(rèn)是重復(fù)這個(gè)紋理圖像湘纵,不過OpenGL也有給了我們更多的選擇:
前面提到的每個(gè)選項(xiàng)都可以使用glTexParameter*函數(shù)對(duì)單獨(dú)的一個(gè)坐標(biāo)軸設(shè)置(s、t(如果是使用3D紋理那么還有一個(gè)r)它們和x滤淳、y梧喷、z是等價(jià)的):
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紋理,因此紋理目標(biāo)是GL_TEXTURE_2D铺敌。
第二個(gè)參數(shù):指定設(shè)置的選項(xiàng)與應(yīng)用的紋理軸绊困。我們打算配置的是WRAP選項(xiàng),并且指定S和T軸适刀。
第三個(gè)參數(shù):傳遞一個(gè)環(huán)繞方式(Wrapping)秤朗,在這個(gè)例子中OpenGL會(huì)給當(dāng)前激活的紋理設(shè)定紋理環(huán)繞方式為GL_MIRRORED_REPEAT。
如果我們選擇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);
紋理過濾
紋理坐標(biāo)不依賴于分辨率(Resolution)作谭,它可以是任意浮點(diǎn)值,所以O(shè)penGL需要知道怎樣將紋理像素(Texture Pixel奄毡,也叫Texel)折欠。
你可以想象你打開一張.jpg格式圖片,不斷放大你會(huì)發(fā)現(xiàn)它是由無數(shù)像素點(diǎn)組成的吼过,這個(gè)點(diǎn)就是紋理像素锐秦;注意不要和紋理坐標(biāo)搞混,紋理坐標(biāo)是你給模型頂點(diǎn)設(shè)置的那個(gè)數(shù)組盗忱,OpenGL以這個(gè)頂點(diǎn)的紋理坐標(biāo)數(shù)據(jù)去查找紋理圖像上的像素酱床,然后進(jìn)行采樣提取紋理像素的顏色。
鄰近過濾
GL_NEAREST(也叫鄰近過濾趟佃,Nearest Neighbor Filtering)是OpenGL默認(rèn)的紋理過濾方式扇谣。當(dāng)設(shè)置為GL_NEAREST的時(shí)候,OpenGL會(huì)選擇中心點(diǎn)最接近紋理坐標(biāo)的那個(gè)像素闲昭。下圖中你可以看到四個(gè)像素罐寨,加號(hào)代表紋理坐標(biāo)。左上角那個(gè)紋理像素的中心距離紋理坐標(biāo)最近序矩,所以它會(huì)被選擇為樣本顏色:
線性過濾
GL_LINEAR(也叫線性過濾鸯绿,(Bi)linear Filtering)它會(huì)基于紋理坐標(biāo)附近的紋理像素,計(jì)算出一個(gè)插值贮泞,近似出這些紋理像素之間的顏色楞慈。一個(gè)紋理像素的中心距離紋理坐標(biāo)越近幔烛,那么這個(gè)紋理像素的顏色對(duì)最終的樣本顏色的貢獻(xiàn)越大啃擦。下圖中你可以看到返回的顏色是鄰近像素的混合色:
那么我看下這兩種在使用圖片的時(shí)候回有什么樣的視覺效果:
GL_NEAREST產(chǎn)生了顆粒狀的圖案,我們能夠清晰看到組成紋理的像素饿悬,而GL_LINEAR能夠產(chǎn)生更平滑的圖案令蛉,很難看出單個(gè)的紋理像素。
當(dāng)進(jìn)行放大(Magnify)和縮小(Minify)操作的時(shí)候可以設(shè)置紋理過濾的選項(xiàng),比如你可以在紋理被縮小的時(shí)候使用鄰近過濾珠叔,被放大時(shí)使用線性過濾蝎宇。我們需要使用glTexParameter*函數(shù)為放大和縮小指定過濾方式。這段代碼看起來會(huì)和紋理環(huán)繞方式的設(shè)置很相似:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
多級(jí)漸遠(yuǎn)紋理
想象一下祷安,假設(shè)我們有一個(gè)包含著上千物體的大房間姥芥,每個(gè)物體上都有紋理。有些物體會(huì)很遠(yuǎn)汇鞭,但其紋理會(huì)擁有與近處物體同樣高的分辨率凉唐。由于遠(yuǎn)處的物體可能只產(chǎn)生很少的片段,OpenGL從高分辨率紋理中為這些片段獲取正確的顏色值就很困難霍骄,因?yàn)樗枰獙?duì)一個(gè)跨過紋理很大部分的片段只拾取一個(gè)紋理顏色台囱。在小物體上這會(huì)產(chǎn)生不真實(shí)的感覺,更不用說對(duì)它們使用高分辨率紋理浪費(fèi)內(nèi)存的問題了读整。
OpenGL中有個(gè)叫多級(jí)漸遠(yuǎn)紋理簿训,意思就是越遠(yuǎn)的東西,會(huì)變的越小米间,像素也會(huì)變差强品。大概就像這樣:
手工為每個(gè)紋理圖像創(chuàng)建一系列多級(jí)漸遠(yuǎn)紋理很麻煩,幸好OpenGL有一個(gè)glGenerateMipmaps函數(shù)屈糊,在創(chuàng)建完一個(gè)紋理后調(diào)用它OpenGL就會(huì)承擔(dān)接下來的所有工作了择懂。
在渲染中切換多級(jí)漸遠(yuǎn)紋理級(jí)別(Level)時(shí),OpenGL在兩個(gè)不同級(jí)別的多級(jí)漸遠(yuǎn)紋理層之間會(huì)產(chǎn)生不真實(shí)的生硬邊界另玖。就像普通的紋理過濾一樣困曙,切換多級(jí)漸遠(yuǎn)紋理級(jí)別時(shí)你也可以在兩個(gè)不同多級(jí)漸遠(yuǎn)紋理級(jí)別之間使用NEAREST和LINEAR過濾。
就像紋理過濾一樣谦去,我們可以使用glTexParameteri將過濾方式設(shè)置為前面四種提到的方法之一:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
一個(gè)常見的錯(cuò)誤是慷丽,將放大過濾的選項(xiàng)設(shè)置為多級(jí)漸遠(yuǎn)紋理過濾選項(xiàng)之一。這樣沒有任何效果鳄哭,因?yàn)槎嗉?jí)漸遠(yuǎn)紋理主要是使用在紋理被縮小的情況下的:紋理放大不會(huì)使用多級(jí)漸遠(yuǎn)紋理要糊,為放大過濾設(shè)置多級(jí)漸遠(yuǎn)紋理的選項(xiàng)會(huì)產(chǎn)生一個(gè)GL_INVALID_ENUM錯(cuò)誤代碼。
生成紋理
和之前一樣妆丘,紋理也需要一個(gè)ID來引用和綁定紋理
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
現(xiàn)在紋理已經(jīng)綁定了锄俄,我們可以使用前面載入的圖片數(shù)據(jù)生成一個(gè)紋理了。紋理可以通過glTexImage2D來生成:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
函數(shù)很長(zhǎng)勺拣,參數(shù)也不少奶赠,所以我們一個(gè)一個(gè)地講解:
- 第一個(gè)參數(shù)指定了紋理目標(biāo)(Target)。設(shè)置為GL_TEXTURE_2D意味著會(huì)生成與當(dāng)前綁定的紋理對(duì)象在同一個(gè)目標(biāo)上的紋理(任何綁定到GL_TEXTURE_1D和GL_TEXTURE_3D的紋理不會(huì)受到影響)药有。
- 第二個(gè)參數(shù)為紋理指定多級(jí)漸遠(yuǎn)紋理的級(jí)別毅戈,如果你希望單獨(dú)手動(dòng)設(shè)置每個(gè)多級(jí)漸遠(yuǎn)紋理的級(jí)別的話苹丸。這里我們填0,也就是基本級(jí)別苇经。
- 第三個(gè)參數(shù)告訴OpenGL我們希望把紋理儲(chǔ)存為何種格式赘理。我們的圖像只有RGB值,因此我們也把紋理儲(chǔ)存為RGB值扇单。
- 第四個(gè)和第五個(gè)參數(shù)設(shè)置最終的紋理的寬度和高度商模。我們之前加載圖像的時(shí)候儲(chǔ)存了它們,所以我們使用對(duì)應(yīng)的變量蜘澜。
下個(gè)參數(shù)應(yīng)該總是被設(shè)為0(歷史遺留的問題)阻桅。- 第七和第八個(gè)參數(shù)定義了源圖的格式和數(shù)據(jù)類型。我們使用RGB值加載這個(gè)圖像兼都,并把它們儲(chǔ)存為char(byte)數(shù)組嫂沉,我們將會(huì)傳入對(duì)應(yīng)值。
- 最后一個(gè)參數(shù)是真正的圖像數(shù)據(jù)扮碧。
當(dāng)調(diào)用glTexImage2D時(shí)趟章,當(dāng)前綁定的紋理對(duì)象就會(huì)被附加上紋理圖像。然而慎王,目前只有基本級(jí)別(Base-level)的紋理圖像被加載了蚓土,如果要使用多級(jí)漸遠(yuǎn)紋理,我們必須手動(dòng)設(shè)置所有不同的圖像(不斷遞增第二個(gè)參數(shù))赖淤∈衿幔或者,直接在生成紋理之后調(diào)用 glGenerateMipmap
咱旱。這會(huì)為當(dāng)前綁定的紋理自動(dòng)生成所有需要的多級(jí)漸遠(yuǎn)紋理确丢。
生成了紋理和相應(yīng)的多級(jí)漸遠(yuǎn)紋理后,釋放圖像的內(nèi)存是一個(gè)很好的習(xí)慣吐限。
stbi_image_free(data);
全部的過程類似這樣:
/**
設(shè)置紋理
*/
- (GLuint)setupTexture:(NSString *)fileName{
// 1鲜侥、使用CoreGraphics把圖像轉(zhuǎn)換成bitmap data
// 獲取圖片的CGImageRef
CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
if (!spriteImage) {
NSLog(@"Failed to load image %@", fileName);
exit(1);
}
// 讀取圖片的大小
size_t width = CGImageGetWidth(spriteImage);
size_t height = CGImageGetHeight(spriteImage);
GLubyte * spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte)); //rgba共4個(gè)byte
CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4,
CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
//在CGContextRef上繪圖
CGContextDrawImage(spriteContext, CGRectMake(0, 0, width, height), spriteImage);
CGContextRelease(spriteContext);
// 2、設(shè)置紋理環(huán)繞和過濾方式诸典,加載圖片形成紋理
GLuint texture;
glGenBuffers(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
//紋理環(huán)繞
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); //超出紋理坐標(biāo)s軸的部分描函,會(huì)重復(fù)紋理坐標(biāo)的邊緣,產(chǎn)生一種邊緣被拉伸的效果
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); //超出紋理坐標(biāo)t軸的部分狐粱,會(huì)重復(fù)紋理坐標(biāo)的邊緣舀寓,產(chǎn)生一種邊緣被拉伸的效果
//紋理過濾
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST ); //紋理縮小,采用鄰近過濾(GL_NEAREST)
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); //紋理放大肌蜻,采用線性過濾(GL_LINEAR)
float fw = width, fh = height;
//加載圖片形成紋理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
glGenerateMipmap(GL_TEXTURE_2D);
free(spriteData);
return texture;
}
紋理應(yīng)用
說了這么多互墓,終于要開始實(shí)踐了。貼代碼容易宋欺,難的是理解轰豆,所以文字?jǐn)⒄f會(huì)比較多胰伍。
之前我們繪制過三角形齿诞,現(xiàn)在我把三角形變成四邊形酸休,在四邊形上添加圖片。
首先我們需要更新下頂點(diǎn)數(shù)據(jù)
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 // 左上
};
由于我們添加了一個(gè)額外的頂點(diǎn)屬性祷杈,我們必須告訴OpenGL我們新的頂點(diǎn)格式:
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
注意斑司,我們同樣需要調(diào)整前面兩個(gè)頂點(diǎn)屬性的步長(zhǎng)參數(shù)為8 * sizeof(float)。
接著我們需要調(diào)整頂點(diǎn)著色器使其能夠接受頂點(diǎn)坐標(biāo)為一個(gè)頂點(diǎn)屬性但汞,并把坐標(biāo)傳給片段著色器:
attribute vec4 position; //輸入?yún)?shù)1 (位置坐標(biāo))
attribute vec2 textCoordinate; //輸入?yún)?shù)2 (紋理坐標(biāo))
uniform mat4 rotateMatrix; //全局參數(shù)
varying lowp vec2 varyTextCoord; //紋理坐標(biāo)
void main()
{
varyTextCoord = vec2(textCoordinate.x, textCoordinate.y);
gl_Position = position ;
}
片段著色器應(yīng)該接下來會(huì)把輸出變量varyTextCoord作為輸入變量宿刮。
頂?shù)字?/p>
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap; //2d紋理采樣器,默認(rèn)的激活紋理單元(0),沒有分配值
void main()
{
gl_FragColor = texture2D(colorMap, varyTextCoord); //采樣紋理的顏色私蕾,第一個(gè)參數(shù)是紋理采樣器僵缺,第二個(gè)參數(shù)是對(duì)應(yīng)的紋理坐標(biāo)
}
我們使用GLSL內(nèi)建的texture函數(shù)來采樣紋理的顏色,它第一個(gè)參數(shù)是紋理采樣器踩叭,第二個(gè)參數(shù)是對(duì)應(yīng)的紋理坐標(biāo)磕潮。texture函數(shù)會(huì)使用之前設(shè)置的紋理參數(shù)對(duì)相應(yīng)的顏色值進(jìn)行采樣。這個(gè)片段著色器的輸出就是紋理的(插值)紋理坐標(biāo)上的(過濾后的)顏色容贝。
現(xiàn)在只剩下在調(diào)用glDrawElements之前綁定紋理來繪制圖形自脯,它會(huì)自動(dòng)把紋理賦值給片段著色器的采樣器:
/**
渲染,最后一步
*/
- (void)render {
//清屏
glClearColor(0, 1.0, 0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
CGFloat scale = [[UIScreen mainScreen] scale]; //獲取視圖放大倍數(shù)斤富,可以把scale設(shè)置為1試試
glViewport(self.frame.origin.x * scale, self.frame.origin.y * scale, self.frame.size.width * scale, self.frame.size.height * scale); //設(shè)置視口大小
glBindTexture(GL_TEXTURE_2D, _texture1);
glDrawArrays(GL_TRIANGLES, 0, 6); //6是頂點(diǎn)的數(shù)量
[self.context presentRenderbuffer:GL_RENDERBUFFER];
}
最后的結(jié)果就是這樣:
使用繼承GLKViewController的
使用GLKTextureLoader這個(gè)類可以直接進(jìn)行紋理加載
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"ic_dog" ofType:@"jpeg"];
GLKTextureInfo* textureInfo = [GLKTextureLoader textureWithContentsOfFile:filePath options:nil error:nil];
_baseEffect = [[GLKBaseEffect alloc] init];
_baseEffect.texture2d0.enabled = GL_TRUE;
_baseEffect.texture2d0.name = textureInfo.name;
最后繪制
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
[_baseEffect prepareToDraw];
glClearColor(0.3f, 0.6f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
int count = sizeof(indices) / sizeof(indices[0]);
glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_INT, 0);
}
居然是倒的膏潮,那么你有辦法讓他變成一下的方向嗎?