top of page
Foto del escritorBraulio Madrid

El "poder" de las instancias por GPU

Si eres un obsesionado con el #rendimiento, este capítulo le va a ser de mucha utilidad, vas a poder lograr exprimir aún más la potencia de los dispositivos, en esta ocasión más concretamente a la #GPU y de que manera. O quizás no.


Unity tiene un abanico enorme de técnicas que podemos utilizar para obtener un mejor rendimiento, las más comúnmente utilizadas son los lotes de objetos marcados como estáticos y los que Unity hace por su propia cuenta con lotes dinámicos si el objeto cumple con ciertas características de número de vértices y tamaños, pero también tiene técnicas como el compartir materiales, la oclusión de planos que usa la cámara, la oclusión de vértices usado static culling, o usando occluders, hasta tiene la opción del uso de capas, que no solo sirven para separar las físicas, sino también para ocultar objetos, pero creo que muchas de estas técnicas palidecen con esta que describiré a continuación. O quizás no.


Las gpus más modernas cuentan con tecnologías que permite usar la copiar una malla y duplicarla muchas veces por encima de 100.000, sin apenas sufrir por rendimiento, y se puede pensar que sentido tiene duplicar una malla si todas se ven igual, no tan rápido, aunque uses la misma malla con el mismo material, aun así puedes crear variaciones de color, diferentes tamaños o diferentes parámetros para el mismo objeto y aun así no sufrir caídas de frames.


Preparando la escena.


  • Van a crear una escena nueva.

  • Creen una esfera en la escena.

  • Creen un nuevo material y asígnelo a la esfera.

  • Creen un nuevo shader de superficie en el proyecto y asígnelo al material que crearon.

  • Ahora arrastren la esfera a la ventana del proyecto, para convertirlo en un prefab

  • Ahora dupliquen tantas veces quieran ese prefab en la escena.


Debe verse algo como esto. Ahora abran el archivo del shader de superficie y edítelo.


Aplicando la posibilidad de instanciar al shader.


Para hacer esto debemos crear un shader de superficie que tenga propiedades que serán las que variaremos a través de código más adelante. Vamos a cambiar un parámetro de color, solo para que sea algo sencillo de entender.


[PerRendererData]_Color ("Color", Color) = (1,1,1,1)

La etiqueta de Per Renderer Data, es similar a [HideInInspector], hasta sobra la existencia de la etiqueta, lo único que hace es indicar que ese parámetro será manipulado mediante scripts más adelante, también puede simplemente prescindir del parámetro y solo llamarlo dentro de CGPROGRAM.


Dentro de CGPROGRAM en la parte donde se nombran las propiedades, entramos las siguientes macros


UNITY_INSTANCING_BUFFER_START(Props)
    UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color)
UNITY_INSTANCING_BUFFER_END(Props)

Note que dentro de la macro INSTANCED_PROP estamos indicando el tipo de variable y el nombre de la propiedad, tal como si se tratara de una propiedad común.


Dentro de la función surf, vamos a incluir la siguiente macro


fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * UNITY_ACCESS_INSTANCED_PROP(Props, _Color);

Note que esta es una función que devuelve un color, donde usamos una textura y luego lo multiplicamos por nuestra propiedad de color, Note que usamos la macro UNITY_ACCESS_INSTANCED_PROP(Bloque de propiedades, parámetro). Unity guarda en alguna parte el acceso a estas propiedades mediante bloques, lo que permite pensar que podemos usar distintos bloques para usar en distintos materiales.


Con esto ya tenemos el shader listo, pero voy a aclarar algunas cosas, es recomendable hacer esto en shaders de superficie, pero eso no quiere decir que no se pueda aplicar en shaders de vértices y fragmentos, pero es más engorroso y posiblemente tengas problemas para calcular los reflejos y las sombras. También tenga en cuenta que solo se permite en hasta un tope de luces que afectan el material, todo lo que pueda abarcar en solo 2 pases, de lo contrario Unity clonará el material y creara una copia pero no una instancia.



Shader "Custom/GPU-Instancing" {
Properties 
{
[PerRendererData]_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}

SubShader 
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows
// Esto le indica a unity que nuestro material es compatible con instancias
// Esto es solo necesario en shaders de vertices y fragmentos
#pragma multi_compile_instancing
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0

sampler2D _MainTex;

struct Input {
	float2 uv_MainTex;
	};
	
half _Glossiness;
half _Metallic;

// Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
// See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
// #pragma instancing_options assumeuniformscaling	UNITY_INSTANCING_BUFFER_START(Props)
// put more per-instance properties here
	UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color)
UNITY_INSTANCING_BUFFER_END(Props)

void surf (Input IN, inout SurfaceOutputStandard o) {
	// Albedo comes from a texture tinted by color
	fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
	o.Albedo = c.rgb;
	// Metallic and smoothness come from slider variables
	o.Metallic = _Metallic;
	o.Smoothness = _Glossiness;
	o.Alpha = c.a;
	}
	ENDCG
}
FallBack "Diffuse"
}

Si todo ha salido correcto, deberán ver que esta opción está presente, de hecho lo está desde que crearon el shader de superficie sin modificar, pero no ocurre lo mismo si lo hacer con un shader de vértices y fragmentos.


