在OpenGL ES中另一種爲多邊形定義顏色創建材質的方法是將紋理映射到多邊形。這是一種很實用的方法,它可以產生很漂亮的外觀並節省大量的處理器時間。比如說,你想在遊戲中造一個磚牆。你當然可以創建一個具有幾千個頂點的複雜物體來定義每塊磚以及磚之間的泥灰。
或者你可以創建一個由兩個三角形構成的方塊(四個頂點),然後將磚的照片映射上去。簡單的幾何體通過紋理映射的方法比使用材質的複雜幾何體的渲染快得多。
功能啓動
爲使用紋理,我們需要打開OpenGL的一些開關以啓動我們需要的一些功能:
glEnable(GL_TEXTURE_2D);
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_SRC_COLOR);
第一個函數打開所有兩維圖像的功能。這個調用是必不可缺的;如果你沒有打開此功能,那麼你就無法將圖像映射到多邊形上。它可以在需要時打開和關閉,但是通常不需要這樣做。你可以啓動此功能而在繪圖時並不使用它,所以通常只需在setup方法中調用一次。
下一個調用打開了混色(blending)功能。 混色提供了通過指定源和目標怎樣組合而合成圖像的功能。例如,它可以允許你將多個紋理映射到多邊形中以產生一個有趣的新的紋理。然而在OpenGL中,“混色”是指合成任何圖像或圖像與多邊形表面合成,所以即使你不需要將多個圖像混合,你也需要打開此功能。
最後一個調用指定了使用的混色方法。混色函數定義了源圖像怎樣與目標圖像或表面合成。OpenGL將計算出(根據我們提供的信息)怎樣將源紋理的一個像素映射到繪製此像素的目標多邊形的一部分。
一旦 OpenGL ES 決定怎樣把一個像素從紋理映射到多邊形,它將使用指定的混色函數來確定最終繪製的各像素的最終值。 glBlendFunc()函數決定我們將怎樣進行混色運算,它採用了兩個參數。第一個參數定義了怎樣使用源紋理。第二個則定義了怎樣使用目標顏色或紋理。在本文簡單的例子中,我們希望繪製的紋理完全不透明而忽略多邊形中現存的顏色或紋理,所以我們設置源爲 GL_ONE,它表示源圖像(被映射的紋理)中各顏色通道的值將乘以1.0或者換句話說,以完全顏色密度使用。目標設置爲 GL_SRC_COLOR,它表示要使用源圖像中被映射到多邊形特定點的顏色。此混色函數的結果是一個完全不透明的紋理。這可能是最常用情況。我可能會在以後的文章中更詳細地介紹一下混色功能,但這可能是你使用最多的一種組合,而且是今天使用的唯一混色功能。
注意:如果你已經使用過OpenGL的混色功能,你應該知道 OpenGL ES 並不支持所有 OpenGL 支持的混色功能。下面是 OpenGL ES 支持的:GL_ZERO, GL_ONE, GL_SRC_COLOR,GL_ONE_MINUS_SRC_COLOR, GL_DST_COLOR, GL_ONE_MINUS_DST_COLOR, GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA, GL_DST_ALPHA, GL_ONE_MINUS_DST_ALPHA, 和 GL_SRC_ALPHA_SATURATE (它僅用於源)。
創建紋理
一旦你啓動了紋理和混色,就可以開始創建紋理了。通常紋理是在開始顯示3D物體給用戶前程序開始執行時或遊戲每關開始加載時創建的。這不是必須的,但卻是一個好的建議,因爲創建紋理需要佔用一些處理器時間,如果在你開始顯示一些複雜的幾何體時進行此項工作,會引起明顯的程序停頓。
OpenGL中的每一個圖像都是一個紋理,紋理是不能直接顯示給最終用戶的,除非它映射到物體上。但是有一個小小的例外,就是對允許你將圖像繪製於指定點的所謂點精靈(point sprites),但它有自己的一套規則,所以那是一個單獨的主題。通常的情況下,任何你希望顯示給用戶的圖像必須放置在由頂點定義的三角形中,有點像貼在上面的粘帖紙。
生成紋理名
爲創建一個紋理,首先必須通知OpenGL ES生成一個紋理名稱。這是一個令人迷惑的術語,因爲紋理名實際上是一個數字:更具體的說是一個GLuint。儘管“名稱”可以指任何字符串,但對於OpenGL ES紋理並不是這樣。它是一個代表指定紋理的整數值。每個紋理由一個獨一無二的名稱表示,所以傳遞紋理名給OpenGL是我們區別所使用紋理的方式。.
然而在生成紋理名之前,我們要定義一個保存單個或多個紋理名的GLuint數組:
GLuint texture[1];
儘管只有一個紋理,但使用一個元素的數組而不是一個GLuint仍是一個好習慣。當然,仍然可以定義單個GLuint進行強制調用。
在過程式程序中,紋理通常存於一個全局數組中,但在Objective-C程序中,使用例程變量保存紋理名更爲常見。下面是代碼:
glGenTextures(1, &texture[0]);
你可以調用glGenTextures()生成多個紋理;傳遞給OpenGL ES的第一個參數指示了要生成幾個紋理。第二個參數需要是一個具有足夠空間保存紋理名的數組。我們只有一個元素,所以只要求OpenGL ES產生一個紋理名。此調用後, texture[0] 將保持紋理的名稱,我們將在任何與紋理有關的地方都使用texture[0]來表示這個特定紋理。
紋理綁定
在爲紋理生成名稱後,在爲紋理提供圖像數據之前,我們必須綁定紋理。綁定使得指定紋理處於活動狀態。一次只能激活一個紋理。活動的或“被綁定”的紋理是繪製多邊形時使用的紋理,也是新紋理數據將加載其上紋理,所以在提供圖像數據前必須綁定紋理。這意味着每個紋理至少被綁定一次以爲OpenGL ES提供此紋理的數據。運行時,可能再次綁定紋理(但不會再次提供圖像數據)以指示繪圖時要使用此紋理。紋理綁定很簡單:
glBindTexture(GL_TEXTURE_2D, texture[0]);
因爲我們使二維圖像創建紋理,所以第一個參數永遠是 GL_TEXTURE_2D。常規OpenGL支持其他類型的紋理,但目前分佈在iPhone上的OpenGL ES版本只支持二維紋理,坦白地說,甚至在常規OpenGL中,二維紋理的使用也遠比其他類型要多得多。
第二個參數是我們需要綁定的紋理名。調用此函數後,先前生成了紋理名稱的紋理將成爲活動紋理。
圖像配置
在第一次綁定紋理後,我們需要設置兩個參數。需要的話,有一些參數可以設置,但在iPhone上,這兩個參數必須設定,否則紋理將不會正常顯示。
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
必須設置這兩個參數的原因是默認狀態下OpenGL設置了使用所謂mipmap。今天我將不討論mipmap,簡單地說,我們不準備使用它。Mipmap是一個圖像不同尺寸的組合,它允許OpenGL選擇最爲接近的尺寸版本以避免過多的插值計算並且在物體遠離觀察者時通過使用更小的紋理來更好地管理內存。感謝矢量單元和圖形芯片,iPhone在圖像插值方面做得很好,所以我們不需要考慮mipmap。我以後可能會專門撰寫一篇文章討論它,但我們今天討論的是怎樣讓OpenGL ES通過線性插值調整圖像到所需的尺寸。因爲GL_TEXTURE_MIN_FILTER 用於紋理需要被收縮到適合多邊形的尺寸的情形,而 GL_TEXTURE_MAG_FILTER 則用於紋理被放大到適合多邊形的尺寸的情況下,所以必須進行兩次調用。在兩種情況下,我們傳遞GL_LINEAR 以通知OpenGL以簡單的線性插值方法調整圖像。
加載圖像數據
在我們第一次綁定紋理後,必須爲OpenGL ES提供紋理的圖像數據。在iPhone上,有兩種基本方法加載圖像數據。如果你在其他書籍上看到使用標準 C I/O方法加載數據的代碼,那也是不錯的選擇,然而這兩種方法應該覆蓋了你將遇到的各種情形。
UIImage方法
如果你想使用JPEG, PNG或其他UIImage支持的格式,那麼你可以簡單地使用圖像數據實例化一個UIImage,然後產生圖像的RGBA 位圖數據:
NSString *path = [[NSBundle mainBundle] pathForResource:@"texture" ofType:@"png"];
NSData *texData = [[NSData alloc] initWithContentsOfFile:path];
UIImage *image = [[UIImage alloc] initWithData:texData];
if (image == nil)
NSLog(@"Do real error checking here");
GLuint width = CGImageGetWidth(image.CGImage);
GLuint height = CGImageGetHeight(image.CGImage);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
void *imageData = malloc( height * width * 4 );
CGContextRef context = CGBitmapContextCreate( imageData, width, height, 8, 4 * width,
colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big );
CGColorSpaceRelease( colorSpace );
CGContextClearRect( context, CGRectMake( 0, 0, width, height ) );
CGContextTranslateCTM( context, 0, height - height );
CGContextDrawImage( context, CGRectMake( 0, 0, width, height ), image.CGImage );
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA,
GL_UNSIGNED_BYTE, imageData);
CGContextRelease(context);
free(imageData);
[image release];
[texData release];
前面幾行代碼很容易理解 – 從程序包中加載一個叫做 texture.png 的圖像。然後使用一些 core graphics 調用將位圖以 RGBA 格式存放。此基本方法是讓我們使用任何UIImage支持的圖像數據然後轉換成OpenGL ES接受的數據格式。
注意:只是因爲 UIImage 不支持一種文件類型並不意味着你不能使用此方法。你仍然有可能通過使用Objective-C的分類來增加 UIImage 對額外的圖像文件類型的支持。
一旦具有了正確格式的位圖數據,我們就可以調用 glTexImage2D() 傳遞圖像數據給 OpenGL ES。完成後,我們釋放了一些內存,包括圖像數據和實際 UIImage 的實例。一旦你傳遞圖像數據給 OpenGL ES,它就會分配內存以擁有一份自己的數據拷貝,所以你可以釋放所有使用的與圖像有關的內存,而且你必須這樣做除非你的程序有更重要的與數據相關的任務。即使是來自壓縮過圖像的紋理也會佔用程序相當多的內存。每個像素佔用四個字節,所以忘記釋放紋理圖像數據的內存會導致內存很快被用盡。
PVRTC方法
iPhone的圖形芯片(PowerVR MBX)對一種稱爲 PVRTC 的壓縮技術提供硬件支持,Apple推薦在開發iPhone應用程序時使用 PVRTC 紋理。他們甚至提供了一篇很好的 技術筆記 描述了怎樣通過使用隨開發工具安裝的命令行程序將標準圖像文件轉換爲 PVRTC 紋理的方法。
你應該知道當使用 PVRTC 時與標準JPEG或PNG圖像相比有可能有些圖像質量的下降。是否值得在你的程序中做出一些犧牲取決於一些因素,但使用 PVRTC 紋理可以節省大量的內存空間。
儘管因爲沒有Objective-C類可以解析 PVRTC 數據獲取其寬和高1信息,你想要手工指定圖像的高和寬,但加載 PVRTC 數據到當前綁定的紋理實際上甚至比加載普通圖像文件更爲簡單。
下面的例子使用默認的texturetool設置加載一個 512×512 的PVRTC紋理:
NSString *path = [[NSBundle mainBundle] pathForResource:@"texture" ofType:@"pvrtc"];
NSData *texData = [[NSData alloc] initWithContentsOfFile:path];
// This assumes that source PVRTC image is 4 bits per pixel and RGB not RGBA
// If you use the default settings in texturetool, e.g.:
//
// texturetool -e PVRTC -o texture.pvrtc texture.png
//
// then this code should work fine for you.
glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG, 512, 512, 0,
[texData length], [texData bytes]);
就這麼簡單。使用glCompressedTexImage2D()從文件加載數據並傳送給OpeNGL ES。而隨後怎樣處理紋理則絕對沒有任何區別。
紋理限制
用於紋理的圖像寬和高必須爲乘方,比如 2, 4, 8, 16, 32, 64, 128, 256, 512, 或 1024。例如圖像可能爲 64×128 或 512×512。
當使用 PVRTC 壓縮圖像時,有一個額外的限制:源圖像必須是正方形,所以你的圖像應該爲 2×2, 4×4 8×8, 16×16, 32×32, 64×64, 128×128, 256×256, 等等。如果你的紋理本身不是正方形,那麼你只需爲圖像加上黑邊使圖像成爲正方形,然後映射紋理使得你需要的部分顯示在多邊形上。我們現在看看紋理是怎樣映射到多邊形的。
紋理座標
當紋理映射啓動後繪圖時,你必須爲OpenGL ES提供其他數據,即頂點數組中各頂點的 紋理座標。紋理座標定義了圖像的哪一部分將被映射到多邊形。它的工作方式有點奇怪。你有一個正方形或長方形的紋理,其左下角爲二維平面的原點,高和寬的單位爲一。像這樣:
這就是我們的“紋理座標系統”,不使用x 和 y 來代表二維空間,我們使用 s 和 t 作爲紋理座標軸,但原理上是一樣的。
除了 s 和 t 軸外,被映射的紋理在多邊形同樣有兩個軸,它們稱爲 u 和 v軸。這是源於許多3D圖像程序中的UV 映射 的術語。
好,我們明白了紋理座標系統,我們現在討論怎樣使用這些紋理座標。當我們指定頂點數組中的頂點時,我們需要在另一個數組中提供紋理座標,它稱爲紋理座標數組。 每個頂點,我們將傳遞兩個 GLfloats (s, t) 來指定頂點在上圖所示座標系統的位置。讓我們看看一個可能是最爲簡單的例子,將整個圖像映射到一個由三角形條組成的正方形上。首先,我們創建一個由四個頂點組成的頂點數組:
現在將兩個框圖疊在一起,所使用的座標數組的值變得很明顯:
將其轉化爲 GLfloat數組:
static const GLfloat texCoords[] = { 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0 };
爲使用紋理座標數組,我們必須啓動它(正如你預料的那樣)。使用 glEnableClientState():
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
爲傳遞紋理座標,調用 glTexCoordPointer():
glTexCoordPointer(2, GL_FLOAT, 0, texCoords);
就是這樣。我們彙總一下把代碼置於 drawView: 方法中。它假設紋理已經被綁定和加載了。
- (void)drawView:(GLView*)view;
{
static GLfloat rot = 0.0;
glColor4f(0.0, 0.0, 0.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
static const Vertex3D vertices[] = {
{-1.0, 1.0, -0.0},
{ 1.0, 1.0, -0.0},
{-1.0, -1.0, -0.0},
{ 1.0, -1.0, -0.0}
};
static const Vector3D normals[] = {
{0.0, 0.0, 1.0},
{0.0, 0.0, 1.0},
{0.0, 0.0, 1.0},
{0.0, 0.0, 1.0}
};
static const GLfloat texCoords[] = {
0.0, 1.0,
1.0, 1.0,
0.0, 0.0,
1.0, 0.0
};
glLoadIdentity();
glTranslatef(0.0, 0.0, -3.0);
glRotatef(rot, 1.0, 1.0, 1.0);
glBindTexture(GL_TEXTURE_2D, texture[0]);
glVertexPointer(3, GL_FLOAT, 0, vertices);
glNormalPointer(GL_FLOAT, 0, normals);
glTexCoordPointer(2, GL_FLOAT, 0, texCoords);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_NORMAL_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
static NSTimeInterval lastDrawTime;
if (lastDrawTime)
{
NSTimeInterval timeSinceLastDraw =
[NSDate timeIntervalSinceReferenceDate] - lastDrawTime;
rot+= 60 * timeSinceLastDraw;
}
lastDrawTime = [NSDate timeIntervalSinceReferenceDate];
}
下面是我使用的紋理:
運行時的結果:
請等下:那並不正確。如果你仔細對比一下紋理圖像和上面的截屏,你就會發現它們並不完全相同。截屏中圖像的y軸(或t軸)完全顛倒了。它上下顛倒了,並不是旋轉,而是翻轉了。
T-軸翻轉之謎
以OpenGL的角度來看,我們並未做錯任何事情,但結果卻是完全錯誤。原因在於iPhone的特殊性。 iPhone中用於Core Graphics的圖像座標系統並與OpenGL ES一致,其y軸在屏幕從上到下而增加。當然在OpenGL ES中正好相反,它的y軸從下向上增加。其結果就是我們早先傳遞給OpenGL ES中的圖像數據從OpenGL ES的角度看完全顛倒了。所以,當我們使用標準的OpenGL ST映射座標映射圖像時,我們得到了一個翻轉的圖像。
普通圖像的修正
當使用非PVRTC圖像時,你可以在傳遞數據到OpenGL ES之前就翻轉圖像的座標,將下面兩行代碼到紋理加載中創建OpenGL環境的語句之後:
CGContextTranslateCTM (context, 0, height);
CGContextScaleCTM (context, 1.0, -1.0);
這將翻轉繪製內容的座標系統,其產生的數據正是OpenGL ES所需要的。下面是結果:
PVRTC 圖像的修正
由於沒有UIKit類可以加載或處理PVRTC 圖像,所以沒有一個簡單的方法翻轉壓縮紋理的座標系統。當然,我們還是有些方法處理這個問題。
一種方法是使用諸如 Acorn 或 Photoshop之類的程序中將圖像轉換爲壓縮紋理前簡單地進行垂直翻轉。這看似小詭計的方法在很多情況下是最好的解決方法,因爲所有的處理都是事前進行的,所以運行時不需要額外的處理時間而且還允許壓縮和未壓縮圖像具有同樣的紋理座標數組。
另一種方法是將t軸的值減一。儘管減法是很快的,但其佔用的時間還是會累積,所以在大部分情況下,儘量要避免繪圖時進行的轉換工作。不論是翻轉圖像或翻轉紋理座標,都要在顯示前進行加載時進行。
更多的映射方式
上個例子中這個圖像都被映射到繪製的正方形上。那是因爲設定的紋理座標所決定的。我們可以改變座標數組僅使用源圖像的中心部分。讓我們看看僅使用了圖像中心部分的另一個框圖:
其座標數組爲:
static const GLfloat texCoords[] = {
0.25, 0.75,
0.75, 0.75,
0.25, 0.25,
0.75, 0.25
};
運行使用了新映射到程序,屏幕上只顯示了圖像的中心部分:
類似地,如果我們只希望顯示紋理的左下部:
座標數組爲:
static const GLfloat texCoords[] = {
0.0, 0.5,
0.5, 0.5,
0.0, 0.0,
0.5, 0.0
};
顯示結果:
等一下,還有更多的方式
實際上,並不是真正還有更多的映射方式,只是說此功能在正方形到正方形的映射時並不很明顯。同樣的步驟適合於幾何體中任何三角形,而且你甚至可以通過非常規方式的映射來扭曲紋理。例如,我們可以定義一個等腰三角形:
但將底部頂點映射到紋理的左下角:
這樣的映射並不會改變幾何體 – 它仍然是等腰三角形而不是直角三角形,但OpenGL ES將扭曲紋理使得第二個圖中的三角形部分以等腰三角形的形式顯示出來。代碼如下:
- (void)drawView:(GLView*)view;
{
glColor4f(0.0, 0.0, 0.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
static const Vertex3D vertices[] = {
{-1.0, 1.0, -0.0},
{ 1.0, 1.0, -0.0},
{ 0.0, -1.0, -0.0},
};
static const Vector3D normals[] = {
{0.0, 0.0, 1.0},
{0.0, 0.0, 1.0},
{0.0, 0.0, 1.0},
};
static const GLfloat texCoords[] = {
0.0, 1.0,
1.0, 0.0,
0.0, 0.0,
};
glLoadIdentity();
glTranslatef(0.0, 0.0, -3.0);
glBindTexture(GL_TEXTURE_2D, texture[0]);
glVertexPointer(3, GL_FLOAT, 0, vertices);
glNormalPointer(GL_FLOAT, 0, normals);
glTexCoordPointer(2, GL_FLOAT, 0, texCoords);
glDrawArrays(GL_TRIANGLES, 0, 3);
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_NORMAL_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
}
運行時結果如下:
注意到紋理正方形左下角的弧形花紋現在處於三角形的底部了嗎?總而言之,紋理上的任何一點都可以映射到多邊形的任何一點。或者換而言之,你可以對任何地點(u,v)使用任何(s,t)而OpenGL ES則爲你進行映射。
平鋪和箝位
我們的紋理座標系統在兩個軸上都是從0.0 到 1.0,如果設置超出此範圍的值會怎麼樣?根據視圖的設置方式有兩種選擇。
平鋪(也叫重複)
一種選擇是平鋪紋理。按OpenGL的術語,也叫“重複”。如果我們將第一個紋理座標數組的所有1.0改爲2.0:
static const GLfloat texCoords[] = {
0.0, 2.0,
2.0, 2.0,
0.0, 0.0,
2.0, 0.0
};
那麼我們得到以下結果:
如果這就是你希望的結果,那麼你應該在setupView:方法中通過glTexParameteri()函數啓動它,像這樣:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
箝位
另一種可能的選擇是讓OpenGL ES簡單地將超過1.0的值限制爲1.0,任何低於0.0的值限制爲 0.0。這實際會引起邊沿像素重複,從而產生奇怪的效果。下圖是使用了箝位的效果:
如果這是你希望的效果,那麼你應該在setupView:方法中使用下面兩行代碼:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
注意 s 和 t 軸是分別設置的,這樣有可能在一個方向上使用平鋪而在另一個方向使用箝位。
結論
本文介紹了OpenGL ES映射紋理到多邊形的基本機制。儘管它很簡單,但需要動下腦筋並自己動手才能真正理解它到底是怎樣工作的,請下載texture_projects 自己測試。
下次我們將介紹矩陣,希望你到時回來。
註腳
實際上有一個先行版的示例代碼介紹了怎樣從文件中讀取PVRTC頭信息以決定圖像的寬和高以及有關壓縮圖像文件的詳細信息。我沒有使用它是因爲 a) 它還沒有正式發佈,如果我使用有可能違背了NDA, b) 此代碼並不能讀取所有的PVRTC文件(包括本文使用的圖像)
感謝 George Sealy 和 Daniel Pasco關於t軸問題的幫助。感謝Apple Dev論壇的”Colombo”。