當(dāng)開發(fā)基于軟件模式的游戲時(shí),通過(guò)縮放視頻緩沖區(qū)來(lái)適應(yīng)顯示尺寸是最棘手的問(wèn)題之一。當(dāng)面對(duì)眾多不同的分辨率時(shí)(比如開放環(huán)境下的Android),該問(wèn)題會(huì)變得更加麻煩,作為開發(fā)人員,我們必須嘗試在性能與顯示質(zhì)量之間找到最佳平衡點(diǎn)。正如我們?cè)诘?章中看到的,縮放視頻緩沖區(qū)從最慢到最快共有3種類型。
軟件模擬:3中類型中最慢,但最容易實(shí)現(xiàn),是沒有GPU的老款設(shè)備上的最佳選擇。但是現(xiàn)在大部分智能手機(jī)都支持硬件加速。
混合模式:這種方式混合使用軟件模擬(創(chuàng)建圖像緩沖區(qū))和硬件渲染(向顯示屏繪制)兩種模式。這種方法速度很快,而且可以在分辨率大于256×256的任意屏幕上渲染圖像。
硬件加速模式:3種類型中最快,但最難實(shí)現(xiàn)。這取決于游戲的復(fù)雜程度,需要更加強(qiáng)勁的GPU。如果有好的硬件,這種方法就可以創(chuàng)造出令人震撼的質(zhì)量和效果。但在終端設(shè)備比較分裂的平臺(tái)上,比如Android,這將是十分艱難的選擇。
這里,我們選擇第二種方式,也是在終端設(shè)備分裂的平臺(tái)上的最佳選擇。你擁有軟件渲染器,并希望將游戲適配到任意分辨率的顯示屏上。此方法非常適合模擬器游戲、街機(jī)游戲、簡(jiǎn)單的射擊游戲等。它在各種低端、中端、高端設(shè)備上都表現(xiàn)很好。
下面我們開始介紹混合模式并探討為什么這種方法更加可行。然后,將深入研究這種方法的實(shí)現(xiàn),包括如何初始化surface并通過(guò)實(shí)際縮放來(lái)繪制到紋理。
1. 為什么使用混合縮放
這種縮放技術(shù)背后的原理很簡(jiǎn)單:
你的游戲根據(jù)給定的尺寸創(chuàng)建圖像緩沖區(qū)(通常采用像素格式RGB565,即移動(dòng)設(shè)備最常用的格式)。例如320×240,這是典型的模擬器尺寸。
當(dāng)一張分辨率為320×240的圖像需要被縮放至平板電腦的尺寸(1024×768)或其他任意相同屏幕的設(shè)備時(shí),我們可以使用軟件模擬的方式來(lái)完成縮放,但會(huì)慢的令人無(wú)法忍受。而采用混合模式進(jìn)行縮放,需要?jiǎng)?chuàng)建OpenGL ES紋理并將圖片(320×240)渲染到GL四邊形上。
紋理會(huì)通過(guò)硬件被縮放到適合顯示屏的尺寸(1024×768),從而你的游戲性能將得到顯著提升。
從實(shí)現(xiàn)的角度看,這個(gè)過(guò)程可描述如下:
初始化OpenGL ES紋理:在游戲視頻被初始化的階段,必須創(chuàng)建硬件surface。其中包含簡(jiǎn)單的紋理,要顯示的視頻圖像會(huì)被渲染至到該紋理(詳見代碼清單1與代碼清單2)。
將圖像緩沖區(qū)繪制到紋理:在游戲循環(huán)的末端,渲染要顯示的視頻圖像到紋理,該紋理會(huì)自動(dòng)縮放至適合顯示屏的尺寸(詳見代碼清單3)。
代碼清單1 創(chuàng)建RGB656格式的空紋理
<SPAN style="FONT-SIZE: 14px">// 紋理ID
static unsigned int mTextureID;
// 被用來(lái)計(jì)算圖片繪制在紋理上的X、Y偏移量
static int xoffset;
static int yoffset;
/**
* 創(chuàng)建RGB565格式的空紋理
* 參數(shù): (w,h) 紋理的寬, 高
* (x_offsety_offset): 圖片繪制在紋理上的X、Y偏移量
*/
static void CreateEmptyTextureRGB565 (int w, int h, int x_offset, int y_offset)
{
int size = w * h * 2;
xoffset = x_offset;
yoffset = y_offset;
// 緩沖區(qū)
unsigned short * pixels = (unsigned short *)malloc(size);
memset(pixels, 0, size);
// 初始化GL狀態(tài)
glDisable(GL_DITHER);
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST);
glClearColor(.5f, .5f, .5f, 1);
glShadeModel(GL_SMOOTH);
glEnable(GL_DEPTH_TEST);
glEnable(GL_TEXTURE_2D);
// 創(chuàng)建紋理
glGenTextures(1, &mTextureID);
glBindTexture(GL_TEXTURE_2D, mTextureID);
// 紋理參數(shù)
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// RGB565格式的紋理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_
SHORT_5_6_5 , pixels);
free (pixels);
} </SPAN>
代碼清單2展示了CreateEmptyTextureRGB565的實(shí)現(xiàn)過(guò)程,創(chuàng)建RGB656格式的空紋理用于繪制,參數(shù)如下:
w和h:要顯示的視頻圖片的尺寸。
x_offset和y_offset:坐標(biāo)系中X軸、Y軸的偏移量,視頻圖片將會(huì)按照這個(gè)坐標(biāo)被渲染到紋理。但是為什么我們需要這些參數(shù)?請(qǐng)繼續(xù)閱讀。
在OpenGL中創(chuàng)建紋理,我們只需要調(diào)用:
<SPAN style="FONT-SIZE: 14px">glGenTextures(1, &mTextureID);
glBindTexture(GL_TEXTURE_2D, mTextureID);</SPAN>
這里的mTextureID是整型變量,用于存儲(chǔ)紋理的ID。然后需要設(shè)置下面這些紋理參數(shù):
GL_TEXTURE_MIN_FILTER:指定紋理縮小的方式,當(dāng)像素被紋理化后并映射到某個(gè)大于單個(gè)紋理元素的區(qū)域時(shí)使用的縮小方式為GL_NEAREST,返回距離像素被紋理化后的中心最近(曼哈頓距離)的紋理元素的值。
GL_TEXTURE_MAG_FILTER:指定紋理放大的方式,當(dāng)像素被紋理化后并映射到某個(gè)小于或等于單個(gè)紋理元素的區(qū)域時(shí)使用的放大方式為GL_LINEAR,返回4個(gè)距離像素被紋理化后的中心最近的紋理元素的加權(quán)平均值。
GL_TEXTURE_WRAP_S:用于設(shè)置紋理坐標(biāo)系中S軸方向上的紋理映射方式為GL_CLAMP,將紋理坐標(biāo)限制在(0,1)范圍內(nèi),當(dāng)映射單張圖像到對(duì)象時(shí),可以有效防止畫面重疊。
GL_TEXTURE_WRAP_T:用于設(shè)置紋理坐標(biāo)系中T軸方向上的紋理映射的方式為GL_CLAMP。
最后,我們通過(guò)glTexImage2D函數(shù)及以下參數(shù)來(lái)指定二維紋理:
GL_TEXTURE_2D:指定目標(biāo)紋理的類型為二維紋理。
Level:指定圖像紋理的詳細(xì)程度。0是最基本的圖像紋理層。
Internal format:指定紋理的顏色成分,在這個(gè)例子中是RGB格式。
Width and height:紋理的尺寸,必須是2的冪。
Format:指定像素?cái)?shù)據(jù)的格式,同時(shí)也必須與內(nèi)部格式相同。
Type:指定像素?cái)?shù)據(jù)的數(shù)據(jù)類型,在本例中使用RGB565(16位)格式。
Pixels:指向內(nèi)存中圖像數(shù)據(jù)的指針,必須使用RGR656編碼。
注意:紋理的尺寸必須是2的冪,如256、512、1024等。但是,要顯示的視頻圖像的尺寸可以是任意尺寸。這就意味著,紋理的尺寸必須是大于或等于要顯示的視頻圖像尺寸的2的冪。稍后我們將進(jìn)行詳細(xì)介紹。
現(xiàn)在,讓我們來(lái)看一看混合視頻縮放的實(shí)際實(shí)現(xiàn),接下來(lái)的兩個(gè)小節(jié)將介紹如何初始化用來(lái)縮放的surface以及如何實(shí)現(xiàn)實(shí)際的繪制。
2. 初始化surface
要進(jìn)行縮放,就必須保證紋理的尺寸大于或等于要顯示的視頻圖像的尺寸。否則,當(dāng)圖像渲染的時(shí)候,會(huì)看到白色或黑色的屏幕。在代碼清單2中,JNI_RGB565_SurfaceInit函數(shù)將確保產(chǎn)生有效的紋理尺寸。使用圖像的寬度和高度為參數(shù),然后調(diào)用getBestTexSize函數(shù)來(lái)獲取最接近要求的紋理尺寸,最后通過(guò)調(diào)用CreateEmptyTextureRGB565函數(shù)來(lái)創(chuàng)建空的紋理。注意,如果圖像尺寸小于紋理尺寸,就通過(guò)計(jì)算X、Y坐標(biāo)的偏移量來(lái)將其置于屏幕的中心。
代碼清單2 初始化surface
<SPAN style="FONT-SIZE: 14px">// 獲取下一個(gè)POT紋理尺寸,該尺寸大于或等于圖像尺寸(WH)
static void getBestTexSize(int w, int h, int *tw, int *th)
{
int width = 256, height = 256;
#define MAX_WIDTH 1024
#define MAX_HEIGHT 1024
while ( width < w && width < MAX_WIDTH) { width *= 2; }
while ( height < h && height < MAX_HEIGHT) { height *= 2; }
*tw = width;
*th = height;
}
/**
* 初始化RGB565 surface
* 參數(shù): (w,h) 圖像的寬高
*/
void JNI_RGB565_SurfaceInit(int w, int h)
{
//最小紋理的寬高
int texw = 256;
int texh = 256;
// 得到紋理尺寸 (必須是POT) >= WxH
getBestTexSize(w, h, &texw, &texh);
// 圖片在屏幕中心?
int offx = texw > w ? (texw - w)/2 : 0;
int offy = texh > h ? (texh - h)/2 : 0;
if ( w > texw || h > texh)
printf ("Error: Invalid surface size %sx%d", w, h);
// 創(chuàng)建OpenGL紋理,用于渲染
CreateEmptyTextureRGB565 (texw, texh, offx, offy);
}
</SPAN>
3. 繪制到紋理
最后,為了將圖像顯示到屏幕上(也稱作surface翻轉(zhuǎn)),我們調(diào)用JNI_RGB565_Flip函數(shù),其參數(shù)是像素?cái)?shù)組(使用RGR656編碼)和要顯示的圖像尺寸。JNI_RGB565_Flip函數(shù)通過(guò)調(diào)用DrawIntoTextureRGB565將圖像繪制到紋理并交換緩沖區(qū)。注意交換緩沖區(qū)的函數(shù)是用Java編碼的,而不是用C語(yǔ)言編碼的,因此我們需要一個(gè)方法來(lái)調(diào)用Java的交換函數(shù)。我們可以通過(guò)使用JNI方法調(diào)用某個(gè)Java方法來(lái)完成緩沖區(qū)的交換工作(見代碼清單3)。
代碼清單3 用四邊形將圖像緩沖區(qū)繪制到紋理
<SPAN style="FONT-SIZE: 14px">// 四邊形頂點(diǎn)的X、Y和Z坐標(biāo)
static const float vertices[] = {
-1.0f, -1.0f, 0,
1.0f, -1.0f, 0,
1.0f, 1.0f, 0,
-1.0f, 1.0f, 0
};
// 四邊形坐標(biāo)(0-1)
static const float coords[] = {
0.0f, 1.0f,
1.0f, 1.0f,
1.0f, 0.0f,
0.0f, 0.0f,
};
// 四邊形頂點(diǎn)索引
static const unsigned short indices[] = { 0, 1, 2, 3};
/**
* 使用四邊形像素(RGB565的unsigned short)將像素?cái)?shù)組繪制到全部屏幕
*
*/
static void DrawIntoTextureRGB565 (unsigned short * pixels, int w, int h)
{
// 清除屏幕
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 啟用頂點(diǎn)和紋理坐標(biāo)
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, mTextureID);
glTexSubImage2D(GL_TEXTURE_2D, 0, xoffset, yoffset, w, h, GL_RGB,
GL_UNSIGNED_SHORT_5_6_5 , pixels);
// 繪制四邊形
glFrontFace(GL_CCW);
glVertexPointer(3, GL_FLOAT, 0, vertices);
glEnable(GL_TEXTURE_2D);
glTexCoordPointer(2, GL_FLOAT, 0, coords);
glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_SHORT, indices);
}
// 翻轉(zhuǎn)surface (繪制到紋理中)
void JNI_RGB565_Flip(unsigned short *pixels , int width, int height)
{
if ( ! pixels) {
return;
}
DrawIntoTextureRGB565 (pixels, width, height);
// 在這里必須交換GLES緩沖區(qū)
jni_swap_buffers ();
}
</SPAN>
使用OpenGL渲染到紋理:
(1) 使用glClear(GL_COLOR_BUFFER_BIT |GL_DEPTH_BUFFER_BIT)清除顏色與深度緩沖區(qū)。
(2) 啟用客戶端狀態(tài):當(dāng)glDrawElements函數(shù)被調(diào)用時(shí),寫入頂點(diǎn)數(shù)組與紋理坐標(biāo)數(shù)組。
(3) 通過(guò)glActiveTexture函數(shù)選擇要激活的紋理單元,初始值是GL_TEXTURE0。
(4) 將已經(jīng)生成的紋理綁定到等待被紋理化的目標(biāo)。GL_TEXTURE_2D (一個(gè)二維紋理)是默認(rèn)的紋理綁定目標(biāo),mTextureID是紋理的ID。
(5) 通過(guò)glTexSubImage2D函數(shù)來(lái)指定二維紋理子圖,參數(shù)如下:
GL_TEXTURE_2D:指定目標(biāo)紋理類型。
level:指定圖像的詳細(xì)程度(即層數(shù))。0是基本的圖像紋理層。
Xoffset:指定紋理像素在X軸方向上、紋理數(shù)組內(nèi)的偏移量。
Yoffset:指定紋理像素在Y軸方向上、紋理數(shù)組內(nèi)的偏移量。
width:指定紋理子圖的寬度。
height:指定紋理子圖的高度
format:指定像素?cái)?shù)據(jù)的格式。
Type:指定像素?cái)?shù)據(jù)的數(shù)據(jù)類型。
data:指定指向內(nèi)存中圖像數(shù)據(jù)的指針。
(6) 通過(guò)調(diào)用以下函數(shù)繪制四邊形頂點(diǎn)、坐標(biāo)與索引:
glFrontFace:?jiǎn)⒂盟倪呅蔚恼妗?
glVertexPointer:定義四邊形的頂點(diǎn)數(shù)據(jù)數(shù)組,頂點(diǎn)數(shù)據(jù)大小為3,數(shù)據(jù)類型是GL_FLOAT,數(shù)組中每個(gè)頂點(diǎn)間的間隔(步長(zhǎng))為0。
glTexCoordPointer:定義四邊形的紋理數(shù)組,紋理坐標(biāo)大小為2,數(shù)據(jù)類型是GL_FLOAT,間隔為0。
glDrawElements:通過(guò)數(shù)據(jù)數(shù)組以三角形扇(GL_TRIANGLE_FAN)的方式渲染多邊形,有4個(gè)頂點(diǎn),類型為短整型(GL_UNSIGNED_SHORT),外加指向索引的指針。
注意,從代碼清單3中我們可以看到四邊形的兩個(gè)軸坐標(biāo)都在[−1,1]區(qū)間內(nèi)。這是因?yàn)镺penGL的坐標(biāo)系統(tǒng)在(−1,1)之間,原點(diǎn)(0,0)在窗口中心(如圖3-10所示)。
在理想的世界里,我們不應(yīng)該過(guò)多地?fù)?dān)心視頻緩沖區(qū)的尺寸(尤其是使用軟件模擬僅有的定標(biāo)器/渲染器)。當(dāng)在Android中使用OpenGL縮放視頻時(shí),這卻是事實(shí)。在這個(gè)示例中,緩沖區(qū)的尺寸至關(guān)重要。接下來(lái)你將學(xué)習(xí)如何處理任意尺寸的視頻,這一點(diǎn)在OpenGL中工作得不是很好。
4. 當(dāng)圖像的尺寸不是2的冪時(shí)會(huì)發(fā)生什么
如前所述,當(dāng)圖像的尺寸是2的冪時(shí)混合縮放會(huì)非常完美。但是,也有可能圖像緩沖區(qū)不是2的冪。例如,在處理Demo引擎的章節(jié)中有一段320×240尺寸的視頻。在這種情況下,圖像仍然被縮放,但是會(huì)縮放到紋理尺寸的百分比大小。在圖2和3中可以看到這個(gè)效果。
在圖2中,有以下尺寸:
設(shè)備顯示器:859×480
紋理:512×256
圖像:320×240
正如我們看到的一樣,圖像被縮放到紋理寬度的62%(320/512*100)和高度的93%
(240/256*100)。因此,在任何分辨率大于256的設(shè)備上,圖像都會(huì)被縮放到設(shè)備提供分辨率的62%×93%?,F(xiàn)在我們來(lái)看看圖3。
圖3 縮放尺寸為2的冪的圖像
在圖3中,有以下尺寸:
設(shè)備顯示器:859×480
紋理:512×256
圖像:512×256
縮放和繪制
在圖3中,我們看見圖像被縮放到設(shè)備提供分辨率的100%,這正是我們想要的。但是如果圖像不是2的冪,那么我們要如何做呢?為了解決這個(gè)問(wèn)題,我們應(yīng)該:
(1) 用軟件縮放器將320×240尺寸的圖像縮放到接近2的冪(這里是512×256)。
(2) 將已縮放的surface轉(zhuǎn)換成RGB656格式的圖像,以兼容前面介紹的DrawInto-TextureRGB565。
(3) 繪制到紋理,從而使用硬件將其縮放到顯示屏的分辨率。
這種解決辦法可能比前面介紹的方法慢,但仍然比純軟件縮放快,尤其是運(yùn)行在高分辨率設(shè)備時(shí)更明顯(如平板電腦)。
代碼清單4展示了如何使用流行的SDL_gfx庫(kù)來(lái)縮放SDL surface。
代碼清單4 用SDL_gfx庫(kù)縮放圖像
<SPAN style="FONT-SIZE: 14px">void JNI_Flip(SDL_Surface *surface )
{
if ( zoom ) {
// 如果surface是8位縮放,就是8位,否則surface就是32的RGBA!
SDL_Surface * sized = zoomSurface( surface, zoomx, zoomy, SMOOTHING_OFF);
JNI_FlipByBPP (sized);
// 必須清理掉!
SDL_FreeSurface(sized);
}
else {
JNI_FlipByBPP (surface);
}
}</SPAN>
縮放和繪制實(shí)現(xiàn)
要放大/縮小SDL surface,需要簡(jiǎn)單地調(diào)用SDL_gfx庫(kù)的zoomSurface:
(1) 一個(gè)SDL surface。
(2) 水平縮放因子:(0-1)
(3) 垂直縮放因子:(0-1)
(4) SMOOTHING_OFF:為了能快速繪制,禁用反鋸齒處理。
接下來(lái),讓我們基于分辨率(每個(gè)像素的位數(shù))來(lái)翻轉(zhuǎn)SDL surface。代碼清單5展示了如何完成8位RBG格式的surface。
代碼清單5 根據(jù)分辨率翻轉(zhuǎn)SDL surface
<SPAN style="FONT-SIZE: 14px">/**
* 通過(guò)每個(gè)像素的位數(shù)翻轉(zhuǎn)SDL surface
*/
static void JNI_FlipByBPP (SDL_Surface *surface)
{
int bpp = surface->format->BitsPerPixel;
switch ( bpp ) {
case 8:
JNI_Flip8Bit (surface);
break;
case 16:
// 替換16位RGB (surface);
break;
case 32:
// 替換32為RGB (surface);
break;
default:
printf("Invalid depth %d for surface of size %dx%d", bpp, surface->w,
surface->h);
}
}
/**
* 替換8位SDL surface
*/
static void JNI_Flip8Bit(SDL_Surface *surface )
{
int i;
int size = surface->w * surface->h;
int bpp = surface->format->BitsPerPixel;
unsigned short pixels [size]; // RGB565
SDL_Color * colors = surface->format->palette->colors;
for ( i = 0 ; i < size ; i++ ) {
unsigned char pixel = ((unsigned char *)surface->pixels)[i];
pixels[i] = ( (colors[pixel].r >> 3) << 11)
| ( (colors[pixel].g >> 2) << 5)
| (colors[pixel].b >> 3); // RGB565
}
DrawIntoTextureRGB565 (pixels, surface->w, surface->h);
jni_swap_buffers ();
}
</SPAN>
指定SDL surface,然后檢查每個(gè)像素的格式:surface->format->BitsPerPixel,并根據(jù)該值創(chuàng)建能夠被DrawIntoTextureRGB565使用的RGB565像素?cái)?shù)組:
<SPAN style="FONT-SIZE: 14px">for ( i = 0 ; i < size ; i++ ) {
unsigned char pixel = ((unsigned char *)surface->pixels)[i];
// RGB565
pixels[i] = ( (colors[pixel].r >> 3) << 11)
| ( (colors[pixel].g >> 2) << 5)
| (colors[pixel].b >> 3);
}</SPAN>
從surface調(diào)色板上提取每個(gè)像素包含的紅、綠和藍(lán)值:
<SPAN style="FONT-SIZE: 14px">SDL_Color * colors = surface->format->palette->colors;
RED: colors[pixel].r
GREEN: colors[pixel].g
BLUE: colors[pixel].b</SPAN>
為了構(gòu)建RGB565像素,需要從每個(gè)顏色組件中拋棄最低有效位: