Optimización de Variantes de Shader de Unity y Consejos de Solución de Problemas

Cuando escribiendo shaders en Unity, tenemos la capacidad de incluir múltiples características, pasadas y lógica de ramificación en un solo archivo fuente. En el momento de la construcción, los archivos fuente de shaders se compilan en programas de shaders, que contienen una o más variantes. Una variante es una versión de ese shader que sigue un solo conjunto de condiciones, resultando (en la mayoría de los casos) en un camino de ejecución lineal sin condicionales de ramificación estática.
La razón por la que usamos variantes, en lugar de mantener los caminos de ramificación todos en un solo shader, es porque las GPU son excelentes para paralelizar código que es predecible y siempre sigue el mismo camino, resultando en un mayor rendimiento. Si hay condicionales presentes en el programa de shader compilado, la GPU necesitará gastar recursos realizando tareas predictivas, esperando a que se completen los otros caminos, y así sucesivamente, introduciendo ineficiencias.
Si bien esto conduce a un rendimiento de GPU significativamente mejor en comparación con la ramificación dinámica, también tiene algunas desventajas. Los tiempos de construcción se alargarán a medida que aumenta el número de variantes, a veces incluso por varias horas por construcción. El juego también tardará más en iniciarse, ya que necesitará gastar más tiempo cargando y precalentando shaders. Finalmente, podrías notar un uso significativo de memoria en tiempo de ejecución por parte de los shaders si las variantes no se gestionan adecuadamente, a veces más de 1GB.
La cantidad de variantes generadas aumenta dependiendo de una variedad de factores, incluyendo palabras clave y propiedades definidas, configuraciones de calidad, niveles gráficos, APIs gráficas habilitadas, efectos de post-procesamiento, la tubería de renderizado activa, modos de iluminación y niebla, y si XR está habilitado, entre otros. Los shaders que resultan en un gran número de variantes a menudo se llaman uber shaders. En tiempo de ejecución, Unity carga la variante que coincide con las configuraciones y palabras clave requeridas, como cubriremos más adelante.
Esto es particularmente impactante cuando consideras que a menudo vemos shaders con más de 100 palabras clave, lo que lleva a un número inmanejable de variantes resultantes, a menudo referido como explosión de variantes de shader. No es inusual ver shaders con un espacio de variante inicial en los millones antes de que se aplique cualquier filtrado.
Para aliviar esto, Unity intentará reducir la cantidad de variantes generadas en base a algunos pases de filtrado. Por ejemplo, si XR no está habilitado, las variantes que se necesitan para eso normalmente serán eliminadas. Unity luego toma en cuenta qué características estás utilizando realmente en tus escenas, como modos de iluminación, niebla, y así sucesivamente. Estos son particularmente difíciles de detectar, ya que los desarrolladores y artistas podrían introducir cambios aparentemente seguros que en realidad conducen a un aumento significativo en las variantes de shader, sin una forma obvia de detectar a menos que pongas algunas salvaguardias como parte de tu pipeline de implementación.
Si bien esto es útil, este proceso no es perfecto, y hay mucho que podemos hacer para eliminar tantas variantes como sea posible sin afectar la calidad visual de tu juego.
Aquí, me gustaría compartir algunos consejos prácticos sobre cómo manejar variantes, entender de dónde vienen y algunas formas efectivas de reducirlas. El tiempo de construcción de tu proyecto y la huella de memoria se beneficiarán enormemente como resultado.
Para más información sobre la eliminación de variantes de sombreadores, consulta Reduciendo variantes de sombreadores en el Manual de Unity.
Las variantes de sombreadores se generan, basadas en todas las combinaciones posibles de característica_de_sombreador y multi_compile palabras clave utilizadas en tu sombreador, entre otros factores. Las palabras clave marcadas como multi_compile siempre se incluyen en tu construcción, mientras que aquellas marcadas como característica_de_sombreador se incluirán si son referenciadas por algún material en tu proyecto. Por esta razón, deberías usar característica_de_sombreador siempre que sea posible.
Para ver qué palabras clave están definidas en un sombreador, puedes seleccionarlo y revisar el Inspector.

Como puedes ver, las palabras clave se dividen en Sobrescribibles y No Sobrescribibles. Las palabras clave locales (las definidas en el archivo de sombreador real) con un alcance global pueden ser sobrescritas por una palabra clave de sombreador global con un nombre coincidente. Si en cambio están definidas en un alcance local (usando multi_compile_local o shader_feature_local), no pueden ser sobrescritas y aparecerán en la sección No sobrescribible debajo. Las palabras clave de sombreador global son proporcionadas por el motor de Unity, y son sobrescribibles. Dado que pueden ser añadidas en cualquier momento del proceso de construcción, no todas las palabras clave globales pueden aparecer en esta lista.
Las palabras clave pueden ser definidas en grupos mutuamente excluyentes, llamados conjuntos, al definirlas en la misma directiva. Al hacer esto, evitas generar variantes para combinaciones de palabras clave que nunca se habilitarán al mismo tiempo (como dos tipos diferentes de iluminación o niebla).
#pragma shader_feature LIGHT_LOW_Q LIGHT_HIGH_QPara reducir la cantidad de palabras clave por plataforma, puedes usar macros de preprocesador para definirlas solo para la plataforma relevante, por ejemplo:
#ifdef SHADER_API_METAL
#pragma shader_feature IOS_FOG_FEATURE
#else
#pragma shader_feature BASE_FOG_FEATURE
#endif
Ten en cuenta que estas expresiones con macros no pueden depender de otras palabras clave o características que no estén solo relacionadas con el objetivo de construcción.
Las palabras clave también pueden ser limitadas a un pase específico, reduciendo la cantidad de combinaciones potenciales. Para hacerlo, puedes agregar uno de los siguientes sufijos a la directiva:
- _vertex
- _fragment
- _hull
- _domain
- _geometry
- _raytracing
Por ejemplo:
#pragma shader_feature_fragment FRAG_FEATURE_1 FRAG_FEATURE_2Esto puede comportarse de manera diferente dependiendo del renderizador que estés usando. Por ejemplo, en OpenGL, OpenGL ES y Vulkan, los sufijos serán ignorados.
Puedes usar la directiva #pragma skip_variants para definir palabras clave que deben ser excluidas al generar variantes para ese shader específico. Al hacer tu compilación de jugador, todas las variantes de shader para ese shader que contengan una de esas palabras clave serán omitidas.
También puedes definir opcionalmente palabras clave usando la directiva #pragma dynamic_branch, que obligará a Unity a depender de ramificación dinámica y no generar variantes para esas palabras clave. Si bien esto reduce la cantidad de variantes resultantes, puede llevar a un rendimiento más débil de la GPU dependiendo del shader y el contenido del juego, por lo que se recomienda perfilar adecuadamente al usarlo.
Para más información sobre palabras clave de shader, consulta Cambiando cómo funcionan los shaders usando palabras clave en el Manual de Unity.
Normalmente, las variantes de shader no se compilarán hasta que realmente construyas el juego. Usando esta opción, puedes inspeccionar las variantes de shader resultantes para una plataforma de compilación específica o API gráfica. Esto te permite verificar errores con anticipación. Además, puedes pegar el código generado en herramientas de análisis de rendimiento de shaders de GPU, como PVRShaderEditor, para optimizaciones adicionales.

En la parte inferior, notarás una entrada que dice cuántas variantes están incluidas, basadas en los materiales presentes en la escena actualmente abierta, sin ninguna eliminación scriptable aplicada. Si presionas el botón Mostrar, mostrará un archivo temporal con información de depuración adicional sobre qué palabras clave se usaron o se eliminaron en varias plataformas, incluyendo el número de variantes de etapa de vértice.
La casilla de verificación Solo preprocesar arriba te permite alternar entre el código de shader compilado y el código fuente de shader preprocesado para una depuración más fácil y rápida.
Si estás utilizando el Render Pipeline incorporado y trabajando con un shader de superficie, tienes la opción de verificar el código generado que Unity utilizará para reemplazar tu fuente de shader simplificada cuando construyas. Luego puedes opcionalmente reemplazar tu fuente de shader con el código generado, si deseas modificar la salida.
Para más información, consulta Ver cuántas variantes de shader tienes en el Manual de Unity.

Al construir el juego, Unity determinará el espacio de variantes para cada shader basado en todas las posibles permutaciones de sus características, configuraciones del motor y otros factores. Estas combinaciones se pasan luego a los preprocesadores para múltiples pasadas de eliminación. Esto se puede extender utilizando callbacks de IPreprocessShaders para crear lógica personalizada para eliminar más variantes de la construcción, como se cubre a continuación.
Los shaders que se incluyen como parte de la lista de Shaders siempre incluidos (bajo Configuración del Proyecto > Gráficos) tendrán todas sus variantes incluidas en la construcción. Por esta razón, es mejor usar esto solo cuando sea estrictamente necesario, ya que puede llevar fácilmente a que se generen un gran número de variantes.
Finalmente, el pipeline de construcción pasará por un proceso llamado deduplicación, identificando variantes idénticas dentro del mismo Pase y asegurando que apunten al mismo bytecode. Esto resultará en un tamaño reducido en disco, pero las variantes idénticas aún afectarán negativamente el tiempo de construcción, el tiempo de carga y el uso de memoria en tiempo de ejecución, por lo que no es un reemplazo para una adecuada eliminación de variantes.
Después de una construcción exitosa, podemos revisar el archivo Editor.log para recopilar información útil sobre qué variantes de shaders se incluyeron en la construcción. Para hacerlo, busca en el archivo de registro "Compilando shader" y el nombre de tu shader. Aquí hay un ejemplo de cómo se ve:
Compiling shader "GameShaders/MyShader" pass "Pass 1" (vp)
Full variant space: 608
After settings filtering: 608
After built-in stripping: 528
After scriptable stripping: 528
Processed in 0.00 seconds
starting compilation...
finished in 0.02 seconds. Local cache hits 528 (0.16s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants
En ciertos casos, podrías ver que la cantidad de variantes aumenta después del paso de filtrado de configuraciones, por ejemplo, si tu proyecto tiene XR habilitado.
Si tu juego soporta múltiples APIs gráficas, también encontrarás información para cada renderizador soportado:
Serialized binary data for shader GameShaders/MyShader in 0.00s
gles3 (total internal programs: 290, unique: 193)
vulkan (total internal programs: 290, unique: 193)Finalmente, verás estos registros de compresión que te darán una indicación del tamaño final, en disco, del shader para una API gráfica específica:
Compressed shader 'GameShaders/MyShader' on vulkan from 1.35MB to 0.19MBSi estás utilizando el Universal Render Pipeline (URP), puedes seleccionar si deseas que se generen registros solo de shaders SRP, de todos los shaders, o desactivar los registros. Para hacerlo, selecciona el Nivel de Registro en Configuración del Proyecto > Gráficos > Configuración Global de URP.

Además, si seleccionas la opción Exportar Variantes de Sombreado a continuación, se generará un archivo JSON después de tu compilación que contiene un informe de las compilaciones de variantes de sombreado. Esto está disponible en Unity 2022.2 o más reciente.
Para entender qué sombreadores se compilan realmente para la GPU en tiempo de ejecución, puedes habilitar la opción Registro de Compilación de Sombreadores, en Configuración del Proyecto > Gráficos.

Esto hará que tu juego imprima en los registros del jugador cada vez que se compile un sombreador mientras juegas. Solo funcionará en compilaciones de desarrollo y en modo Depuración, como se describe en el tooltip.
El formato se ve así:
Compiled Shader: Folder/ShaderName, pass: PASS_NAME, stage: STAGE_NAME, keywords ACTIVE_KEYWORD_1 ACTIVE_KEYWORD_2Ten en cuenta que algunas plataformas, como Android, almacenarán en caché los sombreadores compilados. Por esta razón, es posible que necesites desinstalar y reinstalar el juego antes de hacer una prueba para capturar todos los sombreadores compilados.
Finalmente, puedes usar el paquete Memory Profiler para tomar una instantánea de tu juego mientras se está ejecutando, y luego tener una visión general de qué sombreadores están actualmente cargados en memoria y su tamaño. Ordenar por tamaño normalmente da una buena indicación de qué sombreadores están trayendo más variantes y valen la pena optimizar.

Como parte de los pasos de eliminación, Unity eliminará variantes de sombreadores relacionadas con características gráficas que tu juego no está utilizando. El proceso cambia ligeramente si estás utilizando el Pipeline de Renderizado Integrado o URP.
Para definir esos, ve a Configuración del Proyecto > Gráficos. Desde aquí, mientras usas el Pipeline de Renderizado Integrado, puedes seleccionar qué modos de Mapa de Luz y Niebla soporta tu juego.

Establecerlos en Automático permite que Unity determine qué variantes eliminar según las escenas incluidas en tu compilación.
Si no estás seguro de qué características estás utilizando, también puedes usar el botón Importar desde la escena actual para que Unity determine qué características necesitas. Por supuesto, esto solo es útil si todas tus escenas están utilizando la misma configuración, así que asegúrate de seleccionar una escena representativa al usar esta opción.
Si estás utilizando URP, algunas de estas opciones estarán ocultas. En su lugar, podrás definir qué características requiere tu juego directamente en el activo de Configuración de Pipeline.
Por ejemplo, deshabilitar Huecos de Terreno hará que se eliminen todas las variantes de sombreadores de Huecos de Terreno, reduciendo también el tiempo de compilación.
URP proporciona un control más granular sobre qué características deseas incluir en tu juego, lo que puede resultar en compilaciones más optimizadas con menos variantes no utilizadas.
Nota: Esto solo es relevante al usar el Pipeline de Renderizado Integrado. Estas configuraciones serán ignoradas al usar un pipeline de renderizado scriptable como URP.
Los niveles gráficos se utilizan para aplicar diferentes configuraciones gráficas según el hardware en el que se esté ejecutando tu juego (no confundir con las Configuraciones de Calidad). Cuando el juego comienza, Unity determinará el nivel gráfico de tu dispositivo según las capacidades de hardware, la API gráfica y otros factores.
Se pueden establecer en Configuración del Proyecto > Gráficos > Configuración de Niveles.

Basado en esto, Unity agrega estas tres palabras clave a todos los sombreadores:
UNITY_HARDWARE_TIER1
UNITY_HARDWARE_TIER2
UNITY_HARDWARE_TIER3
Luego genera variantes de sombreadores para cada uno de los niveles gráficos definidos. Si no estás utilizando niveles gráficos y deseas evitar las variantes relacionadas con ellos, debes asegurarte de que todos los niveles gráficos estén configurados exactamente con los mismos ajustes para que Unity omita estas variantes.
Como se mencionó anteriormente, Unity intentará deduplicar variantes que son idénticas, por lo que, si, por ejemplo, dos de los tres niveles tienen los mismos ajustes, esto llevará a una reducción en el tamaño en disco, aunque todas las variantes aún se generarán. Puedes forzar opcionalmente a Unity a generar variantes de nivel para un sombreador y una API de renderizado gráfico dados, utilizando hardware_tier_variants como se muestra a continuación:
// Direct3D 11/12
#pragma hardware_tier_variants d3d11 Para más información, consulta Gráficos niveles en el Pipeline de Renderizado Integrado en el Manual de Unity.
Unity compila un conjunto de variantes de sombreadores para cada API gráfica incluida en tu compilación, por lo que en algunos casos, es beneficioso seleccionar manualmente las APIs y excluir las que no necesitas.
Para hacerlo, ve a Configuración del Proyecto > Jugador. Por defecto, se selecciona la API Gráfica Automática, y Unity incluirá un conjunto de APIs gráficas integradas y elegirá una en tiempo de ejecución dependiendo de las capacidades del dispositivo. Por ejemplo, en Android, Unity intentará usar Vulkan primero, y si el dispositivo no lo soporta, el motor volverá a GLES3.2, GLES3.1 o GLES3.0 (las variantes serán idénticas en esas versiones de GLES).
En su lugar, desactiva la API Gráfica Automática para la plataforma relevante y selecciona manualmente las APIs que deseas incluir. Unity dará prioridad a la primera en la lista.

La desventaja es que podrías limitar la cantidad de dispositivos que soportan tu juego, así que asegúrate de saber lo que estás haciendo al cambiar esto y prueba en una variedad de dispositivos.
Normalmente, en tiempo de ejecución, Unity intenta cargar la variante que está más cerca del conjunto de palabras clave solicitadas si no hay una coincidencia exacta disponible o ha sido eliminada de la compilación del jugador. Si bien esto es conveniente, también oculta problemas potenciales con la configuración de palabras clave de tu sombreador.
Desde Unity 2022.3, puedes seleccionar Coincidencia Estricta de Variantes de Sombreadores en Configuración del Proyecto > Jugador para asegurarte de que Unity solo intente cargar la coincidencia exacta para la combinación de palabras clave locales y globales que necesitas.

Si no se encuentra, utilizará el Sombreador de Error e imprimirá un error en la consola que contiene el sombreador, el índice del sub-sombreador, el pase real y las palabras clave solicitadas. Esto es bastante útil cuando necesitas rastrear variantes faltantes que realmente necesitas. Como suele suceder con la eliminación, esto solo funciona en el Reproductor y no tiene impacto en el Editor.
Mientras juegas el juego en el Editor, Unity rastrea qué shaders y variantes están actualmente en uso en tu escena y te permite exportar eso a una colección. Para hacerlo, navega a Configuración del Proyecto > Gráficos. En la parte inferior, notarás una sección de Carga de Shaders, que muestra cuántos shaders están actualmente rastreados como activos.
Asegúrate de presionar Limpiar antes para tener una muestra más precisa, luego entra en modo de Juego e interactúa con tu escena, asegurándote de encontrar todos los elementos del juego que requieren shaders específicos. Esto aumentará los contadores rastreados. Luego, presiona el botón “Guardar como activo…” para guardar todos esos en un activo de colección.
Para más información, consulta Crear una colección de variantes de shader en el Manual de Unity.

Las Colecciones de Variantes de Shader son activos que contienen una lista de shaders y variantes relacionadas. Se utilizan comúnmente para predefinir qué variantes deseas incluir en tu compilación y para precalentar shaders.

Un enfoque utilizado en algunos proyectos es ejecutar esto para cada nivel del juego, guardando una colección para cada uno de ellos, luego eliminando cualquier variante que no esté presente en ninguna de esas listas utilizando un script IPreprocessShaders (cubierto en la siguiente sección). Si bien esto es conveniente, en mi experiencia también es bastante propenso a errores. Es difícil asegurarse de que encuentres todas las variantes requeridas en una sola partida, y algunas de las características pueden cargarse solo en el dispositivo y en casos específicos, lo que resulta en una lista que no es necesariamente precisa. A medida que tu juego cambia y se agregan nuevos elementos a los niveles o cambian los materiales, las colecciones necesitarán ser actualizadas. Por esta razón, usaría esto principalmente para depuración e investigación, en lugar de integrarlo directamente en tu canal de compilación.
Para más información, consulta Crear una colección de variantes de shader en el Manual de Unity.
Siempre que un shader esté a punto de ser compilado en tu construcción del juego, Unity enviará una llamada de retorno. Esto sucede tanto en construcciones de Player como de Asset Bundles. Podemos escuchar convenientemente estos usando IPreprocessShaders.OnProcessShader e IPreprocessComputeShaders.OnProcessComputeShader (para shaders de cómputo), y agregar lógica personalizada para eliminar variantes innecesarias. De esta manera, podemos reducir significativamente el tiempo de construcción, el tamaño de la construcción y el número total de variantes que entran en tu construcción.
Para hacerlo, crea un script que implemente la interfaz IPreprocessShaders, luego escribe tu lógica de eliminación dentro de OnProcessShader. Por ejemplo, aquí hay un script que eliminará todas las variantes que contengan la palabra clave de shader DEBUG en construcciones de lanzamiento:
public class StripDebugVariantsPreprocessor : IPreprocessShaders
{
public int callbackOrder => 0;
ShaderKeyword keywordToStrip;
public StripDebugVariantsPreprocessor()
{
keywordToStrip = new ShaderKeyword("DEBUG");
}
public void OnProcessShader(Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> data)
{
if (EditorUserBuildSettings.development)
{
return;
}
for (int i = data.Count - 1; i >= 0; i--)
{
if (data[i].shaderKeywordSet.IsEnabled(keywordToStrip))
{
data.RemoveAt(i);
}
}
}
}El orden de las llamadas de retorno te permite definir qué script de preprocesamiento debe ejecutarse primero, permitiéndote crear pases de eliminación de múltiples pasos. Los scripts con una prioridad más baja se ejecutarán primero.
Visita el foro de Gráficos-Shaders para aprender más.
Para más información, consulta las siguientes secciones en el Manual de Unity:
