The problem with AlphaTested Foliage\Vegetation in VR Standalone

Hello everyone, I have been trying to figure this one out for quite a while now. After some digging, I found three acceptable ways to render alpha tested vegetation, one of them seemingly being the ultimate solution, but, so far, I have been unable to implement it in Unity URP Forward renderer.

I will list here the three solutions and describe the problems with them:

  • Alpha Blending - No pixel aliasing at all, but your geometry won’t be sorted properly. Depending on your foliage mesh it might actually work for you.

  • MSAA + Alpha to Coverage (using fwidth()) - The most common you stumble upon when searching about this on the internet. It actually helps correcting the pixel aliasing, if you use stylized foliage textures where the leaves don’t have much detail and are big enough, I guess you can get away with it. It still gets very pixelated, especially if you use more detailed textures, unless you crank up the resolution which will have a big performance impact of course.

references: Advanced VR Graphics Techniques (arm.com), Anti-aliased Alpha Test: The Esoteric Alpha To Coverage | by Ben Golus | Medium

  • Alpha Blending + Alpha Testing - For me, the ultimate solution, you get all benefits of Alpha Blending with correct sorting, plus it is faster than Alpha Blending alone because pixels that don’t pass the ZTest are discarded. How to implement this on Unity URP Forward renderer though, if possible at all? Anyone got any luck with that? You can see an implementation of this in Unity legacy docs Unity - Manual: ShaderLab: legacy alpha testing (unity3d.com).

I am currently trying to implement the latest, it seemed simple enough, but either the sorting fails in some spots or I still get pixel alising.

Conclusions (so far) based on tests made on Meta Quest 2:

  • Foliage mesh stats: 16212 Verts | 8106 tris | 24318 indices.
    Scene contains 6 of them.

  • Shader uses only baked lighting, with some vertex displacement for wind and a standard specular lighting calculation using only the main light direction.

Alpha blending is expensive, the closer you get to the trees the higher the GPU load until frame rate starts to go down below 20.

At reasonable distances (I’d say about 2-3meters) all 6 trees on my scene are taking about 20~30% GPU load. They can be optimized, but one would have to be very conservative about using foliage with this technique.

A possible solution would be disabling the alpha blending pass if the pixel is too close to the camera, but I am not sure if this is at all possible. What can be done easily is switch the material to one that doesn’t use the alpha blending pass when the tree model is too close. In these cases alpha to coverage should be enough I guess.

Alright, so I actually might have got something here. At first I thought you could just have a pass to write to the zbuffer and then have your alpha blend pass afterwards with proper ZTest configuration. What I ended up realizing is that you have to actually render your pixel twice using your vertex/fragment shader for both passes.

  • The alpha test pass renders all pixels that have their (alpha < _Cutoff)
  • The alpha blend pass renders all pixels that have their (alpha > _Cutoff)

This must sound obvious and is actually described like so in the Unity docs I referenced, but for some reason my dumbass didn’t realize how to do that inside the fragment shader.

Here is how each pass is setup and what is done in the fragment shader.

AlphaTest Pass

Pass
{
            Name "AlphaTest"

            Tags {"LightMode" = "SRPDefaultUnlit" } // Not sure about this here
            AlphaToMask On
            Cull Off
             
            #pragma vert your_vertex_function
            #pragma frag your_tree_alpha_test_frag_function
             
            HLSLPROGRAM
            ...
            ENDHLSL
}
half4 your_tree_alpha_test_frag_function(Varyings IN) : SV_Target
{
  ...

  half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv0AndFogCoord.xy);

  texColor.a = (texColor.a - _Cutoff) / max(fwidth(texColor.a), 0.0001) + 0.5; // Alpha to coverage trick
  clip(texColor.a - _Cutoff);

  ...
}

AlphaBlend pass

Pass
{
            Name "ForwardPass"

            Tags {"LightMode" = "UniversalForward"}
           
            ZWrite Off
            ZTest Less
            Cull Off
            Blend SrcAlpha OneMinusSrcAlpha
            
            #pragma vert your_vertex_function
            #pragma frag your_tree_alpha_blend_frag_function

            HLSLPROGRAM
            ...
            ENDHLSL
}
half4 your_tree_alpha_blend_frag_function(Varyings IN) : SV_Target
{
      ...

      half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv0AndFogCoord.xy);
      clip(_Cutoff - texColor.a);

       ...
}

9462359--1329266--upload_2023-11-10_2-0-6.png

The output of each pass is now looking like this:

AlphaTest

AlphaBlend

Both enabled

The trees are looking much better now, it seem to have a considerable additional cost though. Also when I turn the camera the textures seem to be glitching out a bit or losing quality, my guess it is a mipmap issue or texture filtering setting. If anyone can point out improvements or if I am doing anything wrong, I am eager to hear.