我正在制作厄运风格的伪3D游戏。 将世界逐像素渲染到缓冲的图像中,然后将其显示在JPanel上。我想保留这种方法,以便照亮单个像素。
我希望能够将游戏中的纹理着色为许多不同的颜色。 着色整个纹理并将其存储在单独的缓冲图像中会占用我太多时间和内存。因此,我在渲染阶段为纹理的每个像素着色。
我遇到的问题是给每个像素着色非常昂贵。当无色的墙壁覆盖整个屏幕时,我得到约65 fps。当彩色墙壁覆盖屏幕时,我得到30 fps。
这是我为像素着色的功能:
//Change the color of the pixel using its brightness.
public static int tintABGRPixel(int pixelColor, Color tintColor) {
//Calculate the luminance. The decimal values are pre-determined.
double lum = ((pixelColor>>16 & 0xff) * 0.2126 +
(pixelColor>>8 & 0xff) * 0.7152 +
(pixelColor & 0xff) * 0.0722) / 255;
//Calculate the new tinted color of the pixel and return it.
return ((pixelColor>>24 & 0xff) << 24) |
((int)(tintColor.getBlue()*lum) & 0xff) |
(((int)(tintColor.getGreen()*lum) & 0xff) << 8) |
(((int)(tintColor.getRed()*lum) & 0xff) << 16);
}
对不起,密码难以辨认。此函数计算原始像素的亮度,将新颜色乘以亮度,然后将其转换回int。
它仅包含简单的操作,但在最坏的情况下,每帧最多调用此功能一百万次。瓶颈是return语句中的计算。
有没有更有效的方法来计算新颜色? 如果我改变方法,那会是最好的吗?
谢谢
答案 0 :(得分:2)
线程不一定是并行化代码的唯一方法,cpus通常具有诸如SIMD之类的指令集,该指令集可让您一次对多个数字计算相同的算术。 GPU采纳了这一想法并将其运行,从而使您可以在数百至数千个数字上并行运行相同的功能。我不知道如何在Java中执行此操作,但是我确信可以通过某种搜索找到可行的方法。
是否可以减少需要调用该函数的时间?每帧调用任何功能一百万次会很痛苦。除非管理每个函数调用的开销(内联它,重用堆栈帧,尽可能缓存结果),否则您将希望减少工作量。
可能的选项可能是:
如果您可以选择“近似色”,则this SO answer给出应该更便宜地计算的像素亮度(lum)的近似值。 (链接中的公式为Y = 0.33 R + 0.5 G + 0.16 B,可以写成Y =(R + R + B + G + G + G)/ 6。)
下一步是评估您的代码(配置文件是了解Google信息的一个好名词),并查看占用了最多资源的资源。很有可能不是这里的功能,而是另一段代码。或等待纹理加载。
从现在开始,我们将假设问题中提供的功能占用的时间最多。让我们看看它在花时间在做什么。我没有剩下的代码,因此无法对其进行基准测试,但可以编译它并查看生成的字节码。在包含该函数的类上使用javap,我得到以下信息(字节码已被剪切,出现重复的地方)。
public static int tintABGRPixel(int, Color);
Code:
0: iload_0
1: bipush 16
3: ishr
4: sipush 255
7: iand
8: i2d
9: ldc2_w #2 // double 0.2126d
12: dmul
13: iload_0
...
37: dadd
38: ldc2_w #8 // double 255.0d
41: ddiv
42: dstore_2
43: iload_0
44: bipush 24
46: ishr
47: sipush 255
50: iand
51: bipush 24
53: ishl
54: aload_1
55: pop
56: invokestatic #10 // Method Color.getBlue:()I
59: i2d
60: dload_2
61: dmul
62: d2i
63: sipush 255
66: iand
67: ior
68: aload_1
69: pop
...
102: ireturn
乍一看,这看起来很吓人,但是Java字节码很好,因为您可以将每行(或指令)匹配到函数中的某个点。它并没有做任何疯狂的事情,例如将其重写或矢量化,或者使其无法识别的任何事情。
查看更改是否有所改进的通用方法是前后测量代码。有了这些知识,您就可以决定更改是否值得保留。一旦性能足够好,就停止。
我们的穷人档案是查看每条指令,并查看(根据在线资源,平均而言)它有多昂贵。这有点天真,因为每条指令执行所需的时间可能取决于很多因素,例如它所运行的硬件,计算机上的软件版本以及围绕它的指令。
我没有每条指令的时间成本的完整列表,因此我将尝试一些启发式方法。
我盯着字节码一会儿,我注意到的是从第8-42行开始,有很多浮点运算。这部分代码计算出lum(亮度)。除此之外,其他任何事情都没有脱颖而出,所以让我们在记住我们的第一个启发式方法的情况下重写代码。如果您不在乎解释,我将在最后提供最终代码。
让我们考虑一下函数结尾处的蓝色(我们将其标记为B)是什么。更改也将适用于红色和绿色,但是为了简洁起见,我们将其省略。
double lum = ((pixelColor>>16 & 0xff) * 0.2126 +
(pixelColor>>8 & 0xff) * 0.7152 +
(pixelColor & 0xff) * 0.0722) / 255;
...
... | ((int)(tintColor.getBlue()*lum) & 0xff) | ...
这可以重写为 int x =(pixelColor >> 16&0xff),y =(pixelColor >> 8&0xff),z =(pixelColor&0xff); 两倍a = 0.2126,b = 0.7152,c = 0.0722; double lum =(a x + b y + c * z)/ 255; int B =(int)(tintColor.getBlue()* lum)&0xff;
我们不想做那么多的浮点运算,所以让我们做一些分解。这个想法是0.2126可以写为2126/10000。
int x = (pixelColor>>16 & 0xff), y = (pixelColor>>8 & 0xff), z = (pixelColor & 0xff);
int a = 2126, b = 7152, c = 722;
int top = a*x + b*y + c*z;
double temp = (double)(tintColor.getBlue() * top) / 10000 / 255;
int B = (int)temp & 0xff;
因此,我们现在执行三个整数乘法(imul)而不是三个dmul。成本是一个额外的浮动部门,仅此一点可能是不值得的。但是我们可以通过组合两个顺序除法来解决此问题。我们还可以通过将强制转换和除法移动到一行来设置代码以进行更多优化。
int x = (pixelColor>>16 & 0xff), y = (pixelColor>>8 & 0xff), z = (pixelColor & 0xff);
int a = 2126, b = 7152, c = 722;
int top = a*x + b*y + c*z);
int temp = (int)((double)(tintColor.getBlue()*top) / 2550000);
int B = temp & 0xff;
这可能是个好地方。但是,如果您需要从该函数中获得更多性能,我们可以优化除以常数,然后将double转换为int(我认为这是两个昂贵的运算),再将其转换为乘法(长整)和a移。
int x = (pixelColor>>16 & 0xff), y = (pixelColor>>8 & 0xff), z = (pixelColor & 0xff);
int a = 2126, b = 7152, c = 722;
int top = a*x + b*y + c*z;
int Btemp = (int)(( * top * 1766117501L) >> 52);
int B = temp & 0xff;
其中的幻数是当我使用clang编译代码的c ++版本时被幻化的两个。我无法解释如何产生这种魔力,但据我用x,y,z和tintColor.getBlue()的几个值测试过后,它仍然有效。在测试时,我假设所有值都在0到256之间,并且我仅尝试了几个示例。
最终代码如下。请注意,这没有经过良好测试,并且可能遗漏了一些遗漏的情况,因此请告知是否存在任何错误。希望它足够快。
public static int tintABGRPixel(int pixelColor, Color tintColor) {
//Calculate the luminance. The decimal values are pre-determined.
int x = pixelColor>>16 & 0xff, y = pixelColor>>8 & 0xff, z = pixelColor & 0xff;
int top = 2126*x + 7252*y + 722*z;
int Btemp = (int)((tintColor.getBlue() * top * 1766117501L) >> 52);
int Gtemp = (int)((tintColor.getGreen() * top * 1766117501L) >> 52);
int Rtemp = (int)((tintColor.getRed() * top * 1766117501L) >> 52);
//Calculate the new tinted color of the pixel and return it.
return ((pixelColor>>24 & 0xff) << 24) | Btemp & 0xff | (Gtemp & 0xff) << 8 | (Rtemp & 0xff) << 16;
}
答案 1 :(得分:1)
要获得更好的性能,您必须在图像处理过程中摆脱Color
之类的对象,如果您知道某个方法被称为百万次(image.width * image.height
次),那么最好内联此方法。通常,JVM可能会内联此方法本身,但是您不应该冒险。
您可以使用PixelGrabber将所有像素放入阵列中。这是一般用法
final int[] pixels = new int[width * height];
final PixelGrabber pixelgrabber = new PixelGrabber(image, 0, 0, width, height, pixels, 0, 0);
for(int i = 0; i < height; i++) {
for(int j = 0; j < width; j++) {
int p = pixels[i * width + j]; // same as image.getRGB(j, i);
int alpha = ( ( p >> 24) & 0xff );
int red = ( ( p >> 16) & 0xff );
int green = ( ( p >> 8) & 0xff );
int blue = ( p & 0xff );
//do something i.e. apply luminance
}
}
以上只是如何迭代行和列索引的示例,但是在您的情况下,不需要嵌套循环。这样可以合理地提高性能。
使用Java 8流也可以很容易地将其并行化,但是在处理图像时使用流要小心,因为流比普通的旧循环慢得多。
您也可以在适用的情况下尝试用int
替换byte
(即,不需要将各个颜色分量存储在int
中)。基本上尝试使用原始数据类型,甚至在原始数据类型中也使用适用的最小数据类型。
答案 2 :(得分:0)
在这一点上,您实际上已经接近金属。我认为您必须更改方法才能真正改善性能,但是一个快速的主意是缓存lum计算。这是像素颜色的简单功能,您的光明不依赖于任何其他东西。如果您缓存它,可以节省大量计算。缓存时,您也可以缓存此calc:
((pixelColor>>24 & 0xff) << 24)
我不知道这样是否可以为您节省大量时间,但是从现在看来,从微优化的角度来看,这几乎就是您可以做的一切。
现在,您可以重构像素循环以使用并行性,并在CPU上并行执行这些像素计算,这也可能会为您提供下一个想法。
如果以上两种想法均无效,我认为您可能需要尝试将颜色计算推到GPU卡上。这是数以百万计的所有裸机数学运算,这是图形卡最出色的表现。不幸的是,这是一个很深的话题,为了选择最佳选择,必须进行大量的教育。这里有一些有趣的事情需要研究:
我知道其中一些是巨大的框架,不是您所要求的。但是它们可能包含其他相对未知的库,您可以使用这些库将这些数学计算推送到GPU。 @Parrallel批注看起来可能是最有用的JavaCL绑定。