O que você está procurando?

Dicas de otimização e solução de problemas das variantes de shader do Unity

ATTILIO CAROTENUTO / UNITYTechnical Lead
May 28, 2024|15 Mínimo
Dicas de otimização e solução de problemas das variantes de shader do Unity
Esta página da Web foi automaticamente traduzida para sua conveniência. Não podemos garantir a precisão ou a confiabilidade do conteúdo traduzido. Se tiver dúvidas sobre a precisão do conteúdo traduzido, consulte a versão oficial em inglês da página da Web.

Ao escrever shaders no Unity, temos a capacidade de incluir vários recursos, passes e lógica de ramificação em um único arquivo de origem. No momento da compilação, os arquivos de origem do shader são compilados em programas de shader, que contêm uma ou mais variantes. Uma variante é uma versão desse shader que segue um único conjunto de condições, resultando (na maioria dos casos) em um caminho de execução linear sem condicionais de ramificação estática.

O motivo pelo qual usamos variantes, em vez de manter os caminhos de ramificação em um único shader, é que as GPUs são ótimas em paralelizar códigos previsíveis e que sempre seguem o mesmo caminho, resultando em maior rendimento. Se as condicionais estiverem presentes no programa de sombreamento compilado, a GPU precisará gastar recursos realizando tarefas preditivas, aguardando que os outros caminhos sejam concluídos e assim por diante, introduzindo ineficiências.

Embora isso leve a um desempenho significativamente melhor da GPU em comparação com a ramificação dinâmica, também tem algumas desvantagens. Os tempos de construção ficarão mais longos à medida que o número de variantes aumentar, às vezes até mesmo em várias horas por construção. O jogo também demorará mais para ser inicializado, pois precisará gastar mais tempo carregando e pré-aquecendo os shaders. Por fim, o senhor poderá notar um uso significativo de memória em tempo de execução dos shaders se as variantes não forem gerenciadas adequadamente, às vezes acima de 1 GB.

A quantidade de variantes geradas aumenta dependendo de uma variedade de fatores, incluindo palavras-chave e propriedades definidas, configurações de qualidade, níveis gráficos, APIs gráficas habilitadas, efeitos de pós-processamento, pipeline de renderização ativo, modos de iluminação e neblina e se o XR está habilitado, entre outros. Os shaders que resultam em um grande número de variantes são geralmente chamados de uber shaders. Em tempo de execução, o Unity carrega a variante que corresponde às configurações e palavras-chave necessárias, conforme abordaremos mais adiante.

Isso é particularmente impactante quando o senhor considera que frequentemente vemos shaders com mais de 100 palavras-chave, o que leva a um número incontrolável de variantes resultantes, muitas vezes chamadas de explosão de variantes de shader. Não é incomum ver shaders com um espaço de variante inicial na casa dos milhões antes de qualquer filtragem ser aplicada.

Para aliviar isso, a Unity tentará reduzir a quantidade de variantes geradas com base em algumas passagens de filtragem. Por exemplo, se o XR não estiver ativado, as variantes necessárias para isso normalmente serão removidas. Em seguida, a Unity leva em consideração os recursos que o senhor está realmente usando em suas cenas, como modos de iluminação, neblina e assim por diante. Esses problemas são particularmente difíceis de detectar, pois os desenvolvedores e artistas podem introduzir alterações aparentemente seguras que, na verdade, levam a um aumento significativo nas variantes de sombreamento, sem nenhuma maneira óbvia de detectar, a menos que o senhor coloque algumas proteções como parte do pipeline de implantação.

Embora isso seja útil, esse processo não é perfeito, e há muito que podemos fazer para eliminar o maior número possível de variantes sem afetar a qualidade visual do seu jogo.

Aqui, gostaria de compartilhar algumas dicas práticas sobre como lidar com as variantes, entender de onde elas vêm e algumas maneiras eficazes de reduzi-las. O tempo de construção do seu projeto e o espaço de memória serão muito beneficiados como resultado.

Compreensão do impacto das palavras-chave nas variantes

As variantes de shader são geradas com base em todas as combinações possíveis das palavras-chave shader_feature e multi_compile usadas em seu shader, entre outros fatores. As palavras-chave marcadas como multi_compile são sempre incluídas em sua compilação, enquanto as marcadas como shader_feature serão incluídas se forem referenciadas por qualquer material em seu projeto. Por esse motivo, o senhor deve usar shader_feature sempre que possível.

