Search Unity

Question Offsetting sprite depth in an othographic projection

Discussion in 'General Graphics' started by dyamanoha_, Sep 12, 2022.

  1. dyamanoha_

    dyamanoha_

    Joined:
    Mar 17, 2013
    Posts:
    87
    I'm trying to override the depth values of a billboarded sprite such that it's perpendicular to the ground so it doesn't clip through 3D geometry at an angle. My approach to doing this is to simply override the depth of each fragment with the depth of a ray-plane intersection in clip-space. Something's going wrong here and I'm not quite sure where I've taken a misstep so some additional eyes on it would be super helpful. Here's what I've done:

    1. From a script on the billboarded sprite, I've defined the desired 'depth plane' using a) the current position of the sprite and b) the normal facing the camera perpendicular and +Y. The point and normal are transformed to clip space and set as uniforms on the shader material.

    Code (CSharp):
    1. private void OnDrawGizmos()
    2. {
    3.     if (CameraComp != null)
    4.     {
    5.         this.transform.forward = -CameraComp.transform.forward;
    6.  
    7.         //Matrix4x4 projMat = GL.GetGPUProjectionMatrix(CameraComp.projectionMatrix, true);
    8.         //Matrix4x4 worldProjMat = projMat * CameraComp.worldToCameraMatrix;
    9.         Matrix4x4 worldProjMat = CameraComp.projectionMatrix * CameraComp.worldToCameraMatrix;
    10.  
    11.         Vector4 planeOrigin = this.transform.position;
    12.         planeOrigin.w = 1.0f;
    13.         planeOrigin = worldProjMat * planeOrigin;
    14.  
    15.         //// This normalizes z between 0 and 1
    16.         planeOrigin.z = (1.0f + planeOrigin.z) / 2.0f;
    17.  
    18.         Vector3 planeNormal = new Vector3(
    19.             this.transform.forward.x,
    20.             0.0f,
    21.             this.transform.forward.z);
    22.  
    23.         //planeNormal.Normalize();
    24.         planeNormal = worldProjMat * planeNormal;
    25.         planeNormal.Normalize();
    26.  
    27.         SpriteRenderer sr = this.GetComponent<SpriteRenderer>();
    28.         sr.material.SetVector("_DepthPlanePosClip", planeOrigin);
    29.         sr.material.SetVector("_DepthPlaneNormClip", planeNormal);
    30.     }
    31. }
    2. In the shader frag program, I transform the fragment's screen space coords to clip-space [-1, 1], then do a ray-plane intersect test from that fragment out (0, 0, 1). The resulting depth with written to the depth buffer via the DEPTH semantic as a linear value between 0 and 1.

    Code (CSharp):
    1. Shader "Custom/SpriteShader"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Texture", 2D) = "white" {}
    6.         _DepthPlanePosClip("DepthPlanePosClip", Vector) = (0, 0, 0, 1)
    7.         _DepthPlaneNormClip ("DepthPlaneNormClip", Vector) = (0, 0, 0)
    8.     }
    9.     SubShader
    10.     {
    11.         Tags { "RenderType"="AlphaTest" }
    12.  
    13.         Blend SrcAlpha OneMinusSrcAlpha
    14.  
    15.         ZWrite on
    16.         Cull off
    17.  
    18.         Pass
    19.         {
    20.             CGPROGRAM
    21.             #pragma target 3.0
    22.             #pragma vertex vert
    23.             #pragma fragment frag
    24.  
    25.             #include "UnityCG.cginc"
    26.             //#include "Assets/Scripts/Common/Shaders/Utils.cginc"
    27.             float RayPlaneIntersect(float3 o, float3 v, float3 q, float3 n)
    28.             {
    29.                 return dot(q - o, n) / dot(v, n);
    30.             }
    31.  
    32.             struct v2f
    33.             {
    34.                 float2 uv : TEXCOORD0;
    35.             };
    36.  
    37.             sampler2D _MainTex;
    38.             float4 _MainTex_ST;
    39.             float4 _DepthPlanePosClip;
    40.             float3 _DepthPlaneNormClip;
    41.  
    42.             // We need to generate SV_POSITION (clip-space position) and VPOS (screen-space
    43.             // position). Typically we'd put SV_POSITION in the v2f structure, and specify the
    44.             // VPOS semantic on a fragment program input, but for whatever reason that causes
    45.             // an error saying that SV_POSITION and VPOS are duplicate fragment shader input
    46.             // semantics.
    47.             //
    48.             // The way you get around this is to declare the SV_POSITION semantic as an out
    49.             // variable in the vertex program. We can continue to put other varyings like texture
    50.             // coords in fragment input structure.
    51.             v2f vert(
    52.                 float4 vertex : POSITION,
    53.                 float2 uv : TEXCOORD0,
    54.                 out float4 outpos : SV_POSITION
    55.             )
    56.             {
    57.                 v2f o;
    58.                 o.uv = uv;
    59.                 outpos = UnityObjectToClipPos(vertex);
    60.                 return o;
    61.             }
    62.  
    63.             fixed4 frag(v2f i,
    64.                 UNITY_VPOS_TYPE screenPos : VPOS,
    65.                 out float depth : DEPTH) : SV_Target
    66.             {
    67.                 //Clipspace [-1, 1]?
    68.                 float3 fp = float3(
    69.                     screenPos.x / _ScreenParams.x * 2. - 1.,
    70.                     screenPos.y / _ScreenParams.y * 2. - 1.,
    71.                     0.
    72.                 );
    73.  
    74.                 // Assume orthographic projection (forward +Z?).
    75.                 float3 v = float3(0., 0., 1.);
    76.  
    77.                 depth = 1. - RayPlaneIntersect(fp, v, _DepthPlanePosClip, _DepthPlaneNormClip);
    78.  
    79.                 //depth = UNITY_Z_0_FAR_FROM_CLIPSPACE(depth);
    80.  
    81.                 fixed4 col = tex2D(_MainTex, i.uv);
    82.  
    83.                 //return col;
    84.  
    85.                 // Render fragment positions
    86.                 //return fixed4(fp.x, fp.y, 0.0, 1.);
    87.  
    88.                 // Render depth
    89.                 return fixed4(depth, depth, depth, 1.);
    90.                
    91.             }
    92.             ENDCG
    93.         }
    94.     }
    95. }
    96.  
    The results of this is the depth render (return fixed4(depth, depth, depth, 1.)) of the sprite shows pure white at the near-plane, pure-black at the far-plane and a correct gradient between. There is a correct-looking gradient on the sprite itself, showing the top of the sprite as closer to the camera (whiter) than the bottom. However, the problems are 1) the sprite doesn't depth test correctly against geometry in the scene 2) the location along the z-axis of the clip-plane where the shader's depth interacts with geometry depth is variable based on the far-plane value 3) the normal of the plane appears to be incorrect, although I'm not sure how that's possible considering that I'm not manually building any of these transforms and worldView * camera.projectionMatrix seems pretty straightforward.

    Thanks ahead of time for any insights!
    Capture.PNG Capture2.PNG Capture3.PNG