Receiving shadows and writing normals with Graphics.DrawProcedural + Geometry Shader

Hello!

Let me preface this with the fact that I’ve done a bunch of research already and haven’t been able to find enough information to piece this together entirely by myself.

I am using Graphics.DrawProcedural and a vert/geom/frag shader combo to render a bunch of triangles that were previously generated on the CPU using a marching cubes algorithm, and then pushed to a buffer on the GPU.

What is currently functional:

  • The triangles are rendered properly in the correct location using a ForwardBase pass and a CommandBuffer that fires in each camera’s BeforeForwardOpaque.
  • The generated triangles properly cast shadows on other objects in the scene using a ShadowCaster pass and a CommandBuffer that fires in each camera’s BeforeDepthTexture and the main light’s BeforeShadowMapPass.
  • The triangles now properly write depth and normals to the DepthNormals texture using another shader pass.

Currently not functional:

  • The object needs to receive shadows (I assume I have to do something with a ShadowCollector pass, but can’t find enough information about it).
  • The object needs to write normals and depth to the DepthNormals texture (this likely has something to do with a replacement shader that I do not know how to edit).

Things I’ve tried:

  • Digging around in all of the Unity include files (AutoLight, etc.) and attempting to manually wire shadow-related stuff into the geometry shader (since I can’t use the vertex shader macros). This just leads to a bunch of undeclared identifier errors when using things like unity_WorldToLight, etc.
  • Retrieving the light’s depth buffer (to no avail) along with the light’s worldToLocal matrix and sending those in to the shader as a sampler2D and float4x4 every frame.
  • Hours of googling only to find people wanting help casting shadows (which I’ve solved) or responses effectively saying “use Unity’s macros”, which do not function for geometry shaders and/or DrawProcedural.

Relevant code:

This is where the actual draw commands are added:

public void LateUpdate()
    {
        m_shadowCasterBuffer.Clear();
        m_forwardBaseBuffer.Clear();

        m_shadowCasterBuffer.DrawProcedural(transform.localToWorldMatrix, p_material, 1, MeshTopology.Points, 1000000);
        m_forwardBaseBuffer.DrawProcedural(transform.localToWorldMatrix, p_material, 0, MeshTopology.Points, 1000000);

        foreach (Camera camera in Camera.allCameras)
        {
            camera.RemoveAllCommandBuffers();
            camera.AddCommandBuffer(CameraEvent.BeforeDepthTexture, m_shadowCasterBuffer);
            camera.AddCommandBuffer(CameraEvent.BeforeForwardOpaque, m_forwardBaseBuffer);
        }
      
        sun.RemoveAllCommandBuffers();
        sun.AddCommandBuffer(LightEvent.BeforeShadowMapPass, m_shadowCasterBuffer);
    }

Here’s the shader (non-relevant stuff has been removed):

