Con respecto a Java vs C ++, he escrito un motor de vóxel en ambos (la versión de C ++ se muestra arriba). También he estado escribiendo motores voxel desde 2004 (cuando no estaban de moda). :) Puedo decir con pocas dudas que el rendimiento de C ++ es muy superior (pero también es más difícil de codificar). Se trata menos de la velocidad computacional y más sobre la gestión de la memoria. Sin lugar a dudas, cuando está asignando / desasignando tantos datos como sea posible en un mundo de vóxeles, C (++) es el lenguaje a superar. sin embargo, deberías pensar en tu objetivo. Si el rendimiento es su máxima prioridad, vaya con C ++. Si solo quieres escribir un juego sin un rendimiento de vanguardia, Java es definitivamente aceptable (como lo demuestra Minecraft). Hay muchos casos triviales / perimetrales, pero en general puede esperar que Java se ejecute aproximadamente 1.75-2.0 veces más lento que (bien escrito) C ++. Puede ver una versión anterior de mi motor mal optimizada en acción aquí (EDITAR: versión más nueva aquí ). Si bien la generación de fragmentos puede parecer lenta, tenga en cuenta que está generando diagramas voronoi 3D volumétricamente, calculando normales de superficie, iluminación, AO y sombras en la CPU con métodos de fuerza bruta. He probado varias técnicas y puedo obtener una generación de fragmentos 100 veces más rápida utilizando varias técnicas de almacenamiento en caché e instancias.
Para responder el resto de su pregunta, hay muchas cosas que puede hacer para mejorar el rendimiento.
- Almacenamiento en caché. Siempre que pueda, debe calcular los datos una vez. Por ejemplo, cuento la iluminación en la escena. Podría usar iluminación dinámica (en el espacio de la pantalla, como un proceso posterior), pero hornear en la iluminación significa que no tengo que pasar las normales para los triángulos, lo que significa ...
Pase la menor cantidad de datos posible a la tarjeta de video. Una cosa que la gente tiende a olvidar es que cuantos más datos pases a la GPU, más tiempo llevará. Paso en un solo color y una posición de vértice. Si quiero hacer ciclos de día / noche, simplemente puedo hacer una gradación de color o puedo recalcular la escena a medida que el sol cambia gradualmente.
Dado que pasar datos a la GPU es muy costoso, es posible escribir un motor en un software que sea más rápido en algunos aspectos. La ventaja del software es que puede hacer todo tipo de manipulación de datos / acceso a la memoria que simplemente no es posible en una GPU.
Juega con el tamaño del lote. Si está utilizando una GPU, el rendimiento puede variar drásticamente según el tamaño de cada matriz de vértices que pase. En consecuencia, juegue con el tamaño de los fragmentos (si usa fragmentos). Descubrí que los fragmentos de 64x64x64 funcionan bastante bien. No importa qué, mantenga sus trozos cúbicos (sin prismas rectangulares). Esto hará que la codificación y varias operaciones (como las transformaciones) sean más fáciles y, en algunos casos, más efectivas. Si solo almacena un valor para la longitud de cada dimensión, tenga en cuenta que son dos registros menos que se intercambian durante el cálculo.
Considere mostrar listas (para OpenGL). Aunque son la forma "antigua", pueden ser más rápidos. Debe hornear una lista de visualización en una variable ... si llama a operaciones de creación de lista de visualización en tiempo real, será muy lento. ¿Cómo es una lista de visualización más rápida? Solo actualiza el estado, frente a los atributos por vértice. Esto significa que puedo pasar hasta seis caras, luego un color (frente a un color para cada vértice del vóxel). Si está utilizando GL_QUADS y vóxeles cúbicos, ¡esto podría ahorrar hasta 20 bytes (160 bits) por vóxel! (15 bytes sin alfa, aunque generalmente desea mantener las cosas alineadas en 4 bytes).
Utilizo un método de fuerza bruta para representar "fragmentos", o páginas de datos, que es una técnica común. A diferencia de los octrees, es mucho más fácil / rápido leer / procesar los datos, aunque es mucho menos amigable con la memoria (sin embargo, en estos días puede obtener 64 gigabytes de memoria por $ 200- $ 300) ... no es que el usuario promedio tenga eso. Obviamente, no puede asignar una gran matriz para todo el mundo (un conjunto de 1024x1024x1024 de voxels es 4 gigabytes de memoria, suponiendo que se use un int de 32 bits por voxel). Entonces asigna / reparte muchos arreglos pequeños, en función de su proximidad al espectador. También puede asignar los datos, obtener la lista de visualización necesaria y luego volcar los datos para ahorrar memoria. Creo que el combo ideal podría ser utilizar un enfoque híbrido de octrees y arrays: almacenar los datos en una matriz cuando se realiza la generación de procedimientos del mundo, la iluminación, etc.
Renderizar de cerca a lejos ... un píxel recortado es tiempo ahorrado. La GPU arrojará un píxel si no pasa la prueba de profundidad del búfer.
Renderizar solo fragmentos / páginas en la ventana gráfica (se explica por sí mismo). Incluso si el gpu sabe cómo recortar polígonos fuera de la ventana gráfica, pasar estos datos todavía lleva tiempo. No sé cuál sería la estructura más eficiente para esto ("vergonzosamente", nunca he escrito un árbol BSP), pero incluso un simple raycast por fragmento podría mejorar el rendimiento, y obviamente probar contra el frustum de visualización ahorrar tiempo.
Información obvia, pero para los novatos: elimine todos los polígonos que no estén en la superficie, es decir, si un vóxel consta de seis caras, elimine las caras que nunca se representan (están tocando otro vóxel).
Como regla general de todo lo que haces en programación: CACHE LOCALITY! Si puede mantener las cosas en la memoria caché local (incluso por un corto período de tiempo, marcará una gran diferencia. Esto significa mantener sus datos congruentes (en la misma región de memoria) y no cambiar áreas de memoria para procesarlas con demasiada frecuencia). , idealmente, trabaje en un fragmento por subproceso y mantenga esa memoria exclusiva para el subproceso. Esto no solo se aplica al caché de la CPU. Piense en la jerarquía del caché de esta manera (más lenta a más rápida): red (nube / base de datos / etc.) -> disco duro (obtenga un SSD si aún no tiene uno), ram (obtenga un triple canal o mayor RAM si aún no lo tiene), caché (s) de CPU, registros. Intente mantener sus datos en el último extremo, y no lo cambies más de lo necesario.
Enhebrado Hazlo. Los mundos Voxel son muy adecuados para el enhebrado, ya que cada parte se puede calcular (en su mayoría) independientemente de las demás ... Vi literalmente una mejora casi 4x (en un Core i7 de 4 núcleos y 8 hilos) en la generación del mundo procesal cuando escribí el rutinas para enhebrar.
No utilice tipos de datos char / byte. O pantalones cortos. Su consumidor promedio tendrá un procesador AMD o Intel moderno (como usted probablemente). Estos procesadores no tienen registros de 8 bits. Calculan los bytes colocándolos en una ranura de 32 bits, luego los vuelven a convertir (tal vez) en la memoria. Su compilador puede hacer todo tipo de vudú, pero usar un número de 32 o 64 bits le dará los resultados más predecibles (y más rápidos). Del mismo modo, un valor "bool" no toma 1 bit; el compilador a menudo usará 32 bits completos para un bool. Puede ser tentador hacer ciertos tipos de compresión en sus datos. Por ejemplo, podría almacenar 8 vóxeles como un solo número (2 ^ 8 = 256 combinaciones) si todos fueran del mismo tipo / color. Sin embargo, debe pensar en las ramificaciones de esto: podría ahorrar una gran cantidad de memoria, pero también puede dificultar el rendimiento, incluso con un pequeño tiempo de descompresión, porque incluso esa pequeña cantidad de tiempo adicional se escala cúbicamente con el tamaño de su mundo. Imagina calcular un rayo; para cada paso de la emisión de rayos, tendría que ejecutar el algoritmo de descompresión (a menos que encuentre una forma inteligente de generalizar el cálculo de 8 voxels en un paso de rayos).
Como menciona José Chávez, el patrón de diseño de peso mosca puede ser útil. Del mismo modo que usaría un mapa de bits para representar un mosaico en un juego en 2D, puede construir su mundo a partir de varios tipos de mosaico (o bloque) en 3D. La desventaja de esto es la repetición de texturas, pero puede mejorar esto usando texturas de varianza que encajen entre sí. Como regla general, desea utilizar instancias siempre que pueda.
Evite el procesamiento de vértices y píxeles en el sombreador al generar la geometría. En un motor vóxel inevitablemente tendrá muchos triángulos, por lo que incluso un sombreador de píxeles simple puede reducir el tiempo de renderizado en gran medida. Es mejor renderizar a un búfer, luego haces un sombreador de píxeles como un proceso posterior. Si no puede hacer eso, intente hacer cálculos en su sombreador de vértices. Se deben hornear otros cálculos en los datos del vértice cuando sea posible. Los pases adicionales se vuelven muy caros si debe volver a representar toda la geometría (como la asignación de sombras o la asignación de entorno). A veces es mejor renunciar a una escena dinámica en favor de detalles más ricos. Si su juego tiene escenas modificables (es decir, terreno destructible), siempre puede volver a calcular la escena a medida que se destruyen las cosas. La recompilación no es costosa y debería tomar menos de un segundo.
¡Desenrolle sus bucles y mantenga las matrices planas! No hagas esto:
for (i = 0; i < chunkLength; i++) {
for (j = 0; j < chunkLength; j++) {
for (k = 0; k < chunkLength; k++) {
MyData[i][j][k] = newVal;
}
}
}
//Instead, do this:
for (i = 0; i < chunkLengthCubed; i++) {
//figure out x, y, z index of chunk using modulus and div operators on i
//myData should have chunkLengthCubed number of indices, obviously
myData[i] = newVal;
}
EDITAR: a través de pruebas más extensas, he descubierto que esto puede estar mal. Use el caso que funcione mejor para su escenario. En general, las matrices deben ser planas, pero el uso de bucles de índice múltiple a menudo puede ser más rápido según el caso