Para ver quais palavras-chave estão definidas em um shader, o senhor pode selecioná-lo e verificar o Inspector.

Palavras-chave na visualização do Shader Inspector

Como o senhor pode ver, as palavras-chave são divididas em substituíveis e não substituíveis. As palavras-chave Localization (as definidas no arquivo de sombreamento real) com escopo global podem ser substituídas por uma palavra-chave de sombreamento global com um nome correspondente. Se, em vez disso, eles forem definidos em um escopo local (usando multi_compile_local ou shader_feature_local), não poderão ser substituídos e aparecerão na seção Not overridable (Não substituível) abaixo. As palavras-chave globais do shader são fornecidas pelo mecanismo Unity e podem ser substituídas. Como elas podem ser adicionadas em qualquer ponto do processo de compilação, nem todas as palavras-chave globais podem aparecer nessa lista.

As palavras-chave podem ser definidas em grupos mutuamente exclusivos, chamados de conjuntos, definindo-os na mesma diretiva. Ao fazer isso, o senhor evita gerar variantes para combinações de palavras-chave que nunca serão ativadas ao mesmo tempo (como dois tipos diferentes de iluminação ou neblina).

#pragma shader_feature LIGHT_LOW_Q LIGHT_HIGH_Q

Para reduzir a quantidade de palavras-chave por plataforma, o senhor pode usar macros de pré-processamento para defini-las apenas para a plataforma relevante, por exemplo:

#ifdef SHADER_API_METAL
   #pragma shader_feature IOS_FOG_FEATURE
#else
   #pragma shader_feature BASE_FOG_FEATURE
#endif

Observe que essas expressões com macros não podem depender de outras palavras-chave ou recursos que não estejam relacionados apenas ao destino da compilação.

As palavras-chave também podem ser limitadas a uma passagem específica, reduzindo a quantidade de combinações possíveis. Para fazer isso, o senhor pode adicionar um dos seguintes sufixos à diretiva:

  • _vertex
  • _fragmento
  • _hull
  • _domain
  • _geometry
  • _raytracing

Por exemplo:

#pragma shader_feature_fragment FRAG_FEATURE_1 FRAG_FEATURE_2

Isso pode se comportar de forma diferente dependendo do renderizador que o senhor estiver usando. Por exemplo, no OpenGL, os sufixos OpenGL ES e Vulkan serão ignorados.

O senhor pode usar a diretiva #pragma skip_variants para definir palavras-chave que devem ser excluídas ao gerar variantes para esse shader específico. Ao fazer a compilação do jogador, todas as variantes de shader para esse shader que contenham uma dessas palavras-chave serão ignoradas.

Opcionalmente, o senhor também pode definir palavras-chave usando a diretiva #pragma dynamic_branch, que forçará a Unity a confiar na ramificação dinâmica e a não gerar variantes para essas palavras-chave. Embora isso reduza a quantidade de variantes resultantes, pode levar a um desempenho mais fraco da GPU, dependendo do sombreador e do conteúdo do jogo, por isso é recomendável criar um perfil adequado ao usá-lo.

Inspeção do código do shader gerado

Normalmente, as variantes de shader não serão compiladas até que o senhor realmente crie o jogo. Usando essa opção, o senhor pode inspecionar as variantes de shader resultantes para uma plataforma de compilação ou API gráfica específica. Isso permite que o senhor verifique se há erros com antecedência. Além disso, o senhor pode colar o código gerado em ferramentas de análise de desempenho de shader de GPU, como o PVRShaderEditor, para otimizações adicionais.

Palavras-chave na visualização do Shader Inspector

Na parte inferior, o senhor notará uma entrada informando quantas variantes estão incluídas, com base nos materiais presentes na cena aberta no momento, sem a aplicação de nenhuma remoção de script. Se o senhor pressionar o botão Show, será exibido um arquivo temporário com algumas informações adicionais de depuração sobre quais palavras-chave foram usadas ou removidas em várias plataformas, incluindo o número de variantes de estágio de vértice.

A caixa de seleção Preprocess only acima permite que o senhor alterne entre o código do shader compilado e o código-fonte do shader pré-processado para facilitar e agilizar a depuração.

