Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發實例 >> 通過OpenGL ES混合模式縮放視頻緩沖區來適應顯示尺寸

通過OpenGL ES混合模式縮放視頻緩沖區來適應顯示尺寸

編輯:Android開發實例

當開發基於軟件模式的游戲時,通過縮放視頻緩沖區來適應顯示尺寸是最棘手的問題之一。當面對眾多不同的分辨率時(比如開放環境下的Android),該問題會變得更加麻煩,作為開發人員,我們必須嘗試在性能與顯示質量之間找到最佳平衡點。正如我們在第2章中看到的,縮放視頻緩沖區從最慢到最快共有3種類型。

軟件模擬:3中類型中最慢,但最容易實現,是沒有GPU的老款設備上的最佳選擇。但是現在大部分智能手機都支持硬件加速。
混合模式:這種方式混合使用軟件模擬(創建圖像緩沖區)和硬件渲染(向顯示屏繪制)兩種模式。這種方法速度很快,而且可以在分辨率大於256×256的任意屏幕上渲染圖像。
硬件加速模式:3種類型中最快,但最難實現。這取決於游戲的復雜程度,需要更加強勁的GPU。如果有好的硬件,這種方法就可以創造出令人震撼的質量和效果。但在終端設備比較分裂的平台上,比如Android,這將是十分艱難的選擇。

這裡,我們選擇第二種方式,也是在終端設備分裂的平台上的最佳選擇。你擁有軟件渲染器,並希望將游戲適配到任意分辨率的顯示屏上。此方法非常適合模擬器游戲、街機游戲、簡單的射擊游戲等。它在各種低端、中端、高端設備上都表現很好。

下面我們開始介紹混合模式並探討為什麼這種方法更加可行。然後,將深入研究這種方法的實現,包括如何初始化surface並通過實際縮放來繪制到紋理。
1. 為什麼使用混合縮放
這種縮放技術背後的原理很簡單:
你的游戲根據給定的尺寸創建圖像緩沖區(通常采用像素格式RGB565,即移動設備最常用的格式)。例如320×240,這是典型的模擬器尺寸。
當一張分辨率為320×240的圖像需要被縮放至平板電腦的尺寸(1024×768)或其他任意相同屏幕的設備時,我們可以使用軟件模擬的方式來完成縮放,但會慢的令人無法忍受。而采用混合模式進行縮放,需要創建OpenGL ES紋理並將圖片(320×240)渲染到GL四邊形上。
紋理會通過硬件被縮放到適合顯示屏的尺寸(1024×768),從而你的游戲性能將得到顯著提升。
從實現的角度看,這個過程可描述如下:
初始化OpenGL ES紋理:在游戲視頻被初始化的階段,必須創建硬件surface。其中包含簡單的紋理,要顯示的視頻圖像會被渲染至到該紋理(詳見代碼清單1與代碼清單2)。
將圖像緩沖區繪制到紋理:在游戲循環的末端,渲染要顯示的視頻圖像到紋理,該紋理會自動縮放至適合顯示屏的尺寸(詳見代碼清單3)。
代碼清單1 創建RGB656格式的空紋理
代碼如下:

