¿Cómo puedo mejorar las velocidades de renderizado de un juego tipo Voxel / Minecraft?

35

Estoy escribiendo mi propio clon de Minecraft (también escrito en Java). Funciona muy bien en este momento. Con una distancia de visualización de 40 metros, puedo alcanzar fácilmente 60 FPS en mi MacBook Pro 8,1. (Intel i5 + Intel HD Graphics 3000). Pero si pongo la distancia de visión en 70 metros, solo alcanzo 15-25 FPS. En el Minecraft real, puedo poner la distancia de visualización en lejos (= 256 m) sin ningún problema. Entonces, mi pregunta es ¿qué debo hacer para mejorar mi juego?

Las optimizaciones que implementé:

  • Solo mantenga fragmentos locales en la memoria (dependiendo de la distancia de visualización del jugador)
  • Recolección de Frustum (Primero en los trozos, luego en los bloques)
  • Solo dibujar caras realmente visibles de los bloques
  • Usar listas por fragmento que contienen los bloques visibles. Los fragmentos que se vuelven visibles se agregarán a esta lista. Si se vuelven invisibles, se eliminan automáticamente de esta lista. Los bloques se hacen (in) visibles al construir o destruir un bloque vecino.
  • Uso de listas por fragmento que contienen los bloques de actualización. Mismo mecanismo que las listas de bloqueo visibles.
  • Casi no use newdeclaraciones dentro del ciclo del juego. (Mi juego dura unos 20 segundos hasta que se invoca el recolector de basura)
  • Estoy usando listas de llamadas de OpenGL en este momento. ( glNewList(), glEndList(), glCallList()) Para cada lado de una especie de bloque.

Actualmente ni siquiera estoy usando ningún tipo de sistema de iluminación. Ya escuché sobre VBO's. Pero no sé exactamente qué es. Sin embargo, investigaré un poco sobre ellos. ¿Mejorarán el rendimiento? Antes de implementar VBO, quiero intentar usar glCallLists()y pasar una lista de listas de llamadas. En lugar de usar mil vecesglCallList() . (Quiero probar esto, porque creo que el MineCraft real no usa VBO. ¿Correcto?)

¿Hay otros trucos para mejorar el rendimiento?

El perfil de VisualVM me mostró esto (perfil de solo 33 cuadros, con una distancia de visión de 70 metros):

ingrese la descripción de la imagen aquí

Perfilado con 40 metros (246 cuadros):

ingrese la descripción de la imagen aquí

Nota: estoy sincronizando muchos métodos y bloques de código, porque estoy generando fragmentos en otro hilo. Creo que adquirir un bloqueo para un objeto es un problema de rendimiento al hacer esto en un bucle del juego (por supuesto, estoy hablando del momento en que solo hay un bucle del juego y no se generan nuevos fragmentos). ¿Es esto correcto?

Editar: después de eliminar algunos synchronisedbloques y algunas otras pequeñas mejoras. El rendimiento ya es mucho mejor. Aquí están mis nuevos resultados de perfil con 70 metros:

ingrese la descripción de la imagen aquí

Creo que está bastante claro que ese selectVisibleBlockses el problema aquí.

¡Gracias por adelantado!
Martijn

Actualización : después de algunas mejoras adicionales (como el uso de bucles for en lugar de para cada uno, almacenamiento de variables fuera de los bucles, etc.), ahora puedo ejecutar la distancia de visualización 60 bastante bien.

Creo que voy a implementar los VBO lo antes posible.

PD: todo el código fuente está disponible en GitHub:
https://github.com/mcourteaux/CraftMania

Martijn Courteaux
fuente
2
¿Puede darnos una foto de perfil a 40 m para que podamos ver qué podría estar aumentando más rápido que otro?
James
Tal vez demasiado especificado, pero si lo consideras, solo preguntar técnicas sobre cómo acelerar un juego en 3D, suena interesante. Pero el título puede asustar a las personas.
Gustavo Maciel
@Gtoknu: ¿Qué sugieres como título?
Martijn Courteaux
55
Dependiendo de a quién le preguntes, algunas personas dirán que Minecraft tampoco es tan rápido.
Thedaian
Creo que algo como "Qué técnicas pueden acelerar un juego en 3D" debería ser mucho mejor. Piensa en algo, pero trata de no usar la palabra "mejor" o intenta compararlo con algún otro juego. No podemos decir exactamente qué usan en algunos juegos.
Gustavo Maciel

Respuestas:

15

Menciona hacer sacrificios de bloques en bloques individuales; intente tirar eso. La mayoría de los fragmentos de representación deben ser completamente visibles o completamente invisibles.

Minecraft solo reconstruye una lista de visualización / búfer de vértices (no sé cuál usa) cuando se modifica un bloque en un fragmento determinado, y yo también . Si modifica la lista de visualización cada vez que cambia la vista, no obtendrá el beneficio de las listas de visualización.

Además, parece que estás usando fragmentos de altura mundial. Tenga en cuenta que Minecraft utiliza fragmentos cúbicos de 16 × 16 × 16 para sus listas de visualización, a diferencia de cargar y guardar. Si haces eso, hay aún menos razones para sacrificar trozos individuales.

(Nota: no he examinado el código de Minecraft. Toda esta información es un rumor o mis propias conclusiones al observar el renderizado de Minecraft mientras juego).


Consejos más generales:

Recuerde que su renderizado se ejecuta en dos procesadores: CPU y GPU. Cuando su velocidad de fotogramas es insuficiente, entonces uno u otro es el recurso limitante : su programa está vinculado a la CPU o a la GPU (suponiendo que no se intercambie o tenga problemas de programación).

Si su programa se ejecuta al 100% de la CPU (y no tiene otra tarea ilimitada que completar), entonces su CPU está haciendo demasiado trabajo. Debería intentar simplificar su tarea (por ejemplo, hacer menos sacrificios) a cambio de que la GPU haga más. Sospecho firmemente que este es su problema, dada su descripción.

Por otro lado, si la GPU es el límite (lamentablemente, por lo general no hay monitores de carga de 0% -100% convenientes), entonces debe pensar en cómo enviarle menos datos o requerir que llene menos píxeles.

Kevin Reid
fuente
2
Gran referencia, ¡tu investigación sobre ella mencionada en tu wiki fue muy útil para mí! +1
Gustavo Maciel
@OP: solo renderiza caras visibles (no bloques ). Un trozo patológico pero monótono de 16x16x16 tendrá casi 800 caras visibles, mientras que los bloques contenidos tendrán 24,000 caras visibles. Una vez que haya hecho eso, la respuesta de Kevin contiene las siguientes mejoras más importantes.
AndrewS
@ KevinReid Hay algunos programas para ayudar con la depuración de rendimiento. Por ejemplo, AMD GPU PerfStudio le dice si su CPU o GPU está unida y en la GPU qué componente está unido (textura vs fragmento vs vértice, etc.) Y estoy seguro de que Nvidia también tiene algo similar.
akaltar
3

¿Qué es lo que llama tanto a Vec3f.set? Si está construyendo lo que quiere renderizar desde cero cada cuadro, entonces definitivamente es allí donde querría comenzar a acelerarlo. No soy un gran usuario de OpenGL y no sé mucho sobre cómo se representa Minecraft, pero parece que las funciones matemáticas que estás usando te están matando en este momento (solo mira cuánto tiempo pasas en ellas y la cantidad de veces los llaman - muerte por mil cortes llamándolos).

Idealmente, su mundo estaría segmentado de modo que pueda agrupar cosas para renderizar juntas, construir objetos de búfer de vértices y reutilizarlos en múltiples cuadros. Solo necesitaría modificar un VBO si el mundo que representa cambia de alguna manera (como el usuario lo edita). Luego puede crear / destruir VBO para lo que está representando, ya que está dentro del rango de ser visible para mantener bajo el consumo de memoria, solo recibiría el golpe cuando se creó el VBO en lugar de cada cuadro.

Si el recuento de "invocación" es correcto en su perfil, está llamando a muchas cosas muchas veces. (10 millones de llamadas a Vec3f.set ... ¡ay!)

Roger Perkins
fuente
Yo uso este método para toneladas de cosas. Simplemente establece los tres valores para el vector. Esto es mucho mejor que asignar cada vez un nuevo objeto.
Martijn Courteaux
2

Mi descripción (de mi propia experimentación) aquí es aplicable:

Para el renderizado de vóxel, ¿qué es más eficiente: VBO prefabricado o un sombreador de geometría?

Minecraft y su código probablemente usan la tubería de función fija; mis propios esfuerzos han sido con GLSL pero la esencia es generalmente aplicable, creo:

(De memoria) Hice un tronco que era medio bloque más grande que el de la pantalla. Luego probé los puntos centrales de cada fragmento ( Minecraft tiene 16 * 16 * 128 bloques ).