Se estiver usando o Pipeline de Renderização Integrado e trabalhando com um sombreador de superfície, o senhor tem a opção de verificar o código gerado que a Unity usará para substituir sua fonte de sombreamento simplificada ao compilar. Opcionalmente, o senhor pode substituir a fonte do sombreador pelo código gerado, se quiser modificar a saída.

Mostrar opção de código gerado para um sombreador de superfície Texto alternativo: Ativação da opção Mostrar código gerado para um sombreador de superfície
Determinação de quais variantes são geradas no momento da construção

Ao criar o jogo, a Unity determinará o espaço de variantes para cada shader com base em todas as permutações possíveis de seus recursos, configurações da engine e outros fatores. Essas combinações são então passadas para os pré-processadores para várias passagens de remoção. Isso pode ser estendido com o uso de retornos de chamada IPreprocessShaders para criar uma lógica personalizada para remover mais variantes da compilação, conforme abordado abaixo.

Os shaders incluídos como parte da lista de shaders Always-included (em Project Settings > Graphics) terão todas as suas variantes incluídas na compilação. Por esse motivo, é melhor usar esse recurso somente quando for estritamente necessário, pois ele pode facilmente levar à geração de um grande número de variantes.

Por fim, o pipeline de compilação passará por um processo chamado de deduplicação, identificando variantes idênticas dentro do mesmo Pass e garantindo que elas apontem para o mesmo bytecode. Isso resultará na redução do tamanho em disco, mas variantes idênticas ainda afetarão negativamente o tempo de compilação, o tempo de carregamento e o uso de memória em tempo de execução, portanto, não é um substituto para a remoção adequada de variantes.

Após uma compilação bem-sucedida, podemos examinar o arquivo Editor.log para coletar algumas informações úteis sobre quais variantes de shaders foram incluídas na compilação. Para fazer isso, procure no arquivo de registro por "Compiling shader" (Compilando sombreador) e o nome do seu sombreador. Veja, por exemplo, como fica:

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

Em certos casos, o senhor poderá ver a quantidade de variantes aumentar após a etapa de filtragem de configurações, por exemplo, se o projeto tiver o XR ativado.

Se o seu jogo for compatível com várias APIs gráficas, o senhor também encontrará informações para cada renderizador compatível:

Serialized binary data for shader GameShaders/MyShader in 0.00s
	gles3 (total internal programs: 290, unique: 193)
	vulkan (total internal programs: 290, unique: 193)

Por fim, o senhor verá esses logs de compactação que lhe darão uma indicação do tamanho final, em disco, do shader para uma API gráfica específica:

Compressed shader 'GameShaders/MyShader' on vulkan from 1.35MB to 0.19MB

Se estiver usando o Pipeline de Renderização Universal (URP), o usuário poderá selecionar se deseja que os logs sejam gerados somente a partir de shaders SRP, de todos os shaders ou desativar os logs. Para isso, selecione o nível de registro em Configurações do projeto > Gráficos > Configurações globais de URP.

Definição do nível de registro nas configurações globais da URP

Além disso, se o senhor selecionar a opção Export Shader Variants abaixo, será gerado um arquivo JSON após a compilação que contém um relatório das compilações de variantes de shader. Isso está disponível no Unity 2022.2 ou mais recente.

Determinação de quais variantes são usadas em tempo de execução

Para saber quais shaders são realmente compilados para a GPU em tempo de execução, o senhor pode ativar a opção Log Shader Compilation, em Project Settings > Graphics.

Ativação da compilação do Log Shader nas configurações do projeto gráfico

Isso fará com que o jogo seja impresso nos registros do jogador sempre que um shader for compilado enquanto o usuário joga. Ele só funcionará em compilações de desenvolvimento e no modo de depuração, conforme descrito na dica de ferramenta.

O formato é o seguinte:

Compiled Shader: Folder/ShaderName, pass: PASS_NAME, stage: STAGE_NAME, keywords ACTIVE_KEYWORD_1 ACTIVE_KEYWORD_2

Lembre-se de que algumas plataformas, como o Android, armazenam em cache os shaders compilados. Por esse motivo, talvez o senhor precise desinstalar e reinstalar o jogo antes de fazer um teste para capturar todos os shaders compilados.

Por fim, o senhor pode usar o pacote Memory Profiler para tirar um instantâneo do seu jogo durante a execução e ter uma visão geral de quais shaders estão carregados na memória no momento e o tamanho deles. A classificação por tamanho normalmente dá uma boa indicação de quais shaders estão trazendo mais variantes e vale a pena otimizá-los.