<SPAN >// 紋理ID
static unsigned int mTextureID;
// 被用來計算圖片繪制在紋理上的X、Y偏移量
static int xoffset;
static int yoffset;
/**
* 創建RGB565格式的空紋理
* 參數: (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;
// 緩沖區
unsigned short * pixels = (unsigned short *)malloc(size);
memset(pixels, 0, size);
// 初始化GL狀態
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);
// 創建紋理
glGenTextures(1, &mTextureID);
glBindTexture(GL_TEXTURE_2D, mTextureID);
// 紋理參數
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的實現過程,創建RGB656格式的空紋理用於繪制,參數如下:
w和h:要顯示的視頻圖片的尺寸。
x_offset和y_offset:坐標系中X軸、Y軸的偏移量,視頻圖片將會按照這個坐標被渲染到紋理。但是為什麼我們需要這些參數?請繼續閱讀。
在OpenGL中創建紋理,我們只需要調用:
代碼如下:

<SPAN >glGenTextures(1, &mTextureID);
glBindTexture(GL_TEXTURE_2D, mTextureID);</SPAN>

這裡的mTextureID是整型變量,用於存儲紋理的ID。然後需要設置下面這些紋理參數:
GL_TEXTURE_MIN_FILTER:指定紋理縮小的方式,當像素被紋理化後並映射到某個大於單個紋理元素的區域時使用的縮小方式為GL_NEAREST,返回距離像素被紋理化後的中心最近(曼哈頓距離)的紋理元素的值。
GL_TEXTURE_MAG_FILTER:指定紋理放大的方式,當像素被紋理化後並映射到某個小於或等於單個紋理元素的區域時使用的放大方式為GL_LINEAR,返回4個距離像素被紋理化後的中心最近的紋理元素的加權平均值。
GL_TEXTURE_WRAP_S:用於設置紋理坐標系中S軸方向上的紋理映射方式為GL_CLAMP,將紋理坐標限制在(0,1)范圍內,當映射單張圖像到對象時,可以有效防止畫面重疊。
GL_TEXTURE_WRAP_T:用於設置紋理坐標系中T軸方向上的紋理映射的方式為GL_CLAMP。
最後,我們通過glTexImage2D函數及以下參數來指定二維紋理:
GL_TEXTURE_2D:指定目標紋理的類型為二維紋理。
Level:指定圖像紋理的詳細程度。0是最基本的圖像紋理層。
Internal format:指定紋理的顏色成分,在這個例子中是RGB格式。
Width and height:紋理的尺寸,必須是2的冪。
Format:指定像素數據的格式,同時也必須與內部格式相同。
Type:指定像素數據的數據類型,在本例中使用RGB565(16位)格式。
Pixels:指向內存中圖像數據的指針,必須使用RGR656編碼。
注意:紋理的尺寸必須是2的冪,如256、512、1024等。但是,要顯示的視頻圖像的尺寸可以是任意尺寸。這就意味著,紋理的尺寸必須是大於或等於要顯示的視頻圖像尺寸的2的冪。稍後我們將進行詳細介紹。
現在,讓我們來看一看混合視頻縮放的實際實現,接下來的兩個小節將介紹如何初始化用來縮放的surface以及如何實現實際的繪制。
2. 初始化surface
要進行縮放,就必須保證紋理的尺寸大於或等於要顯示的視頻圖像的尺寸。否則,當圖像渲染的時候,會看到白色或黑色的屏幕。在代碼清單2中,JNI_RGB565_SurfaceInit函數將確保產生有效的紋理尺寸。使用圖像的寬度和高度為參數,然後調用getBestTexSize函數來獲取最接近要求的紋理尺寸,最後通過調用CreateEmptyTextureRGB565函數來創建空的紋理。注意,如果圖像尺寸小於紋理尺寸,就通過計算X、Y坐標的偏移量來將其置於屏幕的中心。
代碼清單2 初始化surface
代碼如下:

<SPAN >// 獲取下一個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
* 參數: (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);
// 創建OpenGL紋理,用於渲染
CreateEmptyTextureRGB565 (texw, texh, offx, offy);
}
</SPAN>

3. 繪制到紋理
最後,為了將圖像顯示到屏幕上(也稱作surface翻轉),我們調用JNI_RGB565_Flip函數,其參數是像素數組(使用RGR656編碼)和要顯示的圖像尺寸。JNI_RGB565_Flip函數通過調用DrawIntoTextureRGB565將圖像繪制到紋理並交換緩沖區。注意交換緩沖區的函數是用Java編碼的,而不是用C語言編碼的,因此我們需要一個方法來調用Java的交換函數。我們可以通過使用JNI方法調用某個Java方法來完成緩沖區的交換工作(見代碼清單3)。
代碼清單3 用四邊形將圖像緩沖區繪制到紋理
代碼如下:

<SPAN >// 四邊形頂點的X、Y和Z坐標
static const float vertices[] = {
-1.0f, -1.0f, 0,
1.0f, -1.0f, 0,
1.0f, 1.0f, 0,
-1.0f, 1.0f, 0
};
// 四邊形坐標(0-1)
static const float coords[] = {
0.0f, 1.0f,
1.0f, 1.0f,
1.0f, 0.0f,
0.0f, 0.0f,
};
// 四邊形頂點索引
static const unsigned short indices[] = { 0, 1, 2, 3};
/**
* 使用四邊形像素(RGB565的unsigned short)將像素數組繪制到全部屏幕
*
*/
static void DrawIntoTextureRGB565 (unsigned short * pixels, int w, int h)
{
// 清除屏幕
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 啟用頂點和紋理坐標
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);
}
// 翻轉surface (繪制到紋理中)
void JNI_RGB565_Flip(unsigned short *pixels , int width, int height)
{
if ( ! pixels) {
return;
}
DrawIntoTextureRGB565 (pixels, width, height);
// 在這裡必須交換GLES緩沖區
jni_swap_buffers ();
}
</SPAN>

