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&#39;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&#39;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&#39;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&#39;s the easy part.

It can get trickier if the proprietary API doesn&#39;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&#39;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&#39;t need any changes (we&#39;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&#39;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&#39;s up for implementing bilinear filtering?), don&#39;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校验和
我们感兴趣的块是调色板块,类型区的内容是&#39;P&#39;、&#39;L&#39;、&#39; T&#39;、&#39;E&#39;四个字节,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释放掉。