
En casi todos los juegos, es necesario llenar los niveles del juego con objetos que crean riqueza visual, belleza y variabilidad del mundo virtual. Toma cualquier juego de mundo abierto. Allí los árboles, la hierba, la tierra y el agua son los principales "marcadores de posición" de la imagen. Hoy habrá muy pocas GPGPU, pero intentaré decirte cómo dibujar muchos árboles y piedras en el marco cuando no puedas, pero realmente quiero hacerlo.
Cabe señalar de inmediato que tenemos un pequeño estudio independiente y, a menudo, no tenemos los recursos para dibujar y modelar cada pequeña cosa. De ahí el requisito de que varios subsistemas sean una "superestructura" sobre la funcionalidad preparada del motor. Así fue en el primer artículo del ciclo sobre animaciones (allí usamos y aceleramos el sistema de animación Unity terminado), por lo que estará aquí. Esto simplifica enormemente la introducción de nuevas funciones en el juego (menos para aprender, menos errores, etc.).
Entonces, la tarea: necesitas dibujar muchos bosques. En el juego tenemos una estrategia en tiempo real (RTS) con grandes niveles (30x30 km), y esto establece los requisitos básicos para el sistema de renderizado:
- Con la ayuda del minimapa, podemos transferir instantáneamente a cualquier punto del nivel. Y los datos sobre los objetos para la nueva posición deben estar listos. No podemos confiar en la carga de recursos después de un tiempo en juegos FPS o TPS.
- Los objetos a niveles tan grandes necesitan un número realmente grande. Cientos de miles, si no millones.
- Una vez más, los niveles grandes hacen que sea muy largo y difícil configurar manualmente "bosques". La generación de procedimientos de bosque, piedras y arbustos es necesaria, pero con la posibilidad de ajuste manual y disposición en lugares clave del nivel del juego.
¿Cómo se puede resolver este problema? Tal cantidad de objetos ordinarios dispuestos de la unidad aún no se jalarán. Moriremos en sacrificios y lotes. El renderizado es posible usando instancing. Es necesario escribir un sistema de control. Los árboles necesitan ser modelados. Se debe hacer un sistema de animación de árbol. Ooh Lo quiero bella e inmediatamente. Hay SpeedTree, pero no hay una API para animaciones, la vista superior de las vallas publicitarias es terrible, ya que no existe una "valla publicitaria horizontal" y la documentación es deficiente. ¿Pero cuándo nos detuvo esto? Optimizaremos la representación de SpeedTree.
Renderizado
Veamos, para empezar, si todo es tan malo con los objetos normales de Speedtree:

Aquí hay unos 2.000 árboles en el escenario. Todo está en orden con el renderizado, la instancia combinó los árboles en lotes allí, pero con la CPU todo está mal. La mitad del tiempo que la cámara está renderizando se está enfriando. Y necesitamos cientos de miles. Definitivamente rechazamos GameObjects, pero ahora necesitamos descubrir la estructura del modelo SpeedTree, el mecanismo de cambio LOD y hacer todo con manijas.
El árbol SpeedTree consta de varios LOD (generalmente 4), el último de los cuales es una valla publicitaria, y el resto son geometrías de diversos grados de detalle. Cada uno de ellos consta de varios sabmesh, con su propio material:
Esta no es la especificidad de SpeedTree. Cualquier estructura puede tener tal estructura. La conmutación de LOD se implementa en dos modos disponibles:
- Cross Fade:
- Árbol de velocidad:
CrossFade (en términos de sombreadores de Unity se define mediante la definición del preprocesador LOD_FADE_CROSSFADE) es el método principal para cambiar los LOD para cualquier objeto de escena con varios niveles de detalle. Consiste en el hecho de que cuando se cambia el LOD, la malla que debería desaparecer no solo desaparece (el salto de calidad del modelo será claramente visible), sino que se "disuelve" en la pantalla mediante
tramado . Un efecto simple, y evita el uso de verdadera transparencia (mezcla alfa). El modelo que debería aparecer exactamente de la misma manera "aparece" en la pantalla.
SpeedTree (LOD_FADE_PERCENTAGE) está hecho específicamente para árboles. Además de las coordenadas principales, las coordenadas adicionales de la posición de los vértices junior con respecto al nivel LOD actual se registran en la geometría de las hojas, ramas y tronco. El grado de transición de un nivel a otro es el valor de peso para la interpolación lineal de estas dos posiciones. El movimiento hacia / desde la cartelera se realiza utilizando el método CrossFade.
En principio, esto es todo lo que necesita saber para implementar su propio sistema de conmutación LOD. La representación en sí es simple. Recorremos todo tipo de árboles, todos los LOD y todos los sabmesh de cada LOD. Instalamos el material apropiado y dibujamos todas las instancias de este objeto de una sola vez usando instancing. Por lo tanto, el número de DrawCalls es igual al número de objetos únicos en la escena. ¿Cómo sabemos qué dibujar? Esto nos ayudará
Generador de bosque
El aterrizaje en sí es simple y sin pretensiones. Para cada tipo de árbol, dividimos el mundo en quads para que cada árbol se ajuste a un árbol. Repasamos todos los quads y verificamos la máscara del formulario:

en un punto dado del nivel, ¿es posible plantar un árbol aquí? La máscara, con lugares "boscosos", está dibujada por el diseñador de niveles. Al principio todo estaba en la CPU y C #. El generador funcionaba lentamente, y el tamaño de los niveles creció de modo que esperar la regeneración durante varias decenas de minutos se volvió estresante. Se decidió transferir el generador a la GPU y al sombreador Compute. Aquí, también, todo es simple. Necesitamos el mapa de altura de la tierra, la máscara de plantación de árboles y AppendStructuredBuffer, donde agregamos los árboles generados (posición e ID, que son todos los datos).
Organizado por árboles a mano en puntos clave, un guión especial ingresa en matrices comunes y elimina el objeto original de la escena.
Selección y cambio de LOD
Conocer la posición y el tipo de árbol no es suficiente para hacer una representación efectiva. Es necesario determinar en cada cuadro qué objetos son visibles y qué LOD (considerando la lógica de transición) enviar al render.
Un sombreador de cálculo especial también hará esto. Para cada objeto, primero se realiza el sacrificio de Frustum:
Si el objeto es visible, se ejecuta la lógica de conmutación LOD. Según el tamaño en la pantalla, determinamos el nivel de LOD deseado. Si el modo CrossFade está configurado para el LOD del grupo, entonces incrementamos el tiempo de transición para el tramado. Si SpeedTree Percentage, consideramos el valor de transición normalizado entre LOD.
Las API gráficas modernas tienen funciones maravillosas que permiten pasar información de envío de sorteos a la llamada de sorteo en el búfer de cálculo (por ejemplo, ID3D11DeviceContext :: DrawIndexedInstancedIndirect para D3D11). Esto significa que también puede llenar este búfer de cálculo en la GPU. Por lo tanto, resulta hacer un sistema totalmente independiente de la CPU (bueno, simplemente llame a Graphics.DrawMeshInstancedIndirect). En nuestro caso, solo es necesario registrar el número de instancias de cada sabmesh. El resto de la información (el número de índices en la malla y las compensaciones) es estática.
Compute buffer, con argumentos para llamar a draw, se divide en secciones, cada una de las cuales es responsable de llamar a la representación de su submesh. En el sombreador de cálculo para la malla que se dibujará en el cuadro actual, incremente el valor correspondiente de InstanceCount.
Así es como se ve en el render:
El sacrificio de oclusión de GPU es un próximo paso obvio, pero para RTS con una cámara de este tipo y colinas no muy grandes, las ganancias no son tan obvias (y
aquí está para aquellos interesados). Aún no lo he hecho.
Para que todo se dibuje correctamente, debe ajustar un poco los sombreadores SpeedTree para tomar la posición y los valores de las transiciones entre los LOD de los búferes de cálculo correspondientes.
Ahora dibujamos árboles hermosos, pero estáticos. Y los árboles SpeedTree están influenciados de manera realista por el viento, animándolos. Toda la lógica de tales animaciones está en el archivo SpeedTreeWind.cginc, pero no hay documentación ni acceso a parámetros internos de Unity.
CBUFFER_START(SpeedTreeWind) float4 _ST_WindVector; float4 _ST_WindGlobal; float4 _ST_WindBranch; float4 _ST_WindBranchTwitch; float4 _ST_WindBranchWhip; float4 _ST_WindBranchAnchor; float4 _ST_WindBranchAdherences; float4 _ST_WindTurbulences; float4 _ST_WindLeaf1Ripple; float4 _ST_WindLeaf1Tumble; float4 _ST_WindLeaf1Twitch; float4 _ST_WindLeaf2Ripple; float4 _ST_WindLeaf2Tumble; float4 _ST_WindLeaf2Twitch; float4 _ST_WindFrondRipple; float4 _ST_WindAnimation; CBUFFER_END
¿Cómo los elegiríamos? Para hacer esto, para cada tipo de árbol, renderizaremos el objeto SpeedTree original en algún lugar en un lugar invisible (o más bien, visible en Unity, pero no visible en la cámara; de lo contrario, los parámetros no se actualizarán). Esto se puede lograr aumentando considerablemente el cuadro delimitador y colocando el objeto detrás de la cámara). Cada cuadro se elimina el conjunto de valores deseado utilizando material.GetVector (...).
Entonces, los árboles ondean en el viento, pero la vista superior de las vallas publicitarias es deprimente:
Con la opción de sombreador BILLBOARD_FACE_CAMERA_POS aún peor:
Necesitamos carteles horizontales (de arriba hacia abajo). Esta es una característica estándar de SpeedTree desde la época de King Pea, pero a juzgar por los foros, todavía no se implementa en Unity. Publicación del
foro oficial de SpeedTree: "La integración de Unity nunca utilizó la cartelera horizontal". Nos abrocharemos las manos. La geometría en sí es fácil de hacer. ¿Cómo encontrar las coordenadas UV de un sprite en un atlas para ella?

