BufferedImage 与像素级渲染

常有人说 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


00


事实上,较之最初的 RGB 模式, ARGB 仅仅增加了一个名为 Alpha 的色彩通道。这是一个 8 位的灰度通道,用 256 级灰度来记录图像中的透明度信息,定义透明、不透明和半透明区域。通俗的说,你的 ARGB 图像是否透明,与底层图像的遮挡关系如何,都将由 Alpha 这个参数所决定。

 

00


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) ,则意味着全透明。


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 示例:

 

  1. public   class  ProcessingBall  extends  Screen {  
  2.    
  3.     class  Ball {  
  4.    
  5.        float  x;  
  6.    
  7.        float  y;  
  8.    
  9.        float  speed;  
  10.    
  11.        float  gravity;  
  12.    
  13.        float  w;  
  14.    
  15.        float  life =  255 ;  
  16.    
  17.        Ball(float  tempX,  float  tempY,  float  tempW) {  
  18.            x = tempX;  
  19.            y = tempY;  
  20.            w = tempW;  
  21.            speed = 0 ;  
  22.            gravity = 0 .1f;  
  23.        }  
  24.    
  25.        void  move() {  
  26.            speed = speed + gravity;  
  27.            y = y + speed;  
  28.            if  (y > getHeight()) {  
  29.               speed = speed * -0 .8f;  
  30.               y = getHeight();  
  31.            }  
  32.        }  
  33.    
  34.        boolean  finished() {  
  35.            life--;  
  36.            if  (life <  0 ) {  
  37.               return   true ;  
  38.            } else  {  
  39.               return   false ;  
  40.            }  
  41.        }  
  42.    
  43.        void  display(LPGraphics g) {  
  44.            g.fill(0 , life);  
  45.            g.ellipse(x, y, w, w);  
  46.        }  
  47.     }  
  48.    
  49.     private  ArrayList balls;  
  50.    
  51.     private   int  ballWidth =  48 ;  
  52.    
  53.     PImage image=Utils.loadImage("system/image/logo.png" );  
  54.      
  55.     public  ProcessingBall() {  
  56.        balls = new  ArrayList();  
  57.        balls.add(new  Ball(getWidth() /  20 , ballWidth));  
  58.     }  
  59.    
  60.     public   void  draw(LPGraphics g) {  
  61.        g.background(255 );  
  62.        for  ( int  i = balls.size() -  1 ; i >=  0 ; i--) {  
  63.            Ball ball = (Ball) balls.get(i);  
  64.            ball.move();  
  65.            ball.display(g);  
  66.            if  (ball.finished()) {  
  67.               balls.remove(i);  
  68.            }  
  69.        }  
  70.     }  
  71.    
  72.     public   void  leftClick(MouseEvent e) {  
  73.        balls.add(new  Ball(getMouseX(), getMouseY(), ballWidth));  
  74.     }  
  75.    
  76.     public   void  middleClick(MouseEvent e) {  
  77.    
  78.     }  
  79.    
  80.     public   void  rightClick(MouseEvent e) {  
  81.    
  82.     }  
  83.    
  84.     public   void  onKey(KeyEvent e) {  
  85.    
  86.     }  
  87.    
  88.     public   void  onKeyUp(KeyEvent e) {  
  89.    
  90.     }  
  91.    
  92.     public   static   void  main(String[] args) {  
  93.        GameScene frame = new  GameScene( "球体下落"480360 );  
  94.        Deploy deploy = frame.getDeploy();  
  95.        deploy.setScreen(new  ProcessingBall());  
  96.        deploy.setShowFPS(true );  
  97.        deploy.setFPS(100 );  
  98.        deploy.mainLoop();  
  99.        frame.showFrame();  
  100.     }  
  101.    
  102. }  


00


二、 新生代的 PulpCore

 

项目地址: http://www.interactivepulp.com/pulpcore/

 

事实上, PulpCore 在国外的 Java 圈中也算颇有名气,甚至连某位 JavaFX 开发者都曾以它和自己的项目作过比较。如果有朋友泡过 http://www.javagaming.org/ ,想必应该知道,如果你在该论坛中寻求 Java 游戏框架,那么 3D 方面的优先推荐必然是 JME 2D 方面的优先推荐绝对是 Slick2D ,至于网页游戏开发方面,则必属 PulpCore 无疑。

 

在以 OpenGL 为绝对主流的 javagaming 上,一款以标准 Java2D 开发的框架,居然会受到如此推崇, PulpCore 的技术价值我们可想而知。

 

下图为 PulpCore 提供的应用示例:


00


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 的替代封装,以方便后文讲解分析其渲染方式之用。


00


示例源码:

 

  1. public   class  Test  extends  Screen {  
  2.      
  3.     class  Ball {  
  4.    
  5.        private  CoreImage image = CoreImage.loadImage( "ball.png" );  
  6.    
  7.        private   int  x, y, size;  
  8.    
  9.        protected   int  vx, vy;  
  10.    
  11.        public  Ball( int  x,  int  y,  int  vx,  int  vy) {  
  12.            this .size = image.getWidth();  
  13.            this .x = x;  
  14.            this .y = y;  
  15.            this .vx = vx;  
  16.            this .vy = vy;  
  17.        }  
  18.    
  19.        public   void  move() {  
  20.            x += vx;  
  21.            y += vy;  
  22.            if  (x <  0  || x > getWidth() - size) {  
  23.               vx = -vx;  
  24.            }  
  25.            if  (y <  0  || y > getHeight() - size) {  
  26.               vy = -vy;  
  27.            }  
  28.        }  
  29.    
  30.        public   void  draw(CoreGraphics g) {  
  31.            g.drawImage(image, x, y);  
  32.        }  
  33.    
  34.     }  
  35.    
  36.     private   static   final   int  NUM_BALLS =  99 ;  
  37.    
  38.     private  Random rand;  
  39.    
  40.     private  Ball[] ball;  
  41.    
  42.     public  Test() {  
  43.        // 以当前毫秒生成随机数   
  44.        rand = new  Random(System.currentTimeMillis());  
  45.        ball = new  Ball[NUM_BALLS];  
  46.        // 初始化球体参数   
  47.        for  ( int  i =  0 ; i < NUM_BALLS; i++) {  
  48.            int  x = rand.nextInt(getWidth());  
  49.            int  y = rand.nextInt(getHeight());  
  50.            int  vx = rand.nextInt( 10 );  
  51.            int  vy = rand.nextInt( 10 );  
  52.            ball[i] = new  Ball(x, y, vx, vy);  
  53.        }  
  54.     }  
  55.    
  56.     public   void  draw(CoreGraphics g) {  
  57.        for  ( int  i =  0 ; i < NUM_BALLS; i++) {  
  58.            ball[i].move();  
  59.            ball[i].draw(g);  
  60.        }  
  61.     }  
  62.    
  63.     public   void  leftClick(MouseEvent e) {  
  64.    
  65.     }  
  66.    
  67.     public   void  middleClick(MouseEvent e) {  
  68.    
  69.     }  
  70.    
  71.     public   void  onKey(KeyEvent e) {  
  72.    
  73.     }  
  74.    
  75.     public   void  onKeyUp(KeyEvent e) {  
  76.    
  77.     }  
  78.    
  79.     public   void  rightClick(MouseEvent e) {  
  80.    
  81.     }  
  82.    
  83.     public   static   void  main(String[] args) {  
  84.        GameScene frame = new  GameScene( "多球体渲染"480360 );  
  85.        Deploy deploy = frame.getDeploy();  
  86.        deploy.setScreen(new  Test());  
  87.        deploy.setShowFPS(true );  
  88.        deploy.setFPS(100 );  
  89.        deploy.mainLoop();  
  90.        frame.showFrame();  
  91.     }  
  92.    
  93. }  
 

 

另外笔者还要补充一点,那就是 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 好赚……

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值