El sombreado diferido es solo una técnica para "diferir" la operación de sombreado real para etapas posteriores, esto puede ser excelente para reducir la cantidad de pases necesarios (por ejemplo) para renderizar 10 luces que necesitan 10 pases. Mi punto es que, independientemente de la técnica de representación que esté utilizando, hay ciertas optimizaciones de representación posibles que reducen la cantidad de objetos (vértices, normales, etc.) que su canal de representación necesita procesar.
No hay un estándar de facto para las optimizaciones de renderizado, sino más bien una serie de técnicas que se pueden usar indistintamente o juntas para lograr ciertas características de rendimiento. El uso de cada técnica depende en gran medida de la naturaleza de la escena que se representa.
El renderizado diferido intenta resolver el problema cuando aumenta el número de luces, lo que en el renderizado directo puede hacer que explote el número de pases.
Esas técnicas no optimizan directamente la parte de sombreado diferido, pero según su descripción, la parte de sombreado diferido NO es su problema. Sin embargo, su problema es que está enviando toda la escena a la canalización de representación. Por lo tanto, su motor tiene que procesar (por ejemplo, todos los 100 millones de vértices) en su escena solo para poder enviar el resultado al búfer g, mientras que la mayoría de estos 100 millones de vértices pueden eliminarse trivialmente y no enviarse al preproceso de vértice y paso de fragmentos.
En el caso de un renderizador directo, el vértice N será procesado por la etapa de vértice como un total de vertex count*lights count
y por la etapa de fragmento como un total fragments count*number Lights
, el sombreado diferido efectivamente reduce esto solo vertex count
a la etapa de vértice y fragments count
al recuento de fragmentos, antes de resolver el sombreado real Pero aún así, N puede ser demasiado para procesar, especialmente cuando la mayoría de ellos pueden ser descartados trivialmente.
Esto hace que el sacrificio sea más efectivo en caso de renderizado directo / pases múltiples. Pero tenga en cuenta que la mayoría de los motores usarán un enfoque de representación dual, ya que el sombreado diferido por sí solo no puede resolver objetos transparentes, esto hace que el uso de esas optimizaciones sea imprescindible, no conozco ningún motor comercial que no los haga todos.
El sacrificio de Frustum
Solo los Objetos que están total o parcialmente incluidos en la vista frustum, deben ser enviados a la canalización de renderizado. Este es el concepto básico de eliminación del frustum, desafortunadamente verificar si una malla está dentro / fuera de la vista del frustum puede ser una operación costosa, por lo que, en cambio, los diseñadores de motores usan un volumen delimitador aproximado como un cuadro delimitador o una esfera delimitadora del eje alineado (AABB) , aunque esto podría no ser tan preciso como usar la malla real, la diferencia de precisión no vale la pena comprobar con la malla real.
Incluso con volúmenes delimitadores, realmente no necesita verificar cada uno, alternativamente, puede construir una jerarquía de volúmenes delimitadores para hacer un sacrificio anterior, el uso depende en gran medida de la complejidad de la escena.
Esta es una técnica buena y simple para un motor más pequeño, y casi se usa en todos los motores que he usado. Recomiendo usar una comprobación de volumen / volumen de límite "normal" sin jerarquías si su motor no requiere renderizar escenas muy complejas.
Cara posterior de sacrificio
Esto es imprescindible, ¿por qué dibujar caras que no serán visibles de todos modos? Las API de representación proporcionan una interfaz para activar / desactivar el sacrificio de la cara posterior. A menos que tenga una buena razón para no encenderlo, como algunas de las aplicaciones CAD que necesitan dibujar caras posteriores en ciertas circunstancias, esto es algo que debe hacer.
Eliminación de oclusión
Usando el Z-buffer puede resolver la determinación de visibilidad. Pero el problema es que el búfer Z no siempre es excelente en términos de rendimiento, ya que el búfer Z solo se puede resolver en etapas posteriores de la tubería, los objetos ocluidos deben rasterizarse y pueden escribirse en el búfer Z y Tampón de color antes de fallar la prueba Z
El sacrificio por oclusión resuelve esto haciendo algunas pruebas iniciales para eliminar los objetos ocluidos que se encuentran en el tronco de renderizado. Una implementación práctica del sacrificio de oclusión es usar consultas basadas en puntos y verificar si ciertos objetos son visibles desde una vista de punto específica. Esto también se puede utilizar para eliminar luces que no contribuyen a la imagen final, esto es especialmente útil en un renderizador de motor diferido.
Un gran ejemplo del mundo real de tal técnica es en GTA5, donde los rascacielos están ubicados estratégicamente en el centro de la ciudad, no solo son decoraciones, sino que también funcionan como oclusores, ocluyendo efectivamente el resto de la ciudad y evitando que sea rasterizado
Nivel de detalle
El nivel de detalle es una técnica muy utilizada, la idea es usar una versión más simple de la malla cuando la malla contribuye menos a la escena. hay dos implementaciones comunes; uno simplemente cambia la malla por una más simple cuando ya no contribuye mucho, la malla se selecciona en función de algún factor como la distancia y el número de píxeles (área en la pantalla) que ocupa la malla. La otra versión tesela dinámicamente la malla, esto se usa ampliamente en el renderizado del terreno.
¿Qué pasa si todo esto no funciona?
Bueno, esa es una buena pregunta.
Lo primero que debe hacer es crear un perfil de su aplicación con un generador de perfiles de gráficos y determinar dónde está el cuello de botella. Tenga en cuenta que el cuello de botella puede cambiar a medida que cambia el contenido que se representa. Los cuellos de botella también pueden ser parte del código que se ejecuta en la CPU, por lo que también debe medir eso.
Después de eso, debe hacer algunas optimizaciones en el cuello de botella, tenga en cuenta que no hay una respuesta correcta para esto, y será diferente de un hardware a otro.
Algunos trucos comunes de optimización de GPU:
- Evite ramificarse en sombreadores.
- Pruebe diferentes estructuras de vértices, por ejemplo,
{VNT}
intercaladas en la misma matriz o {V},{N},{T}
en diferentes matrices.
- Dibuja la escena de adelante hacia atrás.
- Apague Z-buffer en algunos puntos, por ejemplo, si una imagen no necesita pruebas de Z.
- Usa texturas comprimidas.
Algunos trucos comunes de optimización de CPU:
- Use funciones en línea para funciones pequeñas.
- Utilice SIMD (Datos múltiples de una sola instrucción) cuando sea posible.
- Evite los saltos de memoria hostiles de caché.
- Use VBO con la cantidad "correcta" de datos. (dependiendo de su hardware), pero generalmente son mejores las llamadas de extracción.
Pero, ¿qué pasa si mi cuello de botella estaba en el sombreado diferido?
En este caso, dado que el sombreado diferido está más preocupado por las luces, la parte más obvia es optimizar los cálculos de sombreado reales. Algunos de los puntos a tener en cuenta:
- Renderiza luces que realmente afectan la imagen final. En otras palabras, apaga las luces que no contribuyen. Esto se puede implementar de manera efectiva utilizando el sacrificio de oclusión que mencioné antes.
- ¿Esta luz necesita el componente especular o algún otro? Tal vez no.
- ¿Esta luz proyecta sombra? Algunas luces no necesitan proyectar sombras.
- ¿Se puede calcular previamente esta contribución ligera? Si no se mueve, probablemente algunos aspectos se pueden calcular previamente.