Blending e Unity Terrain: como se livrar dos cruzamentos e parar de fazer seus olhos doerem

Para ter um mundo realista dentro do jogo, é necessário levar em consideração a interação de várias formas de relevo entre si e com outros modelos. E se as linhas de interseção visíveis entre os modelos 3D prejudicarem a integridade da imagem, vale a pena considerar como eliminá-las. O caso mais comum dessas linhas, que pode ser familiar para muitos, é a interseção de painéis de partículas com geometria opaca.

imagem

Outro exemplo é a composição natural perturbadora da interseção de rochas e vegetação com a superfície da paisagem em cenas ao ar livre.

imagem

Além de vários métodos de suavização (SSAA, MSAA, CSAA, FXAA, NFAA, CMAA, DLAA, TAA, etc.), que mitigam a aparência desafiadora de tais linhas de interseção, mas não corrigem completamente a situação, existem técnicas mais eficazes. Nós os consideraremos.

Mistura de profundidade


O Unity possui uma solução integrada para eliminar interseções visíveis entre partículas transparentes e geometria opaca denominadas partículas macias. Os shaders que suportam esse efeito aumentam ainda mais a transparência das partículas, dependendo de quão pequena é a diferença entre a profundidade do fragmento de partícula e a profundidade da geometria opaca.

imagem
O princípio de operação de partículas macias

Obviamente, para a operação correta de partículas macias, é necessário um buffer de profundidade. No caso de sombreamento diferido, o buffer de profundidade é formado no estágio de renderização de buffers de tela cheia e, levando em consideração o MRT (Múltiplos alvos de renderização, não a tomografia de ressonância magnética), sua presença não é expressa em custos computacionais adicionais.

No caso de sombreamento direto e uso do Unity Legacy Pipeline, era necessário um passe extra para renderizar a geometria opaca para o buffer de profundidade [1] . Essa passagem é ativada atribuindo o valor apropriado à propriedade Camera.depthTextureMode. Esta propriedade não está disponível na janela do inspetor, mas está disponível na API [2] .

Agora você pode implementar sua própria versão do Scriptable Render Pipeline com sombreamento para frente, que com a ajuda do MRT pode renderizar simultaneamente o buffer de profundidade e o buffer de cores.


Eliminando linhas de interseção em shaders que suportam partículas macias

Em geral, não há obstáculos técnicos ao uso do método de mistura de profundidade para eliminar interseções visíveis de modelos 3D com a paisagem:

Ver código
// Blending with depth buffer

#include "UnityCG.cginc"

float BlendStart;
float BlendEnd;
UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);

struct v2f
{
    // ...

    half4 projPos : TEXCOORD0;
};

v2f vert(appdata v)
{
    v2f o;
    UNITY_INITIALIZE_OUTPUT(v2f,o);

    // ...

    o.projPos = ComputeScreenPos(o.pos);
    COMPUTE_EYEDEPTH(o.projPos.z);

    // ...

    return o;
}

fixed4 frag(v2f i) : COLOR
{     
    fixed4 result = 0;
      
    // ... 

    float depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos));
    float sceneZ = LinearEyeDepth(depth);
    float partZ = i.projPos.z;
    float fade = saturate( sceneZ - partZ );
    result.a = smoothstep( BlendStart, BlendEnd, fade );

    // ... 
       
    return result; 
}


No entanto, essa abordagem tem várias desvantagens.

A primeira desvantagem está relacionada ao desempenho. A mistura de profundidade funciona no estágio de mistura de tubos de hardware, ou seja, imediatamente após a rasterização e cálculo do sombreador de fragmento. Nesse estágio, o resultado da execução do shader de fragmento é misturado com o resultado registrado no buffer de saída [3] [4] [5], de acordo com a fórmula predefinida pelas chamadas à API [6] [7] [8] [9] .

Essa é a parte menos progressiva de qualquer pipeline de hardware, no sentido de que funciona exatamente como seu antecessor, há vinte anos. A GPU lê o valor da memória, mistura-o com o valor do shader de fragmento e grava-o novamente na memória.

Também há uma diferença em usar a mistura de profundidade para modelos 3D totalmente transparentes ou parcialmente transparentes. Transparente - por exemplo, outdoors de partículas - mesmo sem se misturar em profundidade, toda a renderização é transparente. No caso de modelos 3D opacos, a transparência real, tangível e visível ao mesclar em profundidade será dotada de apenas um número muito pequeno de fragmentos, enquanto a grande maioria deles permanecerá opaca. Mas o último não significa que a mistura não será usada para sua renderização - ela simplesmente funcionará ociosa.

A segunda desvantagem está relacionada à maneira como a cor da mistura é selecionada. Em resumo, todos os fragmentos que são misturados em um pixel específico da tela ficam em um raio que emana da posição mundial da câmera e passa pela posição mundial desse pixel da tela. Isso, por sua vez, significa que, com qualquer alteração na posição ou orientação da câmera, a paralaxe será observada: fragmentos do modelo 3D localizado mais perto da câmera se moverão mais rapidamente do que fragmentos da paisagem localizados mais longe da câmera [10] [11] . Isso é especialmente visível quando visto de perto, com deslocamento lateral constante da câmera.


Paralaxe lateral ao mover a câmera: fragmentos do modelo 3D são deslocados para uma distância maior em comparação com fragmentos da paisagem


Paralaxe lateral ao mover a câmera: ao fixar a câmera em um fragmento da paisagem, percebe-se a rapidez com que os fragmentos do modelo se

movem.Quando a câmera é girada, o paralaxe é observado imediatamente ao longo de dois eixos das coordenadas da tela. No entanto, na dinâmica isso é menos evidente que o paralaxe lateral.


Paralaxe azimutal quando a câmera é deslocada: é mais difícil para o cérebro reconhecer o padrão de paralaxe quando os fragmentos são deslocados ao longo de dois

eixos. A zona de mesclagem se torna quase invisível quando a direção da visão é perpendicular à superfície normal da paisagem, mas o tamanho dessa zona aumenta rapidamente se você inclinar a câmera para baixo.


Alterando a largura da zona de mesclagem enquanto inclina a câmera

em profundidade A mesclagem pode ser uma boa opção para eliminar as linhas de interseção dos modelos 3D com a paisagem, se não a abundância de artefatos que a acompanham. Este método é mais adequado para efeitos de partículas que não são estáticas e, como regra, não contêm texturas altamente detalhadas; portanto, efeitos de paralaxe não são observados em seus casos.


Mistura do mapa de altura


Outra opção para implementar a mistura de paisagens é usar um mapa de altura, ao qual o Unity fornece acesso através da API TerrainData [12] .

Conhecendo a posição do objeto Terreno e as dimensões do terreno indicadas no TerrainData, e tendo um "mapa de altura" disponível, é possível calcular a altura do terreno em qualquer ponto especificado nas coordenadas do mundo.


Parâmetros de terreno necessários para amostrar o mapa de altura

// Setting up a heightmap and uniforms to use with shaders... 

Shader.SetGlobalTexture(Uniforms.TerrainHeightmap, terrain.terrainData.heightmapTexture);
Shader.SetGlobalVector(Uniforms.HeightmapScale, terrain.terrainData.heightmapScale);
Shader.SetGlobalVector(Uniforms.TerrainSize, terrain.terrainData.size);
Shader.SetGlobalVector(Uniforms.TerrainPos, terrain.transform.position);

Bem, agora, depois de calcular a altura da paisagem, você também pode calcular as coordenadas uv no sombreador para provar o mapa das alturas da paisagem nas coordenadas mundiais.

// Computes UV for sampling terrain heightmap... 

float2 TerrainUV(float3 worldPos)
{
    return (worldPos.xz - TerrainPos.xz) / TerrainSize.xz;
}

Para poder usar o mesmo código em shaders de fragmentos e vértices, a função tex2Dlod é usada para amostragem. Além disso, o mapa de altura não possui níveis de mip, portanto, amostrá-lo com a função tex2D, que calcula automaticamente o nível de mip, é basicamente sem sentido.

// Returns the height of terrain at a given position in world space... 

float TerrainHeight(float2 terrainUV)
{
    float heightmapSample = tex2Dlod(TerrainHeightmap, float4(terrainUV,0,0));
    return TerrainPos.y + UnpackHeightmap(heightmapSample) * HeightmapScale.y * 2;
}

Você pode tentar reproduzir a eliminação de interseções através da transparência sem usar um buffer de profundidade. Isso não resolve outros problemas associados a esse método, mas permite verificar a operacionalidade da mistura usando um mapa de altura.

Ver código
// Blending with terrain heightmap

#include "UnityCG.cginc"

float BlendStart;
float BlendEnd;

sampler2D_float TerrainHeightmap; 
float4 HeightmapScale;
float4 TerrainSize;
float4 TerrainPos;        

struct v2f
{
   // ...

   float3 worldPos : TEXCOORD0;
   float2 heightMapUV : TEXCOORD1;

   // ...
};


v2f vert(appdata v)
{
    v2f o;
    UNITY_INITIALIZE_OUTPUT(v2f,o);
   
    // ...
    
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    o.heightMapUV = TerrainUV(o.worldPos);

    // ...

    return o;
}

fixed4 frag(v2f i) : COLOR
{
    fixed4 result = 0;

    // ... 

    half height = TerrainHeight(i.heightMapUV);
    half deltaHeight = i.worldPos.y - height;
    result.a = smoothstep( BlendStart, BlendEnd, deltaHeight );

    // ... 
       
    return result; 
}





Mistura de profundidade e elevação. A largura da zona de mesclagem difere com os mesmos parâmetros de sombreador.As

ilustrações usam parâmetros de mesclagem idênticos para ambos os métodos. A largura das zonas de mesclagem é visualmente diferente, pois a mesclagem com um mapa de altura não depende do ângulo entre o olhar do observador e o normal da paisagem.

A mesclagem com um mapa de altura é pelo menos em um aspecto melhor do que a mesclagem em profundidade: ela corrige a dependência da mesclagem visível a olho nu no ângulo em que a câmera olha a paisagem. Infelizmente, o efeito de paralaxe ainda será observado.


Paisagismo reconstrução mistura


Para se livrar da paralaxe, você precisa misturar um fragmento do modelo 3D com um fragmento da paisagem verticalmente abaixo dele (a seleção de cores para mixagem nesse caso não depende da posição e orientação da câmera).


Como corrigir a paralaxe: escolhendo um fragmento de paisagem para mesclagem

Claro, estamos falando aqui mais sobre um fragmento de paisagem virtual. Dependendo da posição da câmera, é possível uma situação em que um fragmento da paisagem, com o qual é necessário misturar um fragmento de um modelo 3D, nem caia no campo de visão da câmera. Existe um problema semelhante na renderização de reflexões locais no espaço da tela (SSLR). Consiste no fato de que é impossível renderizar o reflexo de um fragmento que não está na tela [13] .

No caso da paisagem, a cor do fragmento virtual pode ser reconstruída com alta precisão usando texturas auxiliares fornecidas pela API do Unity: mapa normal [14] , mapa de luz [15] , texturas ponderadas para camadas de mesclagem [16] e texturas incluídas em composição das camadas [17] .


Reconstrução de um fragmento da paisagem

Todas as texturas que compõem a paisagem são amostradas de acordo com a mesma UV do mapa de altura. No caso de camadas, as coordenadas para amostragem são ajustadas pelos parâmetros de lado a lado especificados para uma camada específica [18] [19] .

Ver código
// Blending with reconstructed terrain fragments

#include "UnityCG.cginc"

float BlendStart;
float BlendEnd;

sampler2D_float TerrainHeightmapTexture;
sampler2D_float TerrainNormalTexture;
sampler2D TerrainAlphaMap;

float4 HeightmapScale;
float4 TerrainSize;
float4 TerrainPos;
Float4 TerrainLightmap_ST;

UNITY_DECLARE_TEX2D(TerrainSplatMap0);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap0);
half4 TerrainSplatMap0_ST;

UNITY_DECLARE_TEX2D(TerrainSplatMap1);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap1);
half4 TerrainSplatMap1_ST;

UNITY_DECLARE_TEX2D(TerrainSplatMap2);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap2);
half4 TerrainSplatMap2_ST;

UNITY_DECLARE_TEX2D(TerrainSplatMap3);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap3);
half4 TerrainSplatMap3_ST;

struct v2f
{
   // ...

   float3 worldPos : TEXCOORD0;
   float2 heightMapUV : TEXCOORD1;
#if defined(LIGHTMAP_ON)
   float2 modelLightMapUV : TEXCOORD2;
   float2 terrainLightMapUV : TEXCOORD3;
#endif

   // ...
};


v2f vert(appdata v)
{
    v2f o;
    UNITY_INITIALIZE_OUTPUT(v2f,o);
   
    // ...
    
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    o.heightMapUV = TerrainUV(o.worldPos);

#if defined(LIGHTMAP_ON)
    o.modelLightMapUV = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
    o.terrainLightMapUV = o.heightMapUV * TerrainLightmap_ST.xy + TerrainLightmap_ST.zw;
#endif

    // ...

    return o;
}
half3 TerrainNormal(float2 terrainUV)
{
    return tex2Dlod( TerrainNormalTexture, float4(terrainUV,0,0) ).xyz * 2.0 - 1.0;
}

half4 TerrainSplatMap(float2 uv0, float2 uv1, float2 uv2, float2 uv3, half4 control)
{
    half4 splat0 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap0, TerrainSplatMap0, uv0);
    half4 splat1 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap1, TerrainSplatMap1, uv1);
    half4 splat2 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap2, TerrainSplatMap2, uv2);
    half4 splat3 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap3, TerrainSplatMap3, uv3);         
    half4 result = splat0 * control.r + 
                   splat1 * control.g + 
                   splat2 * control.b + 
                   splat3 * control.a;
    return result;
}

half3 TerrainNormalMap(float2 uv0, float2 uv1, float2 uv2, float2 uv3, half4 control)
{
    half4 n0 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap0, TerrainSplatMap0, uv0);
    half4 n1 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap1, TerrainSplatMap1, uv1);
    half4 n2 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap2, TerrainSplatMap2, uv2);
    half4 n3 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap3, TerrainSplatMap3, uv3);
    half3 result = UnpackNormalWithScale(n0, 1.0) * control.r +
                   UnpackNormalWithScale(n1, 1.0) * control.g +
                   UnpackNormalWithScale(n2, 1.0) * control.b +
                   UnpackNormalWithScale(n3, 1.0) * control.a;
    result.z += 1e-5;
    return result;
}

half3 TerrainLightmap(float2 uv, half3 normal)
{
#if defined(LIGHTMAP_ON)
#if defined(DIRLIGHTMAP_COMBINED)
    half4 lm = UNITY_SAMPLE_TEX2D(unity_Lightmap, uv);
    half4 lmd = UNITY_SAMPLE_TEX2D_SAMPLER(unity_LightmapInd, unity_Lightmap, uv);
    half3 result = DecodeLightmapRGBM(lm, unity_Lightmap_HDR);
    result = DecodeDirectionalLightmap(result, lmd, normal);
#else
    half4 lm = UNITY_SAMPLE_TEX2D(unity_Lightmap, uv);
    half3 result = DecodeLightmapRGBM(lm, unity_Lightmap_HDR);
#endif                
#else
    half3 result = UNITY_LIGHTMODEL_AMBIENT.rgb;
#endif
    return result;
}

fixed4 frag(v2f i) : COLOR
{
    fixed4 result = 0;

    // ...

    // compute model color and put it to the result

    // ... 

    // reconstruction of terrain fragment

    float2 splatUV0 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap0);
    float2 splatUV1 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap1);
    float2 splatUV2 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap2);
    float2 splatUV3 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap3);

    half4 control = tex2D(_TerrainAlphaMap, i.heightMapUV);
    half4 terrainColor = TerrainSplatMap(splatUV0, splatUV1, splatUV2, splatUV3, control);

    half3 terrainSurfaceNormal = TerrainNormal(i.heightMapUV);
    half3 terrainSurfaceTangent = cross(terrainSurfaceNormal, float3(0,0,1));
    half3 terrainSurfaceBitangent = cross(terrainSurfaceTangent, terrainSurfaceNormal);

    half3 terrainNormal = TerrainNormalMap(splatUV0, splatUV1, splatUV2, splatUV3, control);
    terrainNormal = terrainNormal.x * terrainSurfaceTangent + 
                    terrainNormal.y * terrainSurfaceBitangent + 
                    terrainNormal.z * terrainSurfaceNormal;
    
    half3 terrainLightmapColor = TerrainLightmap(i.heightMapUV, terrainNormal);
    terrainColor *= terrainLightmapColor;

    // blend model color & terrain color

    half height = TerrainHeight(i.heightMapUV);
    half deltaHeight = i.worldPos.y - height;
    half blendingWeight = smoothstep(BlendStart, BlendEnd, deltaHeight);

    result.rgb = lerp(result.rgb, terrainColor, blendingFactor);
       
    return result; 
}


Assim, a mistura com a reconstrução de fragmentos da paisagem corrige todos os problemas típicos da mistura de profundidade e da mistura com um mapa de altura, incluindo paralaxe.



Paisagismo reconstrução mistura


Desempenho de reconstrução de fragmentos de terreno


Nesse momento, é hora de perguntar: quanto vale esse tipo de compromisso? À primeira vista, a intensidade de recursos da reconstrução de fragmentos de paisagem excede em muito a intensidade de recursos da mistura alfa. Para a reconstrução, é necessário executar com uma dúzia de operações adicionais de leitura da memória. Para a mistura alfa, você só precisa de uma operação de leitura da memória e uma operação de gravação na memória.

Na realidade, tudo dependerá dos recursos da plataforma de hardware. A reconstrução de fragmentos é suportada por compactação de textura, mapeamento mip, poder de processamento do núcleo da GPU e otimizações específicas de pipeline de hardware (rejeição precoce da profundidade). E contra a mistura alfa, o fato já mencionado acima representará que é a parte menos progressiva de qualquer GPU.

No entanto, sempre há espaço para otimização. Por exemplo, no caso de reconstrução da cor da paisagem, a necessidade dessa reconstrução é apenas para uma faixa estreita de fragmentos do modelo 3D localizado não mais do que uma certa altura acima da superfície da paisagem.

A ramificação dinâmica em shaders pode fornecer resultados de desempenho pouco previsíveis, mas há dois pontos que devem ser levados em consideração:

  1. Ignorar cálculos desnecessários na ramificação de uma condição deve ser feito se essa condição não for atendida em uma parte significativa dos casos.
  2. . , ( , ), GPU. ― (branch granularity), , , , , . , , . , GPU , , . , GPU, , 1 (PowerVR SGX).


Visualização de diferentes graus de coerência

No caso da reconstrução de fragmentos, esses dois pontos são levados em consideração: a condição de ramificação na maioria dos casos permitirá interromper a implementação de operações de uso intensivo de recursos para reconstruir a cor da paisagem, e essa condição é coerente, com exceção de um número muito pequeno de fragmentos (na ilustração, são fragmentos que se encontram na fronteira entre as zonas "vermelha" e "verde").


Coerência da reconstrução de fragmentos da paisagem

Resta acrescentar alguns comentários sobre esse método de mesclagem:

  1. O Unity fornece todas as texturas necessárias apenas se a paisagem tiver o modo Instância de desenho ativado [20] ; caso contrário, o mapa normal não estará disponível, o que, por sua vez, não permitirá a reconstrução correta da iluminação da paisagem para mesclagem.
  2. Unity API , (base map) . - .
  3. , API (, Metal 16 ).
  4. 3D- , Terrain, SRP.
  5. 3D- , 3D- .
  6. , «» , «» . , «» , . «» .






Ao projetar modelos 3D, é impossível levar em consideração a variedade de relevos do terreno com os quais esses modelos devem ser usados. Freqüentemente, os modelos 3D precisam ser profundamente "afundados" na paisagem ou girados para ocultar as partes salientes, ou vice-versa - para mostrar as ocultas que devem estar visíveis. Os modelos de "aquecimento" limitam sua aplicabilidade e, se os modelos 3D forem renderizados antes do cenário, isso também causará um efeito de excesso. A curva, por sua vez, também está longe de ser adequada para todos os modelos 3D (por exemplo, não para casas e árvores).


Para ocultar os elementos salientes do modelo 3D, ele deve ser "afogado" na paisagem

Snapping é um termo familiar aos usuários de editores gráficos. Essa é uma função que permite que os pontos de controle “fiquem” nos nós da grade espacial e, nos editores 3D, nas faces e superfícies de outros objetos. Ajustar o mapa das alturas da paisagem no sombreador de vértices pode simplificar bastante o design das cenas.


Modelo 3D sem encaixe. Modelo 3D com ajuste de vértice. Modelo 3D com ajuste e mistura de vértices. Modelo 3D com encaixe de vértice, mistura e iluminação estática

A principal dificuldade na implementação do snap é que você precisa descobrir quais vértices do modelo 3D você precisa encaixar no mapa de altura e quais não valem a pena. Os vértices contêm apenas informações sobre a natureza local da superfície (o que não é suficiente) e não contêm informações sobre sua topologia (que é necessária).

Como em outros casos de aplicação, esse problema é mais fácil de resolver no estágio de modelagem, implementando diretamente os parâmetros necessários nos vértices. Como parâmetro, você deve escolher um atributo intuitivo - por exemplo, o fator de ponderação para o snap (e não a distância até a borda de uma superfície aberta, como gostaríamos de flexibilidade).


Codificação de ponderação para encaixe

Ver código
// Per-vertex snapping with terrain heightmap

#include "UnityCG.cginc"

sampler2D_float TerrainHeightmapTexture;

float4 HeightmapScale;
float4 TerrainSize;
float4 TerrainPos;

struct v2f
{

   // ...

   float3 worldPos : TEXCOORD0;
   float2 heightMapUV : TEXCOORD1;

   // ...

};

v2f vert(appdata v)
{
    v2f o;
    UNITY_INITIALIZE_OUTPUT(v2f,o);
   
    // ...
    
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    o.heightMapUV = TerrainUV(o.worldPos);
    float snappingWeight = v.color.r;                
    half height = TerrainHeight( o.heightMapUV );                
    o.worldPos.y = lerp( o.worldPos.y, height, snappingWeight );
    o.pos = UnityWorldToClipPos( half4( o.worldPos, 1 ) );

    // ...

    return o;
}


A aplicabilidade do snap ao vértice é limitada pela correspondência geral entre o terreno e a superfície do modelo 3D. Para compensar suas diferenças significativas, é necessário usar outros métodos que consomem mais recursos - por exemplo, modelos 3D com skins.


Conclusão


A principal idéia que deve ser retirada do artigo: qualquer shader suficientemente complexo e potencialmente escalável precisa de dados básicos. E a tarefa do desenvolvedor é entender como o sistema gráfico pode ser operado: quais dados ele fornece, como eles podem ser combinados entre si e como usá-los em shaders.

No caso geral, podemos concluir que a única opção para superar a estrutura pela qual as possibilidades de efeitos gráficos são limitadas é combinar os resultados de vários shaders.


Referências



All Articles