使用OpenGL渲染到紋理
(1) 使用glClear(GL_COLOR_BUFFER_BIT |GL_DEPTH_BUFFER_BIT)清除顏色與深度緩沖區。
(2) 啟用客戶端狀態:當glDrawElements函數被調用時,寫入頂點數組與紋理坐標數組。
(3) 通過glActiveTexture函數選擇要激活的紋理單元,初始值是GL_TEXTURE0。
(4) 將已經生成的紋理綁定到等待被紋理化的目標。GL_TEXTURE_2D (一個二維紋理)是默認的紋理綁定目標,mTextureID是紋理的ID。
(5) 通過glTexSubImage2D函數來指定二維紋理子圖,參數如下:
GL_TEXTURE_2D:指定目標紋理類型。
level:指定圖像的詳細程度(即層數)。0是基本的圖像紋理層。
Xoffset:指定紋理像素在X軸方向上、紋理數組內的偏移量。
Yoffset:指定紋理像素在Y軸方向上、紋理數組內的偏移量。
width:指定紋理子圖的寬度。
height:指定紋理子圖的高度
format:指定像素數據的格式。
Type:指定像素數據的數據類型。
data:指定指向內存中圖像數據的指針。
(6) 通過調用以下函數繪制四邊形頂點、坐標與索引:
glFrontFace:啟用四邊形的正面。
glVertexPointer:定義四邊形的頂點數據數組,頂點數據大小為3,數據類型是GL_FLOAT,數組中每個頂點間的間隔(步長)為0。
glTexCoordPointer:定義四邊形的紋理數組,紋理坐標大小為2,數據類型是GL_FLOAT,間隔為0。
glDrawElements:通過數據數組以三角形扇(GL_TRIANGLE_FAN)的方式渲染多邊形,有4個頂點,類型為短整型(GL_UNSIGNED_SHORT),外加指向索引的指針。
注意,從代碼清單3中我們可以看到四邊形的兩個軸坐標都在[−1,1]區間內。這是因為OpenGL的坐標系統在(−1,1)之間,原點(0,0)在窗口中心(如圖3-10所示)。
 
在理想的世界裡,我們不應該過多地擔心視頻緩沖區的尺寸(尤其是使用軟件模擬僅有的定標器/渲染器)。當在Android中使用OpenGL縮放視頻時,這卻是事實。在這個示例中,緩沖區的尺寸至關重要。接下來你將學習如何處理任意尺寸的視頻,這一點在OpenGL中工作得不是很好。
4. 當圖像的尺寸不是2的冪時會發生什麼
如前所述,當圖像的尺寸是2的冪時混合縮放會非常完美。但是,也有可能圖像緩沖區不是2的冪。例如,在處理Demo引擎的章節中有一段320×240尺寸的視頻。在這種情況下,圖像仍然被縮放,但是會縮放到紋理尺寸的百分比大小。在圖2和3中可以看到這個效果。
 
在圖2中,有以下尺寸:
設備顯示器:859×480
紋理:512×256
圖像:320×240
正如我們看到的一樣,圖像被縮放到紋理寬度的62%(320/512*100)和高度的93%
(240/256*100)。因此,在任何分辨率大於256的設備上,圖像都會被縮放到設備提供分辨率的62%×93%。現在我們來看看圖3。
 
圖3 縮放尺寸為2的冪的圖像
在圖3中,有以下尺寸:
設備顯示器:859×480
紋理:512×256
圖像:512×256
縮放和繪制
在圖3中,我們看見圖像被縮放到設備提供分辨率的100%,這正是我們想要的。但是如果圖像不是2的冪,那麼我們要如何做呢?為了解決這個問題,我們應該:
(1) 用軟件縮放器將320×240尺寸的圖像縮放到接近2的冪(這裡是512×256)。
(2) 將已縮放的surface轉換成RGB656格式的圖像,以兼容前面介紹的DrawInto-TextureRGB565。
(3) 繪制到紋理,從而使用硬件將其縮放到顯示屏的分辨率。
這種解決辦法可能比前面介紹的方法慢,但仍然比純軟件縮放快,尤其是運行在高分辨率設備時更明顯(如平板電腦)。
代碼清單4展示了如何使用流行的SDL_gfx庫來縮放SDL surface。
代碼清單4 用SDL_gfx庫縮放圖像
代碼如下:

<SPAN >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>

縮放和繪制實現
要放大/縮小SDL surface,需要簡單地調用SDL_gfx庫的zoomSurface:
(1) 一個SDL surface。
(2) 水平縮放因子:(0-1)
(3) 垂直縮放因子:(0-1)
(4) SMOOTHING_OFF:為了能快速繪制,禁用反鋸齒處理。
接下來,讓我們基於分辨率(每個像素的位數)來翻轉SDL surface。代碼清單5展示了如何完成8位RBG格式的surface。
代碼清單5 根據分辨率翻轉SDL surface
代碼如下:

