J2ME游戏中的图片处理
说明:此文仅发表在J2ME开发网和我的blog上(blog.csdn.net/n5),转载必须经过本人同意(Email: 该邮件地址已受到反垃圾邮件插件保护。要显示它需要在浏览器中启用 JavaScript。 )。
图片资源乃是游戏的外衣,直接影响一个游戏是否看上去很美。在J2ME游戏开发中,由于受到容量和内存的两重限制,图片使用受到极大的限制。在这种环境中,处理好图片的使用问题就显得更加重要。
本文从容量和内存两个方面谈谈J2ME游戏图片处理的基本方法。
一 减少图片容量
方法1:将多张png图片集成到一张图片上。
这是最基本也是最有效的减少png图片容量的办法了。比如你有10张png图片,每张10×15,现在你可以把它集成到一张100×15或者10×150 或者X×X的图片上去。这张大png图片的容量比10张png图片的总容量小很多。这是因为省去了9张图片的文件头,文件结束数据块等等,而且合并了调色板(如果10张图片的调色板恰好相同,则省去了9张图片的调色板所占的容量!这是个不小的数字)
方法2:减少图片的颜色数
减少颜色也算是一个方法?我想说的是什么时候减,谁去减。如果游戏完成后发现容量超出,此时在用优化工具减少颜色,虽然能降低图片容量,但图片效果可能就不让你满意了。所以,在美工作图时就要确定使用的颜色数,手机游戏使用的是象素图,即一个象素一个象素点出来的图像,所以预先规定调色板颜色数量是可以办到的。不过,最终使用优化工具也是有用的,有时候相差一两种颜色,但效果差别并不大,容量却可以变小一些。呵呵,减少颜色确实可以算是一种方法。
方法3:尽可能使用旋转和翻转
这点不用解释了
方法4:使用换调色板技术和自定义图片格式
如果前两种方法还不能满足你对容量的要求,而你的游戏中恰好使用了很多仅颜色不同的怪物,那么可以试试换调色板技术。J2ME规范中规定手机至少可以支持 png格式的图片,每张png都带有调色板数据,如果两张图片除了颜色不同而其他(包括颜色数)完全相同,则只要保存一张图片和其他图片的调色板,这相对于保存多张图片来说节省了不少容量。不过这个方法挺麻烦,你得了解png文件格式,然后做一个工具提取出调色板数据和调色板数据块在png文件中的偏移。内存中保存图像仍使用Image,如果要换调色板,则将png文件读入到一个字节数组中,根据调色板数据块在png中的偏移,用新的调色板代替原来的调色板数据,然后用这个字节数组创建出换色后的Image。也许你觉得保存一张png和n份调色板数据的方法有点浪费。至少多保存了1份调色板数据啊!如果直接将图像数据提取出来,在加上n份调色板数据,岂不是更节省容量。但是使用上面的方法,我们还可以用drawImage渲染。如果这样自定义了图片格式,那只有自己写个渲染函数了,这倒还可以,只不过put pixel的速度在某些机器上非常慢。或者自己构造png格式数据,再使用Image.如果你真得决定这么做,我还有个小建议,不要对图像数据进行压缩, zip压缩大多数时候比你写得压缩算法好(参见J2ME Game开发笔记-压缩还是不压缩)。论坛上有位朋友提过使用bmp格式代替png格式,jar中图片容量更小,也是一个道理。
二 减少图片所占内存
1 图片所占内存的计算
png图片所占用的内存并不对应于图片容量。图片占用的内存的计算为:width*height*bpp。bpp即为系统内置的颜色位数。以Nokia 6600为例,象素格式为565共16位。所以一张100*100的图片占用100*100*(16/8)=20000字节,约为19.5k的内存。象素格式是固定的无法改变,所以只有减少图片的宽和高才能降低其消耗的内存。
2 减少Image对象数量可节约大量内存
减少Image对象数量不等于减少图片数量。我的意思是说,将一张集成图保存在一个Image对象中,通过setClip的方法从这个Iamge对象中选取你需要的图像渲染。不过这个方法牺牲了一点速度,每帧都从集成图Image中减切图像的速度比无减切的渲染慢。但对于数目不多的渲染,比如精灵,使用这个方法没问题。这个方法还有一个问题就是不能释放集成图中不需要的图片,这就要看你集成的程度了。从图片容量和内存管理的角度综合考虑,我一般使用二次集成的方法。比如有n个精灵,先将各精灵所有的图片集成到一张集成图中,得到n张集成图,然后将这n张集成图再次集成到一张更大的集成图中。这样在jar中只存在一张集成图。使用时,先将大集成图分割载入到n个Image对象中即可。这样各个精灵的图片可以单独管理了。
3 使用旋转和翻转
只保存一个原始的Image,需要时再旋转或翻转
后记:
本文仅仅从图片方面谈谈容量和内存,所谈的几点均是普遍的方法,内行人一眼就能看明白,对于新手可以参考一下。减少J2ME游戏容量和内存也确实是一个值得探讨的问题,图片方面仅是其一。想要有较好的效果必须从资源代码等多方面入手,而这之中必须处理好容量,速度,内存,内存峰值,等待时间等等的关系.最后的方案往往是各方面因素相互平衡的结果.
// fixed point constants
private static final int FP_SHIFT = 13;
private static final int FP_ONE = 1 << FP_SHIFT;
private static final int FP_HALF = 1 << (FP_SHIFT - 1);
// resampling modes - valid values for the mode parameter of resizeImage()
// any other value will default to MODE_BOX_FILTER because of the way the conditionals are set in resizeImage()
public static final int MODE_POINT_SAMPLE = 0;
public static final int MODE_BOX_FILTER = 1;
/**
* getPixels
* Wrapper for pixel grabbing techniques.
* I separated this step into it's own function so that other APIs (Nokia, Motorola, Siemens, etc.) can
* easily substitute the MIDP 2.0 API (Image.getRGB()).
* @param src The source image whose pixels we are grabbing.
* @return An int array containing the pixels in 32 bit ARGB format.
*/
int[] getPixels(Image src) {
int w = src.getWidth();
int h = src.getHeight();
int[] pixels = new int[w * h];
src.getRGB(pixels,0,w,0,0,w,h);
return pixels;
}
/**
* drawPixels
* Wrapper for pixel drawing function.
* I separated this step into it's own function so that other APIs (Nokia, Motorola, Siemens, etc.) can
* easily substitute the MIDP 2.0 API (Image.createRGBImage()).
* @param pixels int array containing the pixels in 32 bit ARGB format.
* @param w The width of the image to be created.
* @param h The height of the image to be created. This parameter is actually superfluous, because it
* must equal pixels.length / w.
* @return The image created from the pixel array.
*/
Image drawPixels(int[] pixels, int w, int h) {
return Image.createRGBImage(pixels,w,h,true);
}
/**
* resizeImage
* Gets a source image along with new size for it and resizes it.
* @param src The source image.
* @param destW The new width for the destination image.
* @param destH The new heigth for the destination image.
* @param mode A flag indicating what type of resizing we want to do. It currently supports two type:
* MODE_POINT_SAMPLE - point sampled resizing, and MODE_BOX_FILTER - box filtered resizing (default).
* @return The resized image.
*/
Image resizeImage(Image src, int destW, int destH, int mode) {
int srcW = src.getWidth();
int srcH = src.getHeight();
// create pixel arrays
int[] destPixels = new int[destW * destH]; // array to hold destination pixels
int[] srcPixels = getPixels(src); // array with source's pixels
if (mode == MODE_POINT_SAMPLE) {
// simple point smapled resizing
// loop through the destination pixels, find the matching pixel on the source and use that
for (int destY = 0; destY < destH; ++destY) {
for (int destX = 0; destX < destW; ++destX) {
int srcX = (destX * srcW) / destW;
int srcY = (destY * srcH) / destH;
destPixels[destX + destY * destW] = srcPixels[srcX + srcY * srcW];
}
}
}
else {
// precalculate src/dest ratios
int ratioW = (srcW << FP_SHIFT) / destW;
int ratioH = (srcH << FP_SHIFT) / destH;
int[] tmpPixels = new int[destW * srcH]; // temporary buffer for the horizontal resampling step
// variables to perform additive blending
int argb; // color extracted from source
int a, r, g, b; // separate channels of the color
int count; // number of pixels sampled for calculating the average
// the resampling will be separated into 2 steps for simplicity
// the first step will keep the same height and just stretch the picture horizontally
// the second step will take the intermediate result and stretch it vertically
// horizontal resampling
for (int y = 0; y < srcH; ++y) {
for (int destX = 0; destX < destW; ++destX) {
count = 0; a = 0; r = 0; b = 0; g = 0; // initialize color blending vars
int srcX = (destX * ratioW) >> FP_SHIFT; // calculate beginning of sample
int srcX2 = ((destX + 1) * ratioW) >> FP_SHIFT; // calculate end of sample
// now loop from srcX to srcX2 and add up the values for each channel
do {
argb = srcPixels[srcX + y * srcW];
a += ((argb & 0xff000000) >> 24); // alpha channel
r += ((argb & 0x00ff0000) >> 16); // red channel
g += ((argb & 0x0000ff00) >> 8); // green channel
b += (argb & 0x000000ff); // blue channel
++count; // count the pixel
++srcX; // move on to the next pixel
}
while (srcX <= srcX2 && srcX + y * srcW < srcPixels.length);
// average out the channel values
a /= count;
r /= count;
g /= count;
b /= count;
// recreate color from the averaged channels and place it into the temporary buffer
tmpPixels[destX + y * destW] = ((a << 24) | (r << 16) | (g << 8) | b);
}
}
// vertical resampling of the temporary buffer (which has been horizontally resampled)
System.out.println("Vertical resampling...");
for (int x = 0; x < destW; ++x) {
for (int destY = 0; destY < destH; ++destY) {
count = 0; a = 0; r = 0; b = 0; g = 0; // initialize color blending vars
int srcY = (destY * ratioH) >> FP_SHIFT; // calculate beginning of sample
int srcY2 = ((destY + 1) * ratioH) >> FP_SHIFT; // calculate end of sample
// now loop from srcY to srcY2 and add up the values for each channel
do {
argb = tmpPixels[x + srcY * destW];
a += ((argb & 0xff000000) >> 24); // alpha channel
r += ((argb & 0x00ff0000) >> 16); // red channel
g += ((argb & 0x0000ff00) >> 8); // green channel
b += (argb & 0x000000ff); // blue channel
++count; // count the pixel
++srcY; // move on to the next pixel
}
while (srcY <= srcY2 && x + srcY * destW < tmpPixels.length);
// average out the channel values
a /= count; a = (a > 255) ? 255 : a;
r /= count; r = (r > 255) ? 255 : r;
g /= count; g = (g > 255) ? 255 : g;
b /= count; b = (b > 255) ? 255 : b;
// recreate color from the averaged channels and place it into the destination buffer
destPixels[x + destY * destW] = ((a << 24) | (r << 16) | (g << 8) | b);
}
}
}
// return a new image created from the destination pixel buffer
return drawPixels(destPixels,destW,destH);
}
That will work with MIDP 2.0. With pure MIDP 1.0 there are no methods to get a pixel array from an Image, so to use this you would need to modify the code to use a proprietary extension API if one is available.
Some porting guidelines for proprietary APIs:
As I mentioned in the codes comments, you will need to create versions of getPixels() and drawPixels() that use the proprietary API. That's the easy part.
It can get trickier if the proprietary API doesn't use the 8888 ARGB format. If this is the case you have two choices:
1) Write the getPixels()/drawPixels() so that they convert the images to the 8888 ARGB format. This option would be simpler since you could leave the code for resizeImage() unchanged. But it would be a lot less efficient as all the format conversion will take up valuable processing time.
2) Work with the API's native format. This would be a lot more efficient, but it does require that you rework the code in resizeImage() to adapt to the new pixel format. The point sampling part won't need any changes (we're just copying the colors as they are), but the much nicer looking box-sampling method, will require you to change the following sections:
The extraction of the channels (two occurences) -
Code:
argb = srcPixels[srcX + y * srcW];
a += ((argb & 0xff000000) >> 24); // alpha channel
r += ((argb & 0x00ff0000) >> 16); // red channel
g += ((argb & 0x0000ff00) >> 8); // green channel
b += (argb & 0x000000ff); // blue channel
The recreation of the pixel (two ocurrences) -
Code:
tmpPixels[destX + y * destW] = ((a << 24) | (r << 16) | (g << 8) | b);
// ...
destPixels[x + destY * destW] = ((a << 24) | (r << 16) | (g << 8) | b);
And finally, in the second sampling stage (vertical), when the channels are averaged out, they are also clamped so they won't overflow into neighboring channels. For an 8888 format, they are clamped to 255, but for smaller formats they will need to be clamped differently (for instance, in a 4444 format they would be clamped to 15) -
Code:
// average out the channel values
a /= count; a = (a > 255) ? 255 : a;
r /= count; r = (r > 255) ? 255 : r;
g /= count; g = (g > 255) ? 255 : g;
b /= count; b = (b > 255) ? 255 : b;
Also, if anyone here volunteers to add some more sampling methods (who's up for implementing bilinear filtering?), don't forget to share with the rest of the world.
作为网络标准图片格式,.png已经很小了
但是在.png的图片应用领域里
往往是能小1b就小1b
pngout可以把.png里所有的鳄鱼信息全部擦除,完全无损的压缩,强力推荐
下载地址
http://advsys.net/ken/utils.htm
使用方法
pngout mypic.png
打造自己的PNG类[转]
想像一下,有一个游戏,里面有很多种颜色的人,图片完全一样,只是人物衣服的颜色不同。比如街霸中真的红色的Ken和假的青色的Ken,它们的图形一模一样,只是颜色换掉了。
这时你会怎么做呢?画好多张图片?拜托,都21世纪了,别做这种没有一点技术含量的工作好不好?聪明的你一定会想,如果可以把里面的红色“替换”成青色就好了。OK,那我们就来替换。
GIF、 PNG等很多格式的图片,都是用调色板来记录颜色的。比如记录3号颜色为0xff0000红色,那么我们把3号颜色改为青色的代码,图片中的所有标记为3 号颜色的区域都变成青色了。怎么样?说起来好像很简单吧?^_^下面我们用J2ME手机用的最多的png格式的图片来完成这项工作。
首先我们要清楚png图片的格式。
首先是8 byte的png标志。其次是若干个块,每个块有下列结构:
4 byte Length 块的data区的length
4 byte Type 块的类型
length byte Data 块的data
4 byte CRC 块类型和data两个区共length+4字节的CRC校验和
我们感兴趣的块是调色板块,类型区的内容是'P'、'L'、' T'、'E'四个字节,data区是所有颜色按照0xRRGGBB的格式排列,length区的值是颜色数*3。OK,基础知识准备完毕。(CRC校验和的算法和png结构的详细信息可参考http://www.w3.org/TR/PNG- Structure.html)
接下来设计我们的超级牛X的PalettedImage类,首先提供两个工厂方法,一个通过文件名从包中创建图片,另一个直接从byte数组中创建。创建后马上执行analyze方法,得到颜色数、调色板偏移、CRC校验码偏移等值(针对一张图片这些值是不变的)。以后就可以用setColor替换某种颜色或者用setPalette替换整个调色板的所有颜色值了。每次替换颜色后都记得要重新生成正确的CRC 校验和,并重新创建图片。
这个类的好处在于不必携带极多的图片资源,而只需要一张图片和若干套调色板信息就好了。缺点在于它会占用一个图片的2倍的内存(imgData数组和image对象),不过你可以在得到新Image后就把PalettedImage释放掉。