常有人说 Java 图形渲染很慢?嗯,相对 C/C++ 而言, Java2D 固有的图像处理能力确实有待提高。
但是,这也仅仅局限于对比 C/C++ 应用而言。
如果您是以其它什么东西与之比较,却得出 Java 渲染很慢的结论。那么,或者并不是出自 Java 本身的原因,而在于您并没能搞清楚该怎样正确的使用 Java 绘图。
况且,即便是相对于 C/C++ 而谈, Java 也并非相差到难以望其项背的地步。相对于某些行将就木的技术,至少我们除了异常积极的自行修改 JRE ,或者极端消极的等待 JRE 官方更新以外,还有使用 OpenGL 或者像素级优化这两条道路可走。
在本节当中,我们就先谈点基础的,来说说 Java 渲染的像素级优化吧。
像素与 RGB :
像素是什么?简单的讲,像素就是色彩,像素是系统能够在计算机屏幕上显示的最小染色点。越高位的像素,其拥有的色板也就越丰富,越能表达颜色的真实感。
众所周知,图像是像素的复合,看似绚丽的形象,也无外是一个个肉眼难以分辨的细微颗粒集合罢了。
比如,在一些常见的 Java 图像处理中,我们经常会用到所谓的 RGB24 模式( 24 位三原色模式,在 Java2D 中以 TYPE_INT_RGB 表示),将 Red , Green , Blue 三种色彩加以混合,创造出唯一的色彩点并绘制到计算机之上。而这个色彩点,也就是所谓的像素。因为在 RGB24 中 Red , Green , Blue 三者都被分配有一个 0~255 的强度值,所以该 RGB 模式的极限机能就是 256*256*256 ,即至多可以显示出 16777216 种颜色。
PS :关于 16 位的 RGB565 ( Java2D 中表示为 TYPE_USHORT_565_RGB )以及 RGB555 ( Java2D 中表示为 TYPE_USHORT_555_RGB )会在以后章节中涉及,大家此刻只要知道,使用 24 位以下的图形处理模式,在显示速度上虽然会有提高,视觉效果上却必然会有损失就可以了。
也许有网友会感叹。哇! 16777216 种颜色,这么多?难道都能用上吗?!
没错, 16777216 种颜色确实很多;事实上,这已非常接近于人类肉眼所能观察到的颜色数目极限 , 所以我们又将它称之为真彩色。然而,人类的欲求却是无止境的,即便能够展现出 16777216 种颜色的 RGB 真彩模式,依旧有人嫌弃它的效果太差。
否则,在您计算机“颜色质量”一栏中,或许就不会再有 32 位这种“多余”的选择了。
正是因为人类天性的贪婪,当今 2D 、 3D 图形渲染中最为常见的 ARGB 模式,也就是 32 位真彩模式才会应运而生。
ARGB 模式:
您问什么是 ARGB ?其实,它就是个穿了 Alpha 通道马甲的 RGB 。
事实上,较之最初的 RGB 模式, ARGB 仅仅增加了一个名为 Alpha 的色彩通道。这是一个 8 位的灰度通道,用 256 级灰度来记录图像中的透明度信息,定义透明、不透明和半透明区域。通俗的说,你的 ARGB 图像是否透明,与底层图像的遮挡关系如何,都将由 Alpha 这个参数所决定。
在 Java2D 中, TYPE_INT_ARGB 象征着 32 位十六进制数的 ARGB 色彩模式。
将“ 32 位十六进制数”的概念具象化后,也就是四对十六进制数字的序列。每个十六进制对定义四个颜色通道,即 Red 、 Green 、 Blue 和 Alpha 中每个颜色通道的强度,全以范围介于 0 到 255 之间的十进制数的十六进制表示法。(在 16 进制表示中, FF 是指全强度 ,最高的 255 。 00 是指通道中无颜色,最低为 0 )
正如大家都知道的那样 , 由于颜色值长度需要两位数字 , 因此您需要填充一个通道 , 例如用 01 代替 1 ,这样才可确保十六进制数中始终具有八个数字。还应确保指定十六进制数前缀 0x ,这样才能被 Java 识别为 16 进制。
例如,白色 ( 全强度 ) 用十六进制记数法表示为 : 0xFFFFFFFF 。而黑色正好相反;它在红色、绿色和蓝色中的任何一个通道中都 无颜色,结果就成了 : 0xFF000000 。请注意 , Alpha 通道中的全强度意味着没有 Alpha (FF) ,也就是不透明 , 而无强度 (00) ,则意味着全透明。
利用 ARGB 模式,我们可以轻易的创建出一些 RGB 所无法实现的艳丽图像,完成一些 RGB 所无法企及的缤纷效果。应该说,如果您只是想制作一个让人可以入目的画面,那么普通的 RGB 模式已然游刃有余,但如果您想百尺竿头更进一步,制作出一些让人心旷神怡的视觉盛宴,那就非 ARGB 不可。而一旦您开始使用 ARGB ,就与 Alpha 、 Red 、 Green 、 Blue 这四层色彩通道留下了不解之缘。
在 Java 中获得 ARGB 像素的方法如下:
public static int getARGB( int r, int g, int b, int alpha) {
return (alpha << 24) | (r << 16) | (g << 8) | b;
}
关于 BufferedImage :
当我们需要使用像素级操作,当我们需要设定针对不同图像的不同色彩模式时,最直接有效的方法,就是使用 BufferedImage 。
事实上,就像深入优化 Flash 渲染必须利用 BitmapData 一样,没有对 BufferedImage 的相关了解,提高 Java2D 性能根本无从谈起,甚至不能说你会用 Java2D 。
当您想要创建 BufferedImage ,并对其中像素进行直接操作时,大体上有三种方式可选:
1 、直接创建 BufferedImage ,导出 DataBufferInt 对象获取像素集合。
// 创建一个 640x480 的 BufferedImage ,设定渲染模式为 ARGB
BufferedImage image = new BufferedImage (640, 480,
BufferedImage . TYPE_INT_ARGB );
// 获得当前 BufferedImage 的图像数据 存储器,并转为 DataBufferInt
DataBufferInt dataBuffer = ((DataBufferInt) image.getRaster()
.getDataBuffer());
// 获得对应 BufferedImage 的像素数组
int [] pixels = dataBuffer.getData();
2 、以 int[] 生成 WritableRaster ,以 WritableRaster 产生 BufferedImage 。
// 设定 BufferedImage 的宽与高
int width = 640, height = 480;
int size = width * height;
// 创建数组,用以保存对应 BufferedImage 的像素集合
int [] pixels = new int [size];
// 以指定数组创建出指定大小的 DataBuffer
DataBuffer dataBuffer = new DataBufferInt(pixels, size);
// 创建一个 WritableRaster 对象,用以 管理光栅
WritableRaster raster = Raster.createPackedRaster (dataBuffer, width, height,width, new int [] { 0xFF0000, 0xFF00, 0xFF }, null );
// 创建一个 24 位的 RGB 色彩模型,并填充相应的 R 、 G 、 B 掩码
DirectColorModel directColorModel = new DirectColorModel(24, 0xFF0000, 0xFF00, 0xFF);
// 以下为 32 位 RGB 色彩模型
// DirectColorModel directColorModel = new DirectColorModel(32, 0xFF000000, 0xFF0000, 0xFF00, 0xFF);
// 生成 BufferedImage, 预设 Alpha ,无配置
BufferedImage image = new BufferedImage(directColorModel, raster, true , null );
3 、与方法 2 基本相同,唯一差别在于使用了 SampleModel
int width = 640, height = 480;
int size = width * height;
int [] pixels = new int [size];
// 24 位色彩模型
DirectColorModel directColorModel = new DirectColorModel(24, 0xFF0000,
0xFF00, 0xFF);
// 以 SinglePixelPackedSampleModel 构建像素包
SampleModel sample = new SinglePixelPackedSampleModel(
DataBuffer . TYPE_INT , width, height, new int [] { 0xFF0000,
0xFF00, 0xFF });
// 生成 DataBuffer
DataBuffer dataBuffer = new DataBufferInt(pixels, size);
// 以 SampleModel 及 DataBuffer 生成 WritableRaster
WritableRaster raster = Raster.createWritableRaster (sample, dataBuffer,
new Point(0, 0));
// 生成 BufferedImage
BufferedImage image = new BufferedImage(directColorModel, raster, true , null );
实际上,虽然表面上有所不同,但无论您采用以上何种方式获得 BufferedImage 及其对应的像素集合( PS: 此处并非一定要获得像素的 int[] 形式,如 short[] 、 byte[] 等各式亦可,请根据实际需求决定), pixels 对您而言都将成为一块保存有图像数据的内存区域,针对此 pixels 进行的任何修改,都将被直接反馈于 BufferedImage 之上。
得到了像素集合,我们又该如何将其应用到 Java2D 中呢?下面,我将介绍两个像素级 Java 渲染组件给大家参考。下面我们所使用到的一切操作,也都将围绕 pixels 这个以 int[] 形式出现的数组展开。
一、 古董级的 Processing
项目地址: http://processing.org/
这是一套完整的,开源的,兼顾 2D 与 3D 方面的 Java 渲染组件。事实上, Processing 在针对 Java2D 性能优化上的意义并不太大,因为它本来就不是为了解决性能问题而出现的。
Processing 所做的,更多的是一种效果优化,一种对 Java 语言的延伸。它希望人们能利用它对 Java 的扩充,以简单高效的方式实现绚丽夺目的图形效果。应该说, Processing 将 Java 的语法简化并将其运算结果 “ 感官化 ” ,让使用者能很快享有声光兼备的交互式多媒体作品。
由于 Processing 运行于 PApplet 之上,而 PApplet 继承自 Applet 。也就是说原本的 Processing 也是一种小程序,如果我们要将它应用在网页环境之外,要们就将 PApplet 插入到 Frame/JFrame 当中,要么就将其改写。
为了未来的演示更加方便,笔者选择了改写的道路,将其 PGraphics 渲染层直接封装。以下,是一个已经替换为 Processing 渲染的 LGame 示例:
二、 新生代的 PulpCore
项目地址: http://www.interactivepulp.com/pulpcore/
事实上, PulpCore 在国外的 Java 圈中也算颇有名气,甚至连某位 JavaFX 开发者都曾以它和自己的项目作过比较。如果有朋友泡过 http://www.javagaming.org/ ,想必应该知道,如果你在该论坛中寻求 Java 游戏框架,那么 3D 方面的优先推荐必然是 JME , 2D 方面的优先推荐绝对是 Slick2D ,至于网页游戏开发方面,则必属 PulpCore 无疑。
在以 OpenGL 为绝对主流的 javagaming 上,一款以标准 Java2D 开发的框架,居然会受到如此推崇, PulpCore 的技术价值我们可想而知。
下图为 PulpCore 提供的应用示例:
PS :虽然 PulpCore 所提供的示例多为小游戏,但该作者曾反复强调, PulpCore 是一个开源的 2D 渲染和动画处理框架。
与 Processing 一样,启动 PulpCore 的 CoreApplet 继承自 Applet ,所以 PulpCore 依旧属于 Applet 实现,也就是默认情况下只能运行于网页之上。但相对于标准 Applet 应用, PulpCore 却做了更多的优化,尤其注重用户体验与动画效果。应该说, Pulpcore 是目前为止笔者所见过的,在不损失图像色彩的情况 下最高效的 Java2D 解决方案。
关于图像渲染部分, PulpCore 中有对应于标准 Java2D 的 Graphics 类,名为 CoreGraphics 。其中对像素级操作进行了必要的封装,也基本参照标准 Java2D API 命名。( PS :具体留待下节讲解,目前请自行参考其源码)不过,或许是方便模块化管理的缘故, CoreGraphics 默认情况下并不对外开放,而被统一封装在 PulpCore 所提供的各种精灵类里。
如果您想要获得 CoreGraphics 进行修改,要么请重载 Sprite 的 draw( 需要 super.draw 一下,否则会覆盖到基础操作 ) ,要么请在 Scene2D 中重载 drawScene (需要 super.drawScene 一下,否则会覆盖到基础操作), PulpCore 并没有直接提供给您。对于仅想进行简单图形绘制的用户而言,这不得不说是一个小小的不足。
另外,虽然 PulpCore 也有对应于 Font 的 CoreFont 类,但相比于 Processing 的字体绘制方案,它明显寒酸了很多。
实际上, PulpCore 中的 CoreFont 只是一个分图管理器,由用户导入一张由英文字母及各种符号组成的图像,而 CoreFont 负责分配不同的图像对应不同的字母绘制。这意味着,如果您不自行扩充其 CoreFont 部分,那么 PulpCore 将绝对无法支持中文输入及显示。( PS :目前来说,最偷懒的方法就是将 Processing 中的 PFont 和 text 部分直接“移植”到 CoreGraphics 中使用。毕竟两者都是操作像素绘制图像,很好 copy ……)
针对 PulpCore 的 CoreGraphics ,笔者也提供了一个 LGame 的替代封装,以方便后文讲解分析其渲染方式之用。
示例源码:
另外笔者还要补充一点,那就是 PulpCore 虽然提供了较为完善的“脏绘”机制,却必须和 Sprite 一起使用才能看到效果(被封装到了 Sprite 的 draw 函数里,所以单就渲染速度而言,在 PulpCore 中使用精灵绘图反而比不用更快)。
从下文开始,笔者将以 PulpCore 为基础,逐步讲解 Java 像素级渲染框架的设计与实现。
以下为以PulpCore与Processing进行渲染的LGame实验工程:
http://loon-simple.googlecode.com/files/Pixels-LGame.7z
——————————————————————
心理学上有一个名词叫做 The Halo Effect ,也就是俗称的晕轮效应或者说“刻板印象”。在这种心理现象影响下,很多人往往会将某种事物的“第一印象”当作终身的准则,而无视其实际究竟是怎样的。
其实对于 Java 桌面或网页应用而言,性能上的问题早就已经算不得什么问题。只要人们稍微留心一下,就会发现 Java 在游戏开发方面,至少在网页游戏方面完全可以比某些东西作的更好,更强,更复杂,更快捷,也更稳定。真正关键的,反倒是那些听信了流言蜚语的人们对于 Java 性能上的误解,以及食古不化的偏见,才是真正制约 Java 发展的拦路虎。
所谓积重难返,要想扭转这种顽固偏见,只凭小弟一人是绝对不足够的,还要靠各位 Java 同仁的努力。
都说“荒田无人耕,耕开有人争”,可都等着别人耕田,毕竟太慢,始终没有自己动手那么快捷。毕竟只有将 Java 做大做强,各位同仁才能有更多的出路,更好的待遇,以及更多的 Money 好赚……