<SPAN >/**
* 通過每個像素的位數翻轉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,然後檢查每個像素的格式:surface->format->BitsPerPixel,並根據該值創建能夠被DrawIntoTextureRGB565使用的RGB565像素數組:
代碼如下:

<SPAN >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調色板上提取每個像素包含的紅、綠和藍值:
代碼如下:

<SPAN >SDL_Color * colors = surface->format->palette->colors;
RED: colors[pixel].r
GREEN: colors[pixel].g
BLUE: colors[pixel].b</SPAN>

為了構建RGB565像素,需要從每個顏色組件中拋棄最低有效位:
代碼如下:

<SPAN >colors[pixel].r >> 3 (8 -3 = 5)
colors[pixel].g >> 2 (8 – 2 = 6)
colors[pixel].b >> 3 (8 – 3 = 5)</SPAN>

然後移動每個組件到16位值的正確位置(5+6+5= 16——因此是RGB656):
代碼如下:

<SPAN >pixels[i] = (RED << 11) | (GREEN << 5) | BLUE</SPAN>

最後將新的數組和圖像寬度、高度一起發送到DrawIntoTextureRGB565。最後一個問題,我們需要一種方式來告訴surface是否需要縮放。當surface在第一次被創建時將完成視頻初始化。代碼清單6展示了如何用SDL創建軟件surface。
代碼清單6 初始化縮放surface
代碼如下:

<SPAN >// 應該被縮放?
static char zoom = 0;
// 縮放范圍[0,1]
static double zoomx = 1.0;
static double zoomy = 1.0;
/**********************************************************
* 圖像的構造函數
* 圖像必須是2的冪 (256×256, 512×256,...)
* 以便用OpenGL紋理進行全屏渲染。如果圖像不是
* POT (320×240),那麼它將被縮放
**********************************************************/
SDL_Surface * JNI_SurfaceNew(int width, int height, int bpp, int flags)
{
Uint32 rmask = 0, gmask = 0, bmask =0 , amask = 0;
// 紋理尺寸和偏移量
int realw = 256, realh = 256, offx = 0, offy = 0;
// 圖像必須是2的冪以便OpenGL能縮放它
if ( width > 512 ) {
Sys_Error("ERROR: INVALID IMAGE WIDTH %d (max POT 512×512)", width);
}
// 真實的W/H必須接近POT值的w/h
// 將要縮放到512×256
// 應該是256,但是512的分辨率更高(更慢)
if ( width > 256 ) realw = 512;
// 大小不是POT,就縮放到接近於POT,可選擇:
// 256×256 (快/分辨率低) 512×256 (分辨率較高/較慢)
// 512×512 最慢
if ( ( width != 512 && width != 256) || ( height != 256 ) ) {
zoom = 1;
zoomx = realw / (float)width;
zoomy = realh / (float)height;
offx = offy = 0;
printf("WARNING Texture of size %dx%d will be scaled to %dx%d zoomx=%.3f
zoomy=%.3f"
, width, height, realw, realh, zoomx, zoomy);
}
// 創建渲染器使用的OpenGL紋理
CreateEmptyTextureRGB565 (realw, realh, offx, offy);
// 這是真正的被用於客戶端渲染視頻的surface
return SDL_CreateRGBSurface (SDL_SWSURFACE, width, height, bpp, rmask,
gmask, bmask,
amask);
}
</SPAN>

如果圖像的尺寸不是2的冪,那麼縮放標志將被設為1,並且水平和垂直方向的縮放因子將開始計算。然後,通過調用CreateEmptyTextureRGB565,根據寬度、高度和紋理的X、Y位移量來創建空紋理。最後調用SDL_CreateRGBSurface以創建SDL surface:
SDL_SWSURFACE:告訴SDL創建軟件surface。
width和height:定義surface的尺寸。
bpp:定義surface中每個像素(分辨率)的位數(8、16、24和32)。
rmask、gmask、bmask和amask:這些是每個像素格式的紅、綠、藍和alpha(透明度)的掩碼值。設置為0來讓SDL注意到它(譯者注:設置為0,OpenGL就可以寫入;設置為1,則不能寫入)。
混合縮放的經驗法則
總而言之,當在游戲中像這樣使用混合縮放時,請牢記以下經驗法則:
如果可以,就總是設置視頻的大小為2的冪:256×256或512×56。高於512對於這種技術來說代價太高。
如果不想設置視頻的尺寸,但又想全屏顯示,就可以像前面提到的那樣,用SDL軟件縮放到最接近的2的冪,然後再使用硬件進行縮放。
如果視頻的尺寸大於512×512,混合縮放技術就未必有效(性能需要)。
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved