当开发基于软件模式的游戏时,通过缩放视频缓冲区来适应显示尺寸是最棘手的问题之一。当面对众多不同的分辨率时(比如开放环境下的Android),该问题会变得更加麻烦,作为开发人员,我们必须尝试在性能与显示质量之间找到最佳平衡点。正如我们在第2章中看到的,缩放视频缓冲区从最慢到最快共有3种类型。
这里,我们选择第二种方式,也是在终端设备分裂的平台上的最佳选择。你拥有软件渲染器,并希望将游戏适配到任意分辨率的显示屏上。此方法非常适合模拟器游戏、街机游戏、简单的射击游戏等。它在各种低端、中端、高端设备上都表现很好。
下面我们开始介绍混合模式并探讨为什么这种方法更加可行。然后,将深入研究这种方法的实现,包括如何初始化surface并通过实际缩放来绘制到纹理。
这种缩放技术背后的原理很简单:
从实现的角度看,这个过程可描述如下:
代码清单1 创建RGB656格式的空纹理
// 纹理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);
}
代码清单2展示了CreateEmptyTextureRGB565的实现过程,创建RGB656格式的空纹理用于绘制,参数如下:
在OpenGL中创建纹理,我们只需要调用:
glGenTextures(1, &mTextureID);
glBindTexture(GL_TEXTURE_2D, mTextureID);
这里的mTextureID是整型变量,用于存储纹理的ID。然后需要设置下面这些纹理参数:
最后,我们通过glTexImage2D函数及以下参数来指定二维纹理:
注意:纹理的尺寸必须是2的幂,如256、512、1024等。但是,要显示的视频图像的尺寸可以是任意尺寸。这就意味着,纹理的尺寸必须是大于或等于要显示的视频图像尺寸的2的幂。稍后我们将进行详细介绍。
现在,让我们来看一看混合视频缩放的实际实现,接下来的两个小节将介绍如何初始化用来缩放的surface以及如何实现实际的绘制。
要进行缩放,就必须保证纹理的尺寸大于或等于要显示的视频图像的尺寸。否则,当图像渲染的时候,会看到白色或黑色的屏幕。在代码清单2中,JNI_RGB565_SurfaceInit函数将确保产生有效的纹理尺寸。使用图像的宽度和高度为参数,然后调用getBestTexSize函数来获取最接近要求的纹理尺寸,最后通过调用CreateEmptyTextureRGB565函数来创建空的纹理。注意,如果图像尺寸小于纹理尺寸,就通过计算X、Y坐标的偏移量来将其置于屏幕的中心。
代码清单2 初始化surface
// 获取下一个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);
}
最后,为了将图像显示到屏幕上(也称作surface翻转),我们调用JNI_RGB565_Flip函数,其参数是像素数组(使用RGR656编码)和要显示的图像尺寸。JNI_RGB565_Flip函数通过调用DrawIntoTextureRGB565将图像绘制到纹理并交换缓冲区。注意交换缓冲区的函数是用Java编码的,而不是用C语言编码的,因此我们需要一个方法来调用Java的交换函数。我们可以通过使用JNI方法调用某个Java方法来完成缓冲区的交换工作(见代码清单3)。
代码清单3 用四边形将图像缓冲区绘制到纹理
// 四边形顶点的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 ();
}
使用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函数来指定二维纹理子图,参数如下:
(6) 通过调用以下函数绘制四边形顶点、坐标与索引:
注意,从代码清单3中我们可以看到四边形的两个轴坐标都在[−1,1]区间内。这是因为OpenGL的坐标系统在(−1,1)之间,原点(0,0)在窗口中心(如图3-10所示)。
图3-10 OpenGL坐标系统
在理想的世界里,我们不应该过多地担心视频缓冲区的尺寸(尤其是使用软件模拟仅有的定标器/渲染器)。当在Android中使用OpenGL缩放视频时,这却是事实。在这个示例中,缓冲区的尺寸至关重要。接下来你将学习如何处理任意尺寸的视频,这一点在OpenGL中工作得不是很好。
如前所述,当图像的尺寸是2的幂时混合缩放会非常完美。但是,也有可能图像缓冲区不是2的幂。例如,在处理Demo引擎的章节中有一段320×240尺寸的视频。在这种情况下,图像仍然被缩放,但是会缩放到纹理尺寸的百分比大小。在图2和3中可以看到这个效果。
图2缩放非2的幂尺寸的图像
在图2中,有以下尺寸:
正如我们看到的一样,图像被缩放到纹理宽度的62%(320/512*100)和高度的93%
(240/256*100)。因此,在任何分辨率大于256的设备上,图像都会被缩放到设备提供分辨率的62%×93%。现在我们来看看图3。
图3 缩放尺寸为2的幂的图像
在图3中,有以下尺寸:
缩放和绘制
在图3中,我们看见图像被缩放到设备提供分辨率的100%,这正是我们想要的。但是如果图像不是2的幂,那么我们要如何做呢?为了解决这个问题,我们应该:
(1) 用软件缩放器将320×240尺寸的图像缩放到接近2的幂(这里是512×256)。
(2) 将已缩放的surface转换成RGB656格式的图像,以兼容前面介绍的DrawInto-TextureRGB565。
(3) 绘制到纹理,从而使用硬件将其缩放到显示屏的分辨率。
这种解决办法可能比前面介绍的方法慢,但仍然比纯软件缩放快,尤其是运行在高分辨率设备时更明显(如平板电脑)。
代码清单4展示了如何使用流行的SDL_gfx库来缩放SDL surface。
代码清单4 用SDL_gfx库缩放图像
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);
}
}
缩放和绘制实现
要放大/缩小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
/**
* 通过每个像素的位数翻转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 ();
}
指定SDL surface,然后检查每个像素的格式:surface->format->BitsPerPixel,并根据该值创建能够被DrawIntoTextureRGB565使用的RGB565像素数组:
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);
}
从surface调色板上提取每个像素包含的红、绿和蓝值:
SDL_Color * colors = surface->format->palette->colors;
RED: colors[pixel].r
GREEN: colors[pixel].g
BLUE: colors[pixel].b
为了构建RGB565像素,需要从每个颜色组件中抛弃最低有效位:
colors[pixel].r >> 3 (8 -3 = 5)
colors[pixel].g >> 2 (8 – 2 = 6)
colors[pixel].b >> 3 (8 – 3 = 5)
然后移动每个组件到16位值的正确位置(5+6+5= 16——因此是RGB656):
pixels[i] = (RED << 11) | (GREEN << 5) | BLUE
最后将新的数组和图像宽度、高度一起发送到DrawIntoTextureRGB565。最后一个问题,我们需要一种方式来告诉surface是否需要缩放。当surface在第一次被创建时将完成视频初始化。代码清单6展示了如何用SDL创建软件surface。
代码清单6 初始化缩放surface
// 应该被缩放?
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);
}
如果图像的尺寸不是2的幂,那么缩放标志将被设为1,并且水平和垂直方向的缩放因子将开始计算。然后,通过调用CreateEmptyTextureRGB565,根据宽度、高度和纹理的X、Y位移量来创建空纹理。最后调用SDL_CreateRGBSurface以创建SDL surface:
混合缩放的经验法则
总而言之,当在游戏中像这样使用混合缩放时,请牢记以下经验法则:
《Android 4 游戏高级编程(第2版)》试读电子书免费提供,有需要的留下邮箱,一有空即发送给大家。 别忘啦顶哦!