Obtenemos el antiguo SDK SpeedTreeRT, y encontramos la estructura en la documentación:
struct SBillboard { bool m_bIsActive; const float* m_pTexCoords; const float* m_pCoords; float m_fAlphaTestValue; };
"M_pTexCoords apunta a un conjunto de 4 (s, t) coordenadas de textura que definen las imágenes utilizadas en la cartelera. m_pTexCoords contiene 8 entradas ", dice en un idioma extranjero. Bueno, buscaremos una secuencia de 4 valores de punto flotante en un archivo spm binario, cada uno de los cuales se encuentra en el rango [0..1]. Mediante el método de búsqueda científica, descubrimos que la secuencia deseada está frente a un bloque de 12 flotadores con signos correspondientes al patrón:
float signs[] = { -1, 1, -1, 1, 1, -1, 1, 1, 1, -1, 1, 1 };
Escribimos una pequeña utilidad de consola en los profesionales, que itera sobre todos los archivos spm y busca coordenadas uv para carteles horizontales en ellos. El resultado es una etiqueta CSV:
Azalea_Desktop.spm: 0, 1, 0.333333, 1, 0.333333, 0.666667, 0, 0.666667, Azalea_Desktop_Flowers_1.spm: 0, 1, 0.333333, 1, 0.333333, 0.666667, 0, 0.666667, Azalea_Desktop_Flowers_2.spm: 0, 1, 0.333333, 1, 0.333333, 0.666667, 0, 0.666667, Leaf_Map_Maker_Desktop_1_Modeler_Use_Only.spm: Pattern not found! Leaf_Map_Maker_Desktop_2_Modeler_Use_Only.spm: Pattern not found! BarrelCactus_Cluster_Desktop_1.spm: 0, 0.592376, 0.407624, 0.592376, 0.407624, 0.184752, 0, 0.184752, BarrelCactus_Cluster_Desktop_2.spm: 0, 1, 0.499988, 1, 0.499988, 0.500012, 0, 0.500012, BarrelCactus_Desktop_1.spm: 0, 0.2208, 0.220748, 0.2208, 0.220748, 5.29885e-05, 0, 5.29885e-05, BarrelCactus_Desktop_2.spm: 0, 1, 0.301392, 1, 0.301392, 0.698608, 0, 0.698608,
Para asignar coordenadas de textura a la geometría de la cartelera horizontal, encontramos el registro deseado y lo analizamos.
Ahora es así:
Aún no muy. Usando el umbral de prueba alfa, desvaneceremos la cartelera vertical, en grabaciones desde el ángulo hasta la cámara:
ResumenProfiler que muestra estadísticas dinámicas (cuántas cosas se representan) y estáticas (cuántos objetos y sus parámetros están en la escena):
Bueno, el hermoso video final (la segunda mitad muestra el cambio de niveles de calidad):
Lo que tenemos al final:
- El sistema es totalmente independiente de la CPU.
- Funciona rapido.
- Utiliza activos SpeedTree listos para usar, que puede comprar en Internet.
- Por supuesto, la hice amiga de cualquier grupo LOD, no solo de SpeedTree. Tantos guijarros ahora también son posibles.
Entre las deficiencias se puede observar la falta de eliminación de oclusiones y todavía carteles poco expresivos.