Creando el controlador de instancias.


Hemos hecho el shader, pero aun no pasa nada interesante, como accedemos a la propiedad de _Color que hemos puesto en la macro.


Lo que verán a continuación es mi manera de controlar estas instancias, puede que hayan mejores, la mayoría hacen un enfoque de generar colores aleatorios y llenar toda la escena con esferas. En mi caso me enfocaré en poder elegir personalmente el color de cada esfera, por eso no hice tantas copias.


Vamos a crear un archivo C# en el proyecto, en mi caso lo he nombrado "GPUInstancePropertiesControl.cs" en su caso puede ser cualquier otro.


Primero creo una clase serializable en el que contenga el gameObject y un color.

[System.Serializable]
public class ObjectInstantiable {
    public GameObject gameObject;
    public Color color;
}

Luego la clase principal tendrá acceso a cada una de las propiedades de la clase serializable, ademas creamos una variable del tipo MaterialPropertyBlock llamada props, esta es una clase muy poco frecuente y uno pensaría que normalmente hereda de material, pero realmente hereda del renderer del objeto. Esto para mi es un poco como una caja negra, solo se qué si intentas acceder directamente a la propiedad del _Color a través de shader material, terminas pintando todas las esferas del mismo color, porque esto seria como nosotros manipular el color directamente desde el inspector.


public class GPUInstancePropertiesControl : MonoBehaviour {
    MaterialPropertyBlock props;
    
    public ObjectInstantiable[] objects;

    private void OnValidate()
    {
        if(objects.Length > 0)
        {
            props = new MaterialPropertyBlock();
            foreach (ObjectInstantiable obj in objects)
            {
                props.SetColor("_Color", obj.color);
                MeshRenderer renderer;
                renderer = obj.gameObject.GetComponent<MeshRenderer>();
                renderer.SetPropertyBlock(props);

            }
        }
    }
}

Esta clase precisamente prepara un bloque de propiedades en cache, ya con los cambios de color que hemos hecho y los inyecta en el momento de ser renderizado.


El método OnValidate, se ejecuta cuando el inspector ha detectado cambios, es por decirlo una manera más elegante de no tener que usar la etiqueta [ExecuteInEditMode] en un método OnGUI.


Guarda los cambios y asigna cada una de las esferas y un color distinto a cada una, el resultado tendría que verse algo como esto.



Cual es el poder de las instancias por GPU.


Si has llegado hasta aquí, ya sabes cual es la idea detrás de las instancias, pero aún no sabemos para qué sirve, si ves la siguiente gráfica sabrás la diferencia.


Observe que sin aplicar instancias se generan 18 #DrawCalls y aplicando instancias se generan solo 3, en realidad lo que corresponde a los objetos en pantalla seria 16 y 1 draw call respectivamente.


Que es lo que realmente ocurre aquí, en la gráfica a la izquierda Unity simplemente ignora lo que hemos hecho y aunque es el mismo material, simplemente crea copias del material matando el rendimiento.


En la imagen de la derecha reconoce que el material está preparado para la instanciación y lo aplica y de golpe obtenemos que en un solo evento y esto suena muy alentador, pero nos encontramos con el primer fallo quizás.


Si observa el tiempo que tarda la CPU y la GPU en renderzar el cuadro, el tiempo es casi el mismo y puede reclamarme qué la muestra no es representativa y tiene razón, aquí les muestro una gráfica de una muestra mas grande.


La imagen no es mía, pero ilustra el mismo punto, en el gráfico de la izquierda se ve que solo tiene 13 draw calls, mientras el otro tiene 4800 draw calls y el rendimiento cae por la mitad.


Pero unity no nos está contando todo, es verdad que el rendimiento que se gana es enorme, pero la carga la está tomando la CPU, porque debe listar todas las mayas y sus propiedades, que luego envía a la GPU, lo único que debe hacer la GPU es renderizar un objeto y de ahí instanciar. Mi punto es que eso tiene un costo y no es una formula mágica que nos quita todos los problemas.

Bien y que pasa si queremos agregar un objeto distinto a nuestra escena. Abriremos el frame debugger que se encuentra en window/Analysis/Frame Debugger.

Aunque ahorra algunos drawcalls, las instancias se parte en dos, ahora si pruebas distintos objetos de distintos materiales, simplemente se rompe aunque los materiales de los otros objetos también tengan habilitado las instancias.


También hay que anotar que unity te pone a elegir entre materiales con instancias o game objects estáticos, parece que ambos al tiempo no funcionan bien.


Si bien es una técnica con mucho potencial, veo que su estabilidad se rompe muy fácil y podría realmente no ser de tanta utilidad, por lo general esta técnica se usa en objetos que se repiten muy a menudo como la hierba o en las partículas que ya viene directamente implementado las instancias.


No existe una forma definitiva para mejorar el rendimiento, es bueno conocer todas las técnicas posibles y probarlas, porque realmente la optimización se trata de buscar la mejor combinación de técnicas que benefician casos particulares.


No siendo mas nos vemos en el siguiente blog, Hasta otra.

40 visualizaciones0 comentarios

Comments


bottom of page