Recientemente comencé a trabajar en un juego que tiene lugar en un sistema solar generado por procedimientos. Después de un poco de una curva de aprendizaje (sin haber trabajado con Scala, OpenGL 2 ES o Libgdx antes), tengo una demostración de tecnología básica en la que giras alrededor de un solo planeta con textura de procedimiento:
El problema con el que me encuentro es el rendimiento de la generación de texturas. Una descripción rápida de lo que estoy haciendo: un planeta es un cubo que se ha deformado en una esfera. A cada lado, se aplica textura ansn (por ejemplo, 256 x 256), que se agrupan en una textura 8n xn que se envía al sombreador de fragmentos. Los dos últimos espacios no se usan, solo están allí para asegurarse de que el ancho sea una potencia de 2. La textura se genera actualmente en la CPU, utilizando la versión actualizada de 2012 del algoritmo de ruido simple vinculado en el documento 'Simplex ruido desmitificado. La escena que estoy usando para probar el algoritmo contiene dos esferas: el planeta y el fondo. Ambos utilizan una textura en escala de grises que consta de seis octavas de ruido simple 3D, por ejemplo, si elegimos 128x128 como el tamaño de la textura, hay 128 x 128 x 6 x 2 x 6 = aproximadamente 1.2 millones de llamadas a la función de ruido.
Lo más cerca que estarás del planeta es lo que se muestra en la captura de pantalla y, dado que la resolución objetivo del juego es 1280x720, eso significa que preferiría usar texturas 512x512. Combine eso con el hecho de que las texturas reales serán, por supuesto, más complicadas que el ruido básico (habrá una textura diurna y nocturna, mezclada en el sombreador de fragmentos basado en la luz solar y una máscara especular. Necesito ruido para los continentes, la variación del color del terreno , nubes, luces de la ciudad, etc.) y estamos viendo algo así como 512 x 512 x 6 x 3 x 15 = 70 millones de llamadas de ruido solo para el planeta. En el juego final, habrá actividades al viajar entre planetas, por lo que una espera de 5 o 10 segundos, posiblemente 20, sería aceptable ya que puedo calcular la textura en el fondo mientras viajo, aunque obviamente cuanto más rápido mejor.
Volviendo a nuestra escena de prueba, el rendimiento en mi PC no es demasiado terrible, aunque sigue siendo demasiado lento teniendo en cuenta que el resultado final será aproximadamente 60 veces peor:
128x128 : 0.1s
256x256 : 0.4s
512x512 : 1.7s
Esto es después de que moví todo el código crítico de rendimiento a Java, ya que intentar hacerlo en Scala fue mucho peor. Sin embargo, ejecutar esto en mi teléfono (un Samsung Galaxy S3) produce un resultado más problemático:
128x128 : 2s
256x256 : 7s
512x512 : 29s
Ya es demasiado tiempo, y eso ni siquiera tiene en cuenta el hecho de que serán minutos en lugar de segundos en la versión final. Claramente hay que hacer algo. Personalmente, veo algunas vías potenciales, aunque todavía no estoy particularmente interesado en ninguna de ellas:
- No precalcules las texturas, pero deja que el sombreador de fragmentos calcule todo. Probablemente no sea factible, porque en un momento tenía el fondo como un quad de pantalla completa con un sombreador de píxeles y obtuve aproximadamente 1 fps en mi teléfono.
- Use la GPU para representar la textura una vez, almacénela y use la textura almacenada a partir de ese momento. Upside: podría ser más rápido que hacerlo en la CPU ya que se supone que la GPU es más rápida en los cálculos de coma flotante. Desventaja: los efectos que no pueden (fácilmente) expresarse como funciones de ruido simple (por ejemplo, vórtices de planetas gaseosos, cráteres lunares, etc.) son mucho más difíciles de codificar en GLSL que en Scala / Java.
- Calcule una gran cantidad de texturas de ruido y envíelas con la aplicación. Me gustaría evitar esto si es posible.
- Bajar la resolución. Me compra una ganancia de rendimiento 4x, que en realidad no es suficiente, además pierdo mucha calidad.
- Encuentra un algoritmo de ruido más rápido. Si alguien tiene uno, soy todo oídos, pero se supone que simplex es más rápido que Perlin.
- Adopte un estilo de pixel art, permitiendo texturas de menor resolución y menos octavas de ruido. Aunque originalmente imaginé el juego en este estilo, he llegado a preferir el enfoque realista.
- Estoy haciendo algo mal y el rendimiento ya debería ser uno o dos órdenes de magnitud mejor. Si este es el caso, hágamelo saber.
Si alguien tiene alguna sugerencia, consejo, solución u otro comentario sobre este problema, me encantaría escucharlo.
En respuesta a Layoric, aquí está el código que estoy usando:
//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;
}