Las caras en cada una se extiende en un VBO de matriz de elementos (muchas caras de fragmentos comparten el mismo VBO hasta que está 'lleno'; piense como malloc; aquellos con la misma textura en el mismo VBO donde sea posible) y los índices de vértice para el norte las caras, las caras sur, etc. son adyacentes en lugar de mixtas. Cuando dibujo, hago una glDrawRangeElementspara las caras norte, con la normal ya proyectada y normalizada, en uniforme. Luego hago las caras sur y así sucesivamente, para que las normales no estén en ningún VBO. Para cada fragmento, solo tengo que emitir las caras que serán visibles; solo aquellas en el centro de la pantalla necesitan dibujar los lados izquierdo y derecho, por ejemplo; Esto es simple GL_CULL_FACEa nivel de aplicación.

La mayor aceleración, iirc, fue eliminar caras interiores al poligonalizar cada fragmento.

También es importante la gestión del atlas de textura y la clasificación de caras por textura y poner las caras con la misma textura en el mismo vbo que las de otros fragmentos. Desea evitar demasiados cambios de textura y ordenar las caras por textura, y así minimizar el número de tramos en el glDrawRangeElements. Fusionar caras adyacentes del mismo mosaico en rectángulos más grandes también fue un gran problema. Me refiero a la fusión en la otra respuesta citada anteriormente.

Obviamente, solo poligonaliza esos fragmentos que alguna vez han sido visibles, puede descartar esos fragmentos que no han sido visibles durante mucho tiempo y volver a poligonalizar los fragmentos que se editan (ya que esto es una ocurrencia rara en comparación con el renderizado).

Será
fuente
Me gusta la idea de tu optimización de frustum. Pero, ¿no estás mezclando los términos "bloque" y "fragmento" en tu explicación?
Martijn Courteaux
probablemente si. Un bloque de bloques es un bloque de bloques en inglés.
Will
1

¿Dónde están todas tus comparaciones (BlockDistanceComparator )? Si es de una función de clasificación, ¿podría reemplazarse con una clasificación de radix (que es asintóticamente más rápida y no está basada en la comparación)?

Mirando sus tiempos, incluso si la clasificación en sí no es tan mala, su relativeToOriginfunción se llama dos veces para cada comparefunción; Todos esos datos deben calcularse una vez. Debería ser más rápido clasificar una estructura auxiliar, por ejemplo

struct DistanceIndexPair
{
    float m_distanceSquaredFromOrigin;
    int m_index;
};

y luego en pseudoCode

// for i = 0..numBlocks
//     distanceIndexPairs[i].m_distanceSquaredFromOrigin = ...;
///    distanceIndexPairs[i].m_index = i;
// sort distanceIndexPairs
// for i = 0..numBlocks
//    sortedBlock[i] = unsortedBlocks[ distanceIndexPairs.m_index ]

Lo siento si esa no es una estructura Java válida (no he tocado Java desde la licenciatura) pero espero que entiendas la idea.

celion
fuente
Esto me parece divertido. Java no tiene estructuras. Bueno, hay algo así llamado en el mundo Java pero tiene que ver con bases de datos, no es lo mismo. Pueden crear una clase final con miembros públicos, supongo que funciona.
Theraot
1

Sí, usa VBO y CULL, pero eso se aplica a casi todos los juegos. Lo que quieres hacer es renderizar el cubo solo si es visible para el jugador, Y si los bloques se tocan de una manera específica (digamos un fragmento que no puedes ver porque está bajo tierra), agregas los vértices de los bloques y haces es casi como un "bloque más grande", o en su caso, un trozo. Esto se denomina mallado codicioso y aumenta drásticamente el rendimiento. Estoy desarrollando un juego (basado en voxel) y utiliza un algoritmo de mallado codicioso.

En lugar de renderizar todo de esta manera:

hacer

Lo hace así:

render2

La desventaja de esto es que tienes que hacer más cálculos por porción en la construcción inicial del mundo, o si el jugador elimina / agrega un bloque.

prácticamente cualquier tipo de motor voxel necesita esto para un buen rendimiento.

Lo que hace es verificar si la cara del bloque está tocando otra cara del bloque, y si es así: solo se procesa como una (o cero) cara (s) de bloque. Es un toque costoso cuando se procesan fragmentos muy rápido.