SubShader
{
    Tags { "RenderType"="Opaque" "Queue"="Geometry" }

    Pass
    {
        Name "ForwardBase"
        Tags { "LightMode"="ForwardBase" }

        CGPROGRAM

        #pragma target 5.0
        #pragma vertex VertMain
        #pragma geometry GeomMain
        #pragma fragment FragMain
        #pragma multi_compile_fwdbase
          
        // NOTE: Removed includes (UnityCG, AutoLight, etc.) to be concise.

        struct GeomOutput
        {
            float4 m_clipPos : SV_POSITION;
            float4 m_objectPos : TEXCOORD1;
            float4 m_worldPos : TEXCOORD2;
            float3 m_normal : NORMAL;
            half4 m_colour : COLOR;
        };

        struct FragOutput
        {
            half4 m_colour : SV_Target;
            float m_depth : SV_Depth;
        };

        // NOTE: Removed non-relevant data types and vert shader to be concise since they're just feeding data into the geometry shader
        [maxvertexcount(3)]
        void GeomMain(point VertOutput a_point[1], inout TriangleStream<GeomOutput> a_triangleStream)
        {
            // NOTE: The geometry shader as-is currently places triangles in the correct location, so all the code here is functional.
            // I removed a check for if a given triangle exists in the data buffer or not because that's not relevant to the current issue.
            // u_triDataBuffer is the ComputeBuffer containing vertex positions, normals, and colours. ptr is a pointer into said buffer.
            Triangle tri = u_triDataBuffer[a_point[0].ptr];
            GeomOutput output;

            output.m_objectPos = float4(tri.m_v1.m_objectPos, 1);
            output.m_worldPos = mul(unity_ObjectToWorld, output.m_objectPos);
            output.m_clipPos = UnityObjectToClipPos(output.m_objectPos);
            output.m_normal = mul(unity_ObjectToWorld, float4(tri.m_v1.m_objectNormal, 0)).xyz;
            output.m_colour = half4(tri.m_v1.m_colour);
            a_triangleStream.Append(output);

            output.m_objectPos = float4(tri.m_v2.m_objectPos, 1);
            output.m_worldPos = mul(unity_ObjectToWorld, output.m_objectPos);
            output.m_clipPos = UnityObjectToClipPos(output.m_objectPos);
            output.m_normal = mul(unity_ObjectToWorld, float4(tri.m_v2.m_objectNormal, 0)).xyz;
            output.m_colour = half4(tri.m_v2.m_colour);
            a_triangleStream.Append(output);

            output.m_objectPos = float4(tri.m_v3.m_objectPos, 1);
            output.m_worldPos = mul(unity_ObjectToWorld, output.m_objectPos);
            output.m_clipPos = UnityObjectToClipPos(output.m_objectPos);
            output.m_normal = mul(unity_ObjectToWorld, float4(tri.m_v3.m_objectNormal, 0)).xyz;
            output.m_colour = half4(tri.m_v3.m_colour);
            a_triangleStream.Append(output);
        }
          
        FragOutput FragMain (GeomOutput a_geomOutput)
        {
            FragOutput output;
              
            half4 litColour = /* NOTE: Here is where I do custom lighting. It's technically proprietary so I had to remove it. */;

            // TODO: How do you receive shadows/sample the main directional light's shadow map? That needs to be done here.
            float lightAttenuation = /* ??? */;

            output.m_colour = litColour * lightAttenuation;
            float4 clipPos = mul(UNITY_MATRIX_VP, a_geomOutput.m_worldPos);
            output.m_depth = clipPos.z / clipPos.w;

            return output;
        }

        ENDCG
    }

    Pass
    {
        // NOTE: As in the other pass, I edited out a bunch of non-relevant stuff to be concise. See comments above.
        Name "ShadowCaster"
        Tags { "LightMode"="ShadowCaster" }

        CGPROGRAM

        #pragma target 5.0
        #pragma vertex VertMain
        #pragma geometry GeomMain
        #pragma fragment FragMain
        #pragma multi_compile_shadowcaster

        struct GeomOutput
        {
            float4 m_clipPos : SV_POSITION;
            float4 m_worldPos : TEXCOORD1;
        };

        struct FragOutput
        {
            float4 m_colour : SV_Target;
            float m_depth : SV_Depth;
        };

        [maxvertexcount(3)]
        void GeomMain(point VertOutput a_point[1], inout TriangleStream<GeomOutput> a_triangleStream)
        {
            Triangle tri = u_triDataBuffer[triInfo.ptr];
            GeomOutput output;

            float4 objectPos = float4(tri.m_v1.m_objectPos, 1);
            output.m_worldPos = mul(unity_ObjectToWorld, objectPos);
            output.m_clipPos = UnityObjectToClipPos(objectPos);
            a_triangleStream.Append(output);

            objectPos = float4(tri.m_v2.m_objectPos, 1);
            output.m_worldPos = mul(unity_ObjectToWorld, objectPos);
            output.m_clipPos = UnityObjectToClipPos(objectPos);
            a_triangleStream.Append(output);

            objectPos = float4(tri.m_v3.m_objectPos, 1);
            output.m_worldPos = mul(unity_ObjectToWorld, objectPos);
            output.m_clipPos = UnityObjectToClipPos(objectPos);
            a_triangleStream.Append(output);
        }
          
        FragOutput FragMain (GeomOutput a_geomOutput)
        {
            FragOutput output;
              
            float4 clipPos = mul(UNITY_MATRIX_VP, a_geomOutput.m_worldPos);
            output.m_depth = clipPos.z / clipPos.w;
            output.m_colour = EncodeFloatRGBA(output.m_depth);

            return output;
        }

        ENDCG
    }
}

Would anybody be able to give me an explanation (or point me to one!) about how to receive shadows in a situation like this, and how I would go about implementing a custom depthnormals writing pass? If more info is needed, let me know.

Thank you for the help! :slight_smile:

I have been struggling with similar issues recently and providing you arn’t using transparency on your geometry this should be work for you :slight_smile:

Bare in mind this does not yet work with additional lights only the main directional, as im still struggling to finish it

That was actually some really good insight - thanks for the link! Sadly, I still can’t get it to function.

I’ve found another good resource on the topic right here, which according to I’m doing everything correctly:

At this point I’m assuming it’s because there’s some pass in the Diffuse fallback that gets run automatically by Unity when the shader is on a mesh (which I would do, but I need to render 500000+ triangles, and Unity meshes can not support enough verts for that even at 1 vert per tri just as an index) that I’m currently not running due to using Graphics.DrawProcedural instead.

Ah that Sejton shader is a great read too. I had heard that there were issues with Draw procedural and lighting/shadows but hopefully theres a solution out there!

Have you tried using Unity - Scripting API: Mesh ? Might be a lot simpler - you can set everything up in the editor for what you want re: materials, lighting and shadowing, and just plunk the mesh vertices in code.

Did anyone manage to get this working? I don’t want to use mesh, the whole point of using draw procedural is to avoid hanging the cpu with conversion from compute buffers to arrays.

1 Like

I actually got a code example sent to me from a unity developer that solved it for me long after I made this thread, but sadly I no longer work at the company I was with when I ran into this issue so I don’t have access to that email anymore. I’ll see if a past colleague can dig it up for me and if not I’ll try to reproduce the issue and remember the solution then let you know.

1 Like