Aprende Unreal Engine 4

Rendering en UE4

April 03, 2018 | 8 Minute Read

¿Cómo renderiza UE4 un frame? Conocer el proceso de rendering permite poner orden a todas las técnicas que se usan en UE4 además de cuales son las partes más sensibles para optimizar.

Este post es un resumen de la masterclass de Homam Bahnassi, diseccionado y ampliado con apuntes tomados de la documentación oficial.

Los hilos

El rendering es un proceso extremadamente paralelo. Ocurre en múltiples hilos. Los hilos principales son CPU (Game), CPU (Draw) y GPU.

Rendering Parallel

Empecemos por el primero de ellos:

CPU (Game)

En CPU (Game) es responsable de calcular toda la lógica y las transformadas de los objetos:

  • Animaciones
  • Posiciones de modelos y objetos
  • Físicas
  • AI
  • Spawn & Destroy, Hide & Unhide

En definitiva, cualquier cosa relacionada con la posición de los objetos a cambiar.

CPU (Draw)

Ahora UE4 conoce la posición de cada objeto, pero antes de enviar la imagen a la GPU se hace un filtrado, esto es, decir que modelos vamos a incluir para renderizar y cuales ignorar (ignorar este filtrado podría hacer excesivamente caro el hilo de la GPU).

Para ello el Engine nos ofrece distintas técnicas de culling:

  • Distance Culling
  • Frustum Culling
  • Precomputed Visibility
  • Occlusion Culling

Distance Culling

Como su propio nombre indica, los objetos no se renderizan si están a más de cierta distancia. También se puede hacer que no se rendericen si están a menos de cierta distancia. Estas distancias se pueden cambiar en todos los actores en el parámetro:

Distance Culling

Por defecto no se hace Distance Culling (tanto el parámetro Min Draw Distance como el Desired Max Draw Distance, están por defecto a 0, en el caso del segundo el valor 0 significa infinito).

En vez de usar esta propiedad por cada actor, se puede hacer Distance Culling de una manera más genérica usando el actor Cull Distance Volume. Todos los actores contenidos en este volumen serán culled en función de su tamaño (la arista más larga en su bounding box) y su distancia a la cámara.

Estos tamaños y distancias a la cámara se configuran en el actor Cull Distance Volume:

Cull Distance Volume

En el ejemplo, se definen 3 tamaños cercanos: 50, 120 y 300 cada uno asociado a unas respectivas distancias.

Todos los actores contenidos en el volumen con el tamaño más cercano a 50 unidades (es decir, cuyo tamaño sea menor de 85, ya que si es mayor de 85 el tamaño sería más cercano a los 120 que es la segunda entrada del array) serán ocluidos si están a una distancia de 500 unidades o más lejano de la cámara.

Todos los actores contenidos en el volumen con el tamaño más cercano a 120 (esto es, entre 85 y 210, ya que menos de 85 el tamaño sería más cercano a 50 y más de 210 el tamaño sería más cercano a 300) cuya distancia a la cámara sea mayor de 1000 serán ocluidos.

Por último, todos los objetos contenidos en el volumen cuyo tamaño sea el más cercano a 300 (es decir, 210 o superior) nunca serán ocluidos. Aquí la distancia cero significa infinito.

Frustum Culling:

Frustum Culling chequea los objetos en frente de la cámara. Cuando mayor FoV más objetos a renderizar y menos culled. Esta técnica de culling se hace automáticamente. Puedes ver el frustum de la cámara en el menu Show del editor en Advanced.

Precomputed Visibilty:

Es una técnica compleja, básicamente divide la escena en un grid y para cada celda recuerda que es visible en esa localización. Gracias a ello se pueden responder preguntas complejas como que objetos están ocluidos por cuales otros en función del ángulo y posición de la cámara. Al ser una técnica de precalculado solo se hace con objetos estáticos.

Para hacer uso de la técnica hay que colocar el volumen Precomputed Visibility Volume. Una vez colocado hay que precomputar (al igual q se hace con las luces) en Build > Precompute Visiblity. Se puede ver el grid en el menu Show del editor en Advanced.

Occlusion Culling

Por último, el Dynamic Occlusion Culling que chequea el estado de visibilidad de cada objeto o si está ocluido por otro. Aunque se hace mayoritariamente en CPU también hay algunas partes gestionadas por la GPU.

Algunos comandos interesantes para ver el culling:

freezerendering

Este comando congela el proceso de rendering y por tanto puedes mover la cámara libremente y ver que partes se han ocluido.

Con el comando:

stat initviews

Nos muestra el coste.

En este punto UE4 sabe la posición de cada objeto y que objetos serán renderizados o no.

GPU

El siguiente paso, ahora en GPU, es el Early Z Pass.

Del mismo modo que hemos excluido objetos para renderizar, necesitamos hacer algo parecido para los pixels.

Para hacer esto se genera un depth pass y se usa para determinar que pixels están visibles y cuales ocluidos.

Early Z Pass

Drawcalls

El siguiente paso para renderizar la geometría son los drawcall.

La GPU renderiza en drawcall por drawcall, no por triangulo a triángulo. Un drawcall, por tanto, es un grupo de triángulos que comparten algunas propiedades.

Los drawcall son preparados por el hilo CPU (Draw) y enviados a la GPU uno a uno.

Draw Call

En la imagen superior, la geometría de la izquierda se puede pintar con un drawcall, los triángulos de lo tres cilindros comparten todos los mismas propiedades (= material), mientras que en la derecha al menos necesita dos drawcall (el último cilindro tiene dos materiales).

Aquí la lista de drawcall en una escena de ejemplo:

Draw Call Basepass

Los Drawcall son caros, entre 2000-3000 drawcalls son razonables en un PC/consola, más de 5000 son demasiados y más de 10.000 son un problema. En móviles es otra historia, unos cientos como máximo. Los drawcalls son determinados por los objetos visibles.

En UE4 se pueden medir con el comando

stat RHI

Para optimizar el número de drawcalls:

  • Intentar usar LowPoly, compartir mismo material, mezclar meshes del mismo area.

  • Usar HLOD (Hierarical Level Of Detail). En LOD un modelo se intercambia por otro más simple (en nºtris y material) a partir de una determinada distancia. En HLOD además se mezclan modelos en la distancia y por tanto son menos drawcalls.

  • Usar Instancied Rendering: agrupar manualmente objetos juntos dentro de un simple drawcall. En UE4 con el component Intancied Static Mesh.

Y entonces entra el juego el vertex shader + fragment shader para rasterizar la imagen.

Vertex & Fragment Shader

En un pixel puede haber influido (escrito el color) el fragment shader de distintos drawcalls. A esto se le llama quad overdraw y puede visualizarse en UE en el menú show.

En deferred shading el resultado de VS+FS es almacenado en el geometry buffer (GBuffer) que también se pueden visualizar en UE en el menú show y se usa para calcular la iluminación en la escena y, en definitiva, asignar el color final de cada pixel.

Empezamos con las luces y sombras dinámicas:

Light settings

La dynamic lighting (deferred shading) es calculada en los fragment shaders. Cada luz puntual tiene un máscara en forma de esfera para indicar que pixels están afectados.

Depth buffer

Considerando los atributos de la luz (color, falloff, intensidad, etc.,) el pixel shader hace:

Light normal

En este punto ya tenemos la escena iluminada de manera básica. El siguiente paso: las sombras.

Sombras

La técnica más común para calcular las sombras en real-time es el Shadow Maps. La idea básica es chequear para cada pixel si es visible para una luz dada. Si el pixel está ocluido desde el punto de vista de la luz entonces es una sombra. Se calcula un depth buffer desde el punto de vista de la luz. Más info aquí

Light shadows

En la imagen superior la escena, a la izquierda el depth buffer desde el punto de vista de la cámara y a la derecha desde el punto de vista de la luz.

Depth buffer camera

Las luces y sombras dinámicas tienen una serie de pros y contras:

  • Pros
    • Es renderizado en tiempo real usando el GBuffer
    • Las luces pueden cambiar, mover, añadirse o eliminarse
    • No necesita ninguna preparación especial del modelo
  • Cons
    • Las sombras dinámicas son MUY caras

Las luces y sombras estáticas son precalculadas y almacenadas en unas texturas llamadas lightmaps. Estas texturas son multiplicadas por el basecolor.

Aquí más información de como funciona la iluminación en UE4.