public void greedyMesh(int p, BlockData[][][] blockData){
        boolean[][][][] mask = new boolean[blockData.length][blockData[0].length][blockData[0][0].length][6];

    for(int side=0; side<6; side++){
        for(int x=0; x<blockData.length; x++){
            for(int y=0; y<blockData[0].length; y++){
                for(int z=0; z<blockData[0][0].length; z++){
                    if(data[x][y][z] > Material.AIR && !mask[x][y][z][side] && blockData[x][y][z].faces[side]){
                        if(side == 0 || side == 1){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=y; i<blockData[0].length; i++){
                                if(i == y){
                                    for(int j=z; j<blockData[0][0].length; j++){
                                        if(!mask[x][i][j][side] && blockData[x][i][j].id == blockData[x][y][z].id && blockData[x][i][j].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[x][i][z+j][side] || blockData[x][i][z+j].id != blockData[x][y][z].id || !blockData[x][i][z+j].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x][y+i][z+j][side] = true;
                                }
                            }

                            if(side == 0)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+1, y, z), new VoxelVector3i(x+1, y+height, z+width), new VoxelVector3i(1, 0, 0), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y, z+width), new VoxelVector3i(x, y+height, z), new VoxelVector3i(-1, 0, 0), Material.getColor(data[x][y][z])));
                        }else if(side == 2 || side == 3){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=x; i<blockData.length; i++){
                                if(i == x){
                                    for(int j=z; j<blockData[0][0].length; j++){
                                        if(!mask[i][y][j][side] && blockData[i][y][j].id == blockData[x][y][z].id && blockData[i][y][j].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[i][y][z+j][side] || blockData[i][y][z+j].id != blockData[x][y][z].id || !blockData[i][y][z+j].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x+i][y][z+j][side] = true;
                                }
                            }

                            if(side == 2)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y+1, z+width), new VoxelVector3i(x+height, y+1, z), new VoxelVector3i(0, 1, 0), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+height, y, z+width), new VoxelVector3i(x, y, z), new VoxelVector3i(0, -1, 0), Material.getColor(data[x][y][z])));
                        }else if(side == 4 || side == 5){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=x; i<blockData.length; i++){
                                if(i == x){
                                    for(int j=y; j<blockData[0].length; j++){
                                        if(!mask[i][j][z][side] && blockData[i][j][z].id == blockData[x][y][z].id && blockData[i][j][z].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[i][y+j][z][side] || blockData[i][y+j][z].id != blockData[x][y][z].id || !blockData[i][y+j][z].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x+i][y+j][z][side] = true;
                                }
                            }

                            if(side == 4)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+height, y, z+1), new VoxelVector3i(x, y+width, z+1), new VoxelVector3i(0, 0, 1), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y, z), new VoxelVector3i(x+height, y+width, z), new VoxelVector3i(0, 0, -1), Material.getColor(data[x][y][z])));
                        }
                    }
                }
            }
        }
    }
}
Liam Larsen
fuente
1
¿Y vale la pena? Parece que un sistema LOD sería más apropiado.
MichaelHouse
0

Parece que su código se está ahogando en objetos y llamadas a funciones. Calculando los números, no parece que esté ocurriendo ningún revestimiento.

Puede intentar encontrar un entorno Java diferente, o simplemente meterse con la configuración del que tiene, pero una forma simple y sencilla de hacer su código, no rápido, pero mucho menos lento es al menos internamente en Vec3f para detener codificación OOO *. Haga que cada método sea autocontenido, no llame a ninguno de los otros métodos solo para realizar alguna tarea de baja categoría.

Editar: Si bien hay una sobrecarga en todo el lugar, parece que ordenar los bloques antes de renderizar es el peor comedor de rendimiento. ¿Es eso realmente necesario? Si es así, probablemente deberías comenzar por un ciclo y calcular la distancia de cada bloque al origen, y luego ordenar por eso.

* Demasiado orientado a objetos

aaaaaaaaaaaa
fuente
Sí, ahorrarás memoria, ¡pero perderás CPU! Entonces OOO no es demasiado bueno en los juegos en tiempo real.
Gustavo Maciel
Tan pronto como comience la creación de perfiles (y no solo el muestreo), cualquier línea que JVM normalmente hace desaparece. Es algo así como la teoría cuántica, no se puede medir algo sin cambiar el resultado: p
Michael
@Gtoknu Eso no es universalmente cierto, en algún nivel de OOO las llamadas a funciones comienzan a ocupar más memoria que el código en línea. Yo diría que hay una buena parte del código en cuestión que está alrededor del punto de equilibrio para la memoria.
aaaaaaaaaaaa
0

También podría intentar desglosar las operaciones matemáticas en operadores bit a bit. Si usted tiene 128 / 16, tratar de hacer un operador de bits: 128 << 4. Esto ayudará mucho con sus problemas. No intentes hacer que las cosas funcionen a toda velocidad. Realice la actualización de su juego a una velocidad de 60 o algo, e incluso desglosarlo para otras cosas, pero tendría que destruir y / o colocar vóxeles o tendría que hacer una lista de tareas, lo que reduciría sus fps. Podría hacer una tasa de actualización de aproximadamente 20 para las entidades. Y algo así como 10 para actualizaciones mundiales y / o generación.

JBakker
fuente