5. 图像模块(Graphics)
最后一个模块是图像操作模块,用来绘制图像到屏幕上。不过要想高性能的绘制图像,就不得不了解一些基本的图像编程知识。让我们从绘制2D图像开始,首先要了解的一个问题是:图像究竟是如何绘制到屏幕的?答案相当复杂,我们不需要知道所有的细节。
光栅、像素和帧缓冲(Framebuffers)
现在的显示器都是基于光栅的,光栅是一个两维度的格子组成,也就是像素格。光栅格子的长宽,我们一般用像素来表示。如果仔细观察显示器(或者用放大镜),我们就可以发现显示器上面有一个一个的格子,这就是像素格或者光栅格。每个像素的位置可以用坐标表示,于是引入了二维坐标系统,这也意味着坐标值是整数。显示器源源不断地收到从图形处理器传过来的图像流,解码每个像素的颜色(程序或者操作系统设定),然后绘制到屏幕上。每秒钟显示器会进行多次刷新,刷新频率单位是Hz,比如LCD显示器主流刷新率是85Hz。
图形处理器需要从一个特殊的存储区域获取像素信息以便显示在显示器上,这个区域就叫做视频内存区,或者叫VRAM。这个区域一般称作帧缓冲区(framebuffer)。因此一个完整的屏幕图形叫做一个帧。对于每个显示器栅格中的像素,在帧缓冲区都有一个对应的内存地址。当我们需要改变屏幕显示内容时,我们只需要简单地改变帧缓冲区中的内容即可。
下图是显示器栅格和帧缓冲区的简单示意图:
垂直同步和双缓冲
普通的绘图方法,当要绘制的对象太复杂,尤其是含有位图时,这时的画面会显示的很慢,对于运动的画面,会给人“卡”住了的感觉,有时候还会导致画面闪烁。于是我们采用双缓冲技术(采用两个framebuffer)。 双缓冲的原理可以这样形象的理解:把电脑屏幕看作一块黑板。首先我们在内存环境中建立一个“虚拟“的黑板,然后在这块黑板上绘制复杂的图形,等图形全部绘制完毕的时候,再一次性的把内存中绘制好的图形“拷贝”到另一块黑板(屏幕)上。采取这种方法可以提高绘图速度,极大的改善绘图效果。下面是原理图:
要知道什么是垂直同步,必须要先明白显示器的工作原理。显示器上的所有图像都是一线一线的扫描上去的,无论是隔行扫描还是逐行扫描,显示器,都有2种同步参数——水平同步和垂直同步。水平同步信号决定了CRT画出一条横越屏幕线的时间,垂直同步信号决定了CRT从屏幕顶部画到底部,再返回原始位置的时间,而恰恰是垂直同步代表着CRT显示器的刷新率水平!
关闭垂直同步:我们平时运行操作系统一般屏幕刷新率一般都是在85Hz上下,此时显卡就会每按照85Hz的频率时间来发送一个垂直同步信号,信号和信号的时间间隔是85的分辨率所写一屏图像时间。
打开垂直同步:在游戏中,或许强劲的显卡迅速的绘制完一屏的图像,但是没有垂直同步信号的到达,显卡无法绘制下一屏,只有等85单位的信号到达,才可以绘制。这样fps自然要受到操作系统刷新率运行值的制约。也就是说,当然打开后如果你的游戏画面FPS数能达到或超过你显示器的刷新率,这时你的游戏画面FPS数被限制为你显示器的刷新率。如果达不到会出现不同程度的跳帧现象,FPS与刷新率差距越大跳帧越严重。一般对于高性能的显卡建议打卡,游戏画面会更好!打开后能防止游戏画面高速移动时画面撕裂现象,比如实况足球等。
关闭垂直同步,那么游戏中作完一屏画面,显卡和显示器无需等待垂直同步信号,就可以开始下一屏图像的绘制,自然可以完全发挥显卡的实力。
但是,不要忘记,正是因为垂直同步的存在,才能使得游戏进程和显示器刷新率同步,使得画面平滑,使得画面稳定。取消了垂直同步信号,固然可以换来更快的速度,但是在图像的连续性上,性能势必打折扣。这也是关闭垂直同步后发现画面不连续的理论原因!
图像格式
比较流行的两个图形格式是JPEG和 PNG。JPEG是有损压缩格式,PNG是无损压缩格式,因此PNG格式可以百分百重现原始的图像。有损压缩格式通常占用少的磁盘空间。我们采用何总压缩格式取决于我们的磁盘空间。和音频类似,当我们加载到内存中时,我们需要完全地解压一个图像。因此,即使你的压缩图像在磁盘上只有20K,在RAM中你依然需要width×height ×color depth的存储空间。
图像叠加
假定有一个我们可以渲染的帧缓冲区(framebuffer),同时有几个加载到RAM中的图片,我们笑需要把RAM中的图片逐次放入到帧缓冲区,比如一个背景图片和一个前景图片如图所示:
这个过程就叫做图像的合成和叠加,我们需要把不同的图片合成一个最终显示的图片。绘制图片的此项很重要,因为上面的图片总会覆盖下面的图片。
上面图像合成出现了问题:第二张图片的白色背景覆盖了第一张背景图片。我们怎样把第二张图的白色背景消去呢?这就需要alpha混合(alpha blending)。alpha混合是一种把源点的颜色值和目标点的颜色值按照一定的算法进行运算,得到一种透明的效果。
下面是最终合成图像的RGB值,公式如下
red = src.red * src.alpha + dst.red * (1 – src.alpha) blue = src.green * src.alpha + dst.green * (1 – src.alpha) green = src.blue * src.alpha + dst.blue * (1 – src.alpha)
src和dst分别是我们需要混合的源图像和目标图像(源图像相当于人物,目标图像相当于背景)。下面是一个例子。
src = (1, 0.5, 0.5), src.alpha = 0.5, dst = (0, 1, 0) red = 1 * 0.5 + 0 * (1 – 0.5) = 0.5 blue = 0.5 * 0.5 + 1 * (1 – 0.5) = 0.75 red = 0.5 * 0.5 + 0 * (1 – 0.5) = 0.25
效果如下图所示
上述公式用了两次乘法,乘法消耗的时间多,为了提高运算速度,可以进行优化。如
red = (src.red- dst.red) * src.alpha + dst.red
Alpha是一个浮点数,我们可以转换成整数运算,因为一种颜色最多占8Bit,所以Alpha值最多是256,于是我们把Alpha的值乘以256,然后运算的时候再除以256,就得到下面的公式:
red = (src.red- dst.red) * src.alpha /256+ dst.red
这里,Alpha是一个0到256的数值。
具体到这个例子,我们只需要把源文件的白色像素的alpha值设为0即可。最终效果如下图:
图像模块的接口代码
通过以上介绍,我们可以开始设计我们的图像模块的接口。问下需要实现如下功能:
- 从磁盘加载图片到内存中,为以后绘制到屏幕做准备。
- 用特定颜色清除framebuffer
- 用指定颜色在framebuffer指定位置绘制像素。
- 在framebuffer上绘制线条和矩形。
- 绘制上面内存中的图片到framebuffer,能够整个绘制和部分绘制,alpha混合绘制。
- 得到framebuffer的长宽。
这里用两个接口来实现:Graphics和 Pixmap,下面是Graphics接口:
package com.badlogic.androidgames.framework;
public interface Graphics {
public static enum PixmapFormat {
ARGB8888, ARGB4444, RGB565
}
public Pixmap newPixmap(String fileName, PixmapFormat format);
public void clear(int color);
public void drawPixel(int x, int y, int color);
public void drawLine(int x, int y, int x2, int y2, int color);
public void drawRect(int x, int y, int width, int height, int color);
public void drawPixmap(Pixmap pixmap, int x, int y, int srcX, int srcY, int srcWidth, int srcHeight);
public void drawPixmap(Pixmap pixmap, int x, int y);
public int getWidth();
public int getHeight();
}
枚举 PixmapFormat保存了该游戏支持的像素的颜色值(包括透明度)。比如ARGB8888,A表示透明度,R表示红色,G表示绿色,B表示蓝色,他们非别用8位来表示,就是各有256种状态。 接下来看下接口的方法:
- Graphics.newPixmap()方法加载指定格式的图片。
- Graphics.clear()方法用特定颜色清除framebuffer。
- Graphics.drawPixel()方法在framebuffer中指定位置绘制给定颜色的像素。
- Graphics.drawLine()和 Graphics.drawRect()方法在framebuffer绘制线条和矩形。
- Graphics.drawPixmap()方法绘制图像的到framebuffer。(x, y) 坐标指定了framebuffer绘制的起始位置,参数 srcX和srcY指定了图片被绘制部分的起始位置。srcWidth和srcHeight制定了绘制的宽度和高度。
- Graphics.getWidth()和Graphics.getHeight()方法返回framebuffer的宽度和高度。
//Pixmap接口
package com.badlogic.androidgames.framework;
import com.badlogic.androidgames.framework.Graphics.PixmapFormat;
public interface Pixmap {
public int getWidth();
public int getHeight();
public PixmapFormat getFormat();
public void dispose();
}
- Pixmap.getWidth()和Pixmap.getHeight()方法返回图像的宽度和高度。
- Pixmap.getFormat()返回图片的格式。
- Pixmap.dispose()方法。Pixmap实例使用内存资源和其他潜在的系统资源,如果我们不在需要它,我们需要回收资源,这也该方法的作用。
PS: 欢迎关注公众号"Devin说",会不定期更新Java相关技术知识。