最近,我开始研究在程序生成的太阳系中进行的游戏。经过一番学习之后(之前都没有使用Scala,OpenGL 2 ES或Libgdx进行过学习),我进行了一个基本的技术演示,您将围绕一个程序化纹理的行星旋转:
我遇到的问题是纹理生成的性能。我正在做的事情的简要概述:行星是已变形为球体的立方体。每一面都应用anxn(例如256 x 256)纹理,该纹理捆绑在一个8n xn纹理中,并发送到片段着色器。最后两个空格未使用,它们仅用于确保宽度为2的幂。使用在'Simplex'论文中链接的2012年更新的单纯形噪声算法,当前正在CPU上生成纹理。噪音消失了。我用来测试算法的场景包含两个球体:行星和背景。两者都使用由六个八度的3D单工噪声组成的灰度纹理,因此,例如,如果我们选择128x128作为纹理大小,则有128 x 128 x 6 x 2 x 6 =约120万个噪声函数调用。
您最接近地球的是屏幕快照中显示的内容,并且由于游戏的目标分辨率为1280x720,这意味着我希望使用512x512纹理。结合这样的事实,实际的纹理当然比基本噪声要复杂(白天和黑夜的纹理,将基于阳光和镜面反射的片段着色器混合在一起。我需要大洲的噪声,地形颜色变化,云层,城市的灯光等),而我们正在寻找的是512 x 512 x 6 x 3 x 15 =仅此星球就需要7,000万个噪声的东西。在最终游戏中,行星之间旅行时会有活动,所以等待5或10秒(可能是20秒)是可以接受的,因为我可以在旅行时计算背景纹理,尽管显然速度越快越好。
回到我们的测试场景,我的PC上的性能并不是太糟糕,尽管考虑到最终结果将比原来差60倍,但仍然太慢了:
128x128 : 0.1s
256x256 : 0.4s
512x512 : 1.7s
这是在我将所有对性能至关重要的代码移至Java之后的事情,因为在Scala中尝试这样做要差得多。但是,在我的手机(三星Galaxy S3)上运行它会产生更成问题的结果:
128x128 : 2s
256x256 : 7s
512x512 : 29s
已经太长了,这甚至没有考虑到最终版本将是分钟而不是秒的事实。显然需要做些事情。就个人而言,我看到了一些潜在的途径,尽管我并不特别热衷于其中任何一种:
- 不要预先计算纹理,而是让片段着色器计算所有内容。可能不可行,因为有一次我将背景作为具有像素着色器的全屏四边形,并且手机上的速度约为1 fps。
- 使用GPU渲染一次纹理,然后将其存储并使用存储的纹理。上行空间:可能比在CPU上执行速度更快,因为在浮点计算中应该认为GPU会更快。缺点:在GLSL中,比起Scala / Java,难以(简单地)表达为单纯噪声功能(例如,气体行星涡,月球坑等)的效果要困难得多。
- 计算大量的噪声纹理并将其随应用程序一起提供。我想尽可能避免这种情况。
- 降低分辨率。给我买了4倍的性能提升,这还远远不够,而且我失去了很多质量。
- 查找更快的噪声算法。如果有人有一个,我将不知所措,但是单纯形应该已经比perlin更快。
- 采用像素艺术风格,允许使用较低分辨率的纹理和较少的噪声八度。当我最初以这种风格设想游戏时,我开始偏爱逼真的方法。
- 我做错了,性能应该已经提高了一个或两个数量级。如果是这种情况,请告诉我。
如果有人对这个问题有任何建议,技巧,解决方法或其他评论,我希望听到他们的声音。
为了回应Layoric,以下是我正在使用的代码:
//The function that generates the simplex noise texture
public static Texture simplex(int size) {
byte[] data = new byte[size * size * columns * 4];
int offset = 0;
for (int y = 0; y < size; y++) {
for (int s = 0; s < columns; s++) {
for (int x = 0; x < size; x++) {
//Scale x and y to [-1,1] range
double tx = ((double)x / (size - 1)) * 2 - 1;
double ty = 1 - ((double)y / (size - 1)) * 2;
//Determine point on cube in worldspace
double cx = 0, cy = 0, cz = 0;
if (s == 0) { cx = 1; cy = tx; cz = ty; }
else if (s == 1) { cx = -tx; cy = 1; cz = ty; }
else if (s == 2) { cx = - 1; cy = -tx; cz = ty; }
else if (s == 3) { cx = tx; cy = - 1; cz = ty; }
else if (s == 4) { cx = -ty; cy = tx; cz = 1; }
else if (s == 5) { cx = ty; cy = tx; cz = - 1; }
//Determine point on sphere in worldspace
double sx = cx * Math.sqrt(1 - cy*cy/2 - cz*cz/2 + cy*cy*cz*cz/3);
double sy = cy * Math.sqrt(1 - cz*cz/2 - cx*cx/2 + cz*cz*cx*cx/3);
double sz = cz * Math.sqrt(1 - cx*cx/2 - cy*cy/2 + cx*cx*cy*cy/3);
//Generate 6 octaves of noise
float gray = (float)(SimplexNoise.fbm(6, sx, sy, sz, 8) / 2 + 0.5);
//Set components of the current pixel
data[offset ] = (byte)(gray * 255);
data[offset + 1] = (byte)(gray * 255);
data[offset + 2] = (byte)(gray * 255);
data[offset + 3] = (byte)(255);
//Move to the next pixel
offset += 4;
}
}
}
Pixmap pixmap = new Pixmap(columns * size, size, Pixmap.Format.RGBA8888);
pixmap.getPixels().put(data).position(0);
Texture texture = new Texture(pixmap, true);
texture.setFilter(TextureFilter.Linear, TextureFilter.Linear);
return texture;
}
//SimplexNoise.fbm
//The noise function is the same one found in http://webstaff.itn.liu.se/~stegu/simplexnoise/SimplexNoise.java
//the only modification being that I replaced the 32 in the last line with 16 in order to end up with
//noise in the range [-0.5, 0.5] instead of [-1,1]
public static double fbm(int octaves, double x, double y, double z, double frequency) {
double value = 0;
double f = frequency;
double amp = 1;
for (int i = 0; i < octaves; i++) {
value += noise(x*f, y*f, z*f) * amp;
f *= 2;
amp /= 2;
}
return value;
}