Acelerar la generación de texturas procesales

14

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:

ingrese la descripción de la imagen aquí

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; 
}
FalconNL
fuente
¿Podría publicar lo que tiene actualmente en Java para su función de ruido? No digo que existan ganancias de rendimiento, pero alguien podría detectar algo para darle un impulso.
Darren Reid el
He agregado el código que estoy usando a la publicación original.
FalconNL
No está relacionado con su Q per se, pero debe llamar a dispose () en su pixmap después de que haya terminado con él.
junkdog

Respuestas:

10

Puede combinar los enfoques (2) y (3) de esta manera:

  • Primero, use GPU para generar una serie de texturas de ruido y guardarlas. Este será su "caché de ruido"; solo puedes hacerlo una vez en la primera ejecución.
  • Para generar una textura en el juego, combina algunas texturas del caché; esto debería ser muy rápido. Luego, si es necesario, agregue efectos especiales como vórtices encima de eso.
  • Alternativamente, también puede generar previamente algunas texturas de "efectos especiales", y simplemente mezclarlas para obtener el resultado final.
No importa
fuente
+1 Creo que generar un montón de texturas y empaquetarlas con el juego para combinar o aplicar efectos simples sería la mejor manera de hacerlo.
TheNickmaster21
2

La generación de textura de procedimiento es ab * * de un mofo en términos de tiempo de cálculo. Es lo que es.

La mejor implementación de Simplex Noise que he encontrado es la de Stefan Gustavson .

Más allá de la mejora del tiempo de cálculo real (en realidad es bastante difícil superar el hecho de que simplemente está pidiendo mucho de su computadora cuando calcula 1024x1024 texturas de procedimiento), una de las mejores maneras de reducir la percepción tiempo de espera es tener su aplicación realiza la mayor cantidad de trabajo de fondo posible.

Así que comienza a generar texturas en el lanzamiento del juego en el hilo de fondo , mientras el usuario todavía está jugando con las opciones y el menú o viendo el avance de nivel.

La otra cosa a considerar es simplemente almacenar en caché varios cientos de texturas generadas en el disco y seleccionar aleatoriamente una de ellas en el momento de la carga. Más disco, pero menos tiempo de carga.

bobobobo
fuente