Visão geral dos shaders no Memory Profiler
Stripping baseado em configurações gráficas

Como parte dos passes de remoção, a Unity removerá as variantes de shader relacionadas aos recursos gráficos que seu jogo não está usando. O processo muda um pouco se o senhor estiver usando o Pipeline de Renderização Integrado ou URP.

Para defini-las, vá para Project Settings > Graphics (Configurações do projeto > Gráficos). A partir daqui, ao usar o Pipeline de Renderização Integrado, o senhor pode selecionar os modos Lightmap e Fog compatíveis com o seu jogo.

Configurações de decapagem do sombreador gráfico

Configurá-las como Automatic permite que a Unity determine quais variantes devem ser removidas com base nas cenas incluídas em sua compilação.

Se não tiver certeza de quais recursos está usando, também é possível usar o botão Import from Current Scene (Importar da cena atual) para permitir que a Unity descubra quais recursos são necessários. É claro que isso só é útil se todas as suas cenas estiverem usando as mesmas configurações, portanto, certifique-se de selecionar uma cena representativa ao usar essa opção.

Se o senhor estiver usando o URP, algumas dessas opções ficarão ocultas. Em vez disso, o senhor poderá definir os recursos necessários para o seu jogo diretamente no ativo Pipeline Settings.

Por exemplo, a desativação do Terrain Holes fará com que todas as variantes do Terrain Holes Shader sejam removidas, reduzindo também o tempo de construção.

O URP oferece um controle mais granular sobre os recursos que o usuário deseja incluir no jogo, o que pode resultar em compilações mais otimizadas com menos variantes não utilizadas.

Stripping baseado em camadas de gráficos

Observação: Isso só é relevante ao usar o Pipeline de Renderização Integrado. Essas configurações serão ignoradas ao usar um pipeline de renderização com script, como o URP.

As camadas de gráficos são usadas para aplicar diferentes configurações de gráficos com base no hardware em que o jogo está sendo executado (não confundir com as configurações de qualidade). Quando o jogo for iniciado, o Unity determinará o nível gráfico do seu dispositivo com base nos recursos de hardware, na API de gráficos e em outros fatores.

Elas podem ser definidas em Project Settings > Graphics > Tier Settings.

Configurações de nível gráfico

Com base nisso, a Unity adiciona essas três palavras-chave a todos os shaders:

UNITY_HARDWARE_TIER1

UNITY_HARDWARE_TIER2

UNITY_HARDWARE_TIER3

Em seguida, ele gera variantes de shader para cada uma das camadas gráficas definidas. Se o senhor não estiver usando camadas de gráficos e quiser evitar as variantes relacionadas a elas, precisará garantir que todas as camadas de gráficos estejam definidas exatamente com as mesmas configurações para que o Unity ignore essas variantes.

Conforme mencionado anteriormente, a Unity tentará desduplicar as variantes idênticas, portanto, se, por exemplo, duas das três camadas tiverem as mesmas configurações, isso levará a uma redução no tamanho do disco, embora todas as variantes ainda sejam geradas. Opcionalmente, o senhor pode forçar a Unity a gerar variantes de camada para um determinado shader e API de renderização gráfica, usando hardware_tier_variants, conforme mostrado abaixo:

// Direct3D 11/12
#pragma hardware_tier_variants d3d11 
Decapagem baseada em APIs gráficas

A Unity compila um conjunto de variantes de shader para cada API gráfica incluída em sua compilação, portanto, em alguns casos, é vantajoso selecionar manualmente as APIs e excluir as que o senhor não precisa.

Para fazer isso, vá para Project Settings > Player. Por padrão, a opção Auto Graphics API está selecionada, e o Unity incluirá um conjunto de APIs gráficas integradas e escolherá uma em tempo de execução, dependendo dos recursos do dispositivo. Por exemplo, no Android, a Unity tentará usar o Vulkan primeiro e, se o dispositivo não for compatível com ele, o mecanismo voltará para o GLES3.2, GLES3.1 ou GLES3.0 (as variantes serão idênticas nessas versões do GLES).

Em vez disso, desative a Auto Graphics API para a plataforma relevante e selecione manualmente as APIs que deseja incluir. A Unity dará prioridade ao primeiro da lista.

Desative o Auto Graphics API para selecionar suas APIs preferidas

A desvantagem é que o número de dispositivos compatíveis com o jogo pode ser limitado, portanto, certifique-se de saber o que está fazendo ao alterar isso e teste em vários dispositivos.

Correspondência estrita da variante do shader

Normalmente, em tempo de execução, o Unity tenta carregar a variante que mais se aproxima do conjunto de palavras-chave solicitadas se uma correspondência exata não estiver disponível ou tiver sido removida da compilação do player. Embora isso seja conveniente, também esconde possíveis problemas com a configuração das palavras-chave do sombreador.

A partir do Unity 2022.3, o senhor pode selecionar Strict Shader Variant Matching em Project Settings > Player para garantir que o Unity só tente carregar a correspondência exata para a combinação de palavras-chave locais e globais de que o senhor precisa.

Habilitar Strict Shader Variant Matching nas configurações do projeto

Se não for encontrado, ele usará o Error Shader e imprimirá um erro no console contendo o shader, o índice do subshader, a passagem real e as palavras-chave solicitadas. Isso é muito útil quando o senhor precisa rastrear as variantes ausentes de que realmente precisa. Como de costume com a remoção, isso só funciona no Player e não tem impacto no Editor.

Exportação de variantes usadas para uma Shader Variants Collection

Ao jogar o jogo no Editor, o Unity controla quais shaders e variantes estão em uso no momento na sua cena e permite que o senhor exporte isso para uma coleção. Para isso, navegue até Project Settings > Graphics (Configurações do projeto > Gráficos). Na parte inferior, o senhor verá uma seção Shader Loading, que mostra quantos shaders estão ativos no momento.

Certifique-se de clicar em Limpar antes para ter uma amostra mais precisa e, em seguida, entre no modo Reproduzir e se envolva com a cena, garantindo que você encontre todos os elementos do jogo que exigem shaders específicos. Isso aumentará os contadores rastreados. Em seguida, pressione o botão "Save to asset..." para salvar todos eles em um ativo de coleção.

O botão Salvar no ativo

As Collections de variantes de shader são ativos que contêm uma lista de shaders e variantes relacionadas. Eles são normalmente usados para predefinir quais variantes o senhor deseja incluir na compilação e para pré-aquecer os shaders.

Adicionando um shader a uma coleção de variantes de shader

Uma abordagem usada em alguns projetos é executar isso para cada nível do jogo, salvando uma coleção para cada um deles e, em seguida, removendo as variantes que não estão presentes em nenhuma dessas listas usando um script IPreprocessShaders (abordado na próxima seção). Embora isso seja conveniente, na minha experiência, também é bastante propenso a erros. É difícil garantir que o senhor encontre todas as variantes necessárias em um único jogo, e alguns dos recursos podem ser carregados apenas no dispositivo e em casos específicos, resultando em uma lista que não é necessariamente precisa. Conforme o jogo muda e novos elementos são adicionados aos níveis ou os materiais mudam, as Collections precisarão ser atualizadas. Por esse motivo, eu o usaria principalmente para fins de depuração e investigação, em vez de integrá-lo diretamente ao seu pipeline de compilação.

Eliminação de variantes de shader com script

Sempre que um shader estiver prestes a ser compilado no jogo, o Unity enviará um retorno de chamada. Isso acontece nas compilações de Player e Asset Bundles. Podemos ouvi-los de forma conveniente usando IPreprocessShaders.OnProcessShader e IPreprocessComputeShaders.OnProcessComputeShader (para shaders de computação) e adicionar lógica personalizada para eliminar variantes desnecessárias. Dessa forma, podemos reduzir bastante o tempo de compilação, o tamanho da compilação e o número total de variantes que entram em sua compilação.

Para isso, crie um script que implemente a interface IPreprocessShaders e, em seguida, escreva sua lógica de remoção em OnProcessShader. Por exemplo, aqui está um script que removerá todas as variantes que contenham a palavra-chave do shader DEBUG em compilações de versão:

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);
         }
      }
   }
}

A ordem de retorno de chamada permite que o senhor defina qual script de pré-processamento deve ser executado primeiro, permitindo que crie passes de remoção de várias etapas. Os scripts com prioridade mais baixa serão executados primeiro.

Visite o fórum de discussão Graphics-Shaders para saber mais.