Search Unity

Question SV_IsFrontFace Direction incorrect with Cull Off

Discussion in 'Shaders' started by Windwalk_Rosco, Oct 28, 2022.

  1. Windwalk_Rosco

    Windwalk_Rosco

    Joined:
    Aug 10, 2020
    Posts:
    20
    I am working on a shader that will render a 2d texture with information about objects intersecting a plane. To do so, I was going to render all objects through an orthographic camera clipped just above the plane surface, writing data about the face direction (in theory if we render a backface without a matching front face, that should mean the object is intersecting).

    However I was running into some issues and I think I found the root of the problem. I set up this shader to test it. It turns Culling and ZTesting off (so in theory all faces should get rendered) and renders them as transparent red for front and blue for back. Using a sphere, if I set Cull to Front or Back, I get the result you would expect - full red or blue. But if I set Cull to Off, I don't get full purple. I'm very confused about this result - it seems to imply the direction of faces gets mixed up when trying to render overlapping front and back faces.

    I can render this in two separate passes (one for front and one for back) but was hoping someone might have an idea of what is actually happening here.

    Render with Cull Back:
    upload_2022-10-28_13-58-26.png

    Render with Cull Front:
    upload_2022-10-28_13-59-9.png

    Render with Cull Off:
    upload_2022-10-28_13-59-47.png

    Shader:
    Code (CSharp):
    1. Shader "WaterTest2"
    2. {
    3.     Properties
    4.     {
    5.         [MainColor] _BaseColor ("Color", Color) = (1, 1, 1, 1)
    6.     }
    7.  
    8.     SubShader
    9.     {
    10.         Tags { "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline" "IgnoreProjector" = "True" "ShaderModel" = "4.5" }
    11.  
    12.         Pass
    13.         {
    14.             Name "WaterIntersection"
    15.  
    16.             Cull Off
    17.             // Cull Front
    18.             ZTest Always
    19.             ZWrite Off
    20.  
    21.             BlendOp Add
    22.             Blend SrcAlpha OneMinusSrcAlpha
    23.  
    24.             HLSLPROGRAM
    25.  
    26.             #pragma exclude_renderers gles gles3 glcore
    27.             #pragma target 4.5
    28.  
    29.             #pragma vertex IntersectionPassVertex
    30.             #pragma fragment IntersectionPassFragment
    31.  
    32.             #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
    33.  
    34.             struct Attributes
    35.             {
    36.                 float4 positionOS : POSITION;
    37.                 float3 normalOS : NORMAL;
    38.                 float4 tangentOS : TANGENT;
    39.  
    40.             };
    41.  
    42.             struct Varyings
    43.             {
    44.                 float3 normalWS : TEXCOORD0;
    45.                 float4 positionCS : SV_POSITION;
    46.  
    47.             };
    48.  
    49.             Varyings IntersectionPassVertex(Attributes input)
    50.             {
    51.                 Varyings output = (Varyings)0;
    52.  
    53.                 VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
    54.                 output.positionCS = vertexInput.positionCS;
    55.  
    56.                 VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS);
    57.                 output.normalWS = normalInput.normalWS;
    58.  
    59.                 return output;
    60.             }
    61.  
    62.             half4 IntersectionPassFragment(Varyings input, bool facing : SV_IsFrontFace) : SV_Target
    63.             {
    64.                 // half NdotV = dot(input.normalWS, float3(0, 1, 0));
    65.                 // facing = NdotV >= 0;
    66.  
    67.                 return facing ? half4(1, 0, 0, 0.25) : half4(0, 0, 1, 0.25);
    68.             }
    69.  
    70.             ENDHLSL
    71.         }
    72.     }
    73. }
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    BlendOp Add
    is the default blend operation, and
    Blend SrcAlpha OneMinusSrcAlpha
    is traditional alpha blending. Alpha blending is not commutative, meaning the order changes the result.

    When rendering a mesh with
    ZWrite Off
    and
    Cull Off
    , the triangles of the mesh render in whatever order they're saved in the mesh data, which isn't always a "logical" order. And almost certainly isn't going to be furthest to nearest. So sometimes the "back" triangles are rendering first and sometimes the "front" triangles are rendering first. In one case the blue is alpha blended with the black background, and then the red is alpha blended with the dark blue results of the previous blend. And in the other red is alpha blended with the black background, and then blue is alpha blended with the dark red results of the previous blend. So you're getting either (0.25, 0.0, 0.1875) if the blue backfaces render first, or (0.1875, 0.0, 0.25) if the red renders first.
     
    aras-p likes this.
  3. Windwalk_Rosco

    Windwalk_Rosco

    Joined:
    Aug 10, 2020
    Posts:
    20
    Ahhh yeah that makes sense, thanks!

    That is also the root of the issue I was facing while trying to do both front and back in the same pass with stencil. Essentially what I am trying to accomplish is generating an sdf of plane intersections like this post: https://twitter.com/HarryAlisavakis/status/1481644176349401089

    I have it working right now by doing front/back in two separate passes and then running JFA over it in a compute shader (@bgolus huge thanks to your jfa article!). First pass draws back face coordinates and increments stencil, second pass decrements stencil for front faces and draws black if it's the last one. Trying to do this in one pass hits the same issue as above because, as you pointed out, the draw order isn't guaranteed. Curious if anyone has an idea on how it could maybe be done in one pass?

    Attempt at doing it in one pass:

    Code (CSharp):
    1. Pass
    2. {
    3.     Name "WaterIntersectionAll"
    4.     Tags { "LightMode" = "CustomWater" }
    5.  
    6.     Cull Off
    7.     ZTest Always
    8.     ZWrite Off
    9.  
    10.     Stencil
    11.     {
    12.         Ref 1
    13.  
    14.         CompBack Always
    15.         PassBack IncrSat
    16.  
    17.         CompFront Equal
    18.         PassFront DecrSat
    19.         FailFront DecrSat
    20.     }
    21.  
    22.     HLSLPROGRAM
    23.  
    24.     #pragma exclude_renderers gles gles3 glcore
    25.     #pragma target 4.5
    26.  
    27.     #pragma vertex IntersectionPassVertex
    28.     #pragma fragment IntersectionPassFragment
    29.  
    30.     #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
    31.  
    32.     float _WaterMapSize;
    33.  
    34.     struct Attributes
    35.     {
    36.         float4 positionOS : POSITION;
    37.     };
    38.  
    39.     struct Varyings
    40.     {
    41.         float4 positionCS : SV_POSITION;
    42.     };
    43.  
    44.     Varyings IntersectionPassVertex(Attributes input)
    45.     {
    46.         Varyings output = (Varyings)0;
    47.  
    48.         VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
    49.         output.positionCS = vertexInput.positionCS;
    50.  
    51.         return output;
    52.     }
    53.  
    54.     half4 IntersectionPassFragment(Varyings input, bool facing : SV_IsFrontFace) : SV_Target
    55.     {
    56.         if (facing)
    57.         {
    58.             return half4(0, 0, 0, 1);
    59.         }
    60.         else
    61.         {
    62.             float2 ndc = input.positionCS.xy / _WaterMapSize;
    63.             return half4(ndc.xy, 0, 1);
    64.         }
    65.     }
    66.  
    67.  
    68.     ENDHLSL
    69. }