Cross section shader troubles

Hey everyone,

I’m currently trying to create an effect which shows a cross-section of certain objects.
The actual cutting-off part I have figured out. Either by using the near-clip of the camera or by using a dot product of a simulated slice-plane.

Where I’m stuck is with filtering out everything behind the cross-section part.

Basically what I am trying to show is only the red parts of the following.

A great resource has been these two threads:

Which have, to my understanding, exactly the same goal as mine but as the final solution has never been posted I’m still lost.

I suspect the solution lies with using separate passes and/or the stencil buffer but I can’t seem to wrap my head around correctly using these.
If someone could please enlighten me in the correct way to use these to achieve the effects described.

Because there’s not really a good way to do this. The first link you posted already mostly listed the “best” options.

A) Don’t use mesh shapes at all and use analytical SDFs if you only need spheres / boxes. Otherwise look into the various projects out there that generate SDFs from arbitrary 3D meshes. Then have a mesh plane that draws the intersection of the mesh surface with the SDF shapes.

B) Use an extra camera to render objects with the clear color and front faces black, back faces white. Then render that on a mesh plane. Only works if meshes are air tight and have no intersecting geometry.

But there’s one option I didn’t list then, which is:
C) Abuse the frak out of the depth buffer, which has the same problem as B, but could be done without a render texture and by rendering the regular geometry (again, assuming they’re air tight and have no intersecting geometry).

The trick is render the front faces using a depth only pass, but still doing the cross section. Then render the back faces with the cross section. The result should be back faces that should be hidden by the front faces won’t be visible. But this comes with a big caveat that it only works if viewed from “above” the cutting plane. If you’re below it then nothing renders at all. You can solve that by flipping which side of the plane gets cut based on which side of the plane the camera is on.

Thanks for your suggestions Bgolus!

Option A won’t work unfortunately as my case requires authored 3d models.
Option B would be plausible as it satisfies the conditions you mention.

I’m most interested in Option C and have already tried before to get that working.
The object needs to only be visible from 1 direction so this would be perfect.
Where I am stuck however is the stencil buffer part.
Below is the bulk of my shader code. What I intent to do is render the front faces first to the buffer. Then render the back faces and discard whats in the buffer already.

SubShader
    {
        Tags { "RenderType"="Opaque" "Queue"="Overlay" }
        LOD 100
        ZWrite Off
        // Write front facing only to stencil
        Pass
        {
            Cull Back
            ColorMask 0
            Stencil
            {
                Ref 2
                Comp Always
                Pass Replace
            }
        }
        // Render back faces but discard front faces from earlier pass
        Pass
        {
            Cull Front
            Stencil
            {
                Ref 2
                Comp Equal
                Pass Zero
            }
            // ...
            // Just writes unlit color to fragment
            // ...

When applied this shows no object but I guess that is because everything is being discarded?
Where would the cross section part need to happen? I already have another shader working which clips an object based on a plane position and normal.

Small update!

As you can see from the gif below I made some progress with the shader.
How it works right now is pretty much as described by Bgolus above in option C.

It renders the front faces only to the depth buffer.
The front clipping is done (right now) with the near clip plane of the camera.
The second pass of the shader culls the front faces and only shows the buffer which is greater of equal.

Edit: As it might be a bit hard to see from the gif (and there is nothing to see in the editor)
You’re seeing 2 white spheres, 1 white cube and 1 blue sphere being clipped.

For completion sake, here is the entire shader that is used.

Shader "Unlit/ToCutObjects"
{
    Properties
    {
        _ColorFrontFace ("Color Front Face", Color) = (1,1,1,1)
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue"="Overlay"}
        LOD 100

        // write 2 to the buffer wherever front faces are visible
        Pass
        {
            ZWrite On
            Cull Back
            ColorMask 0
          
            Stencil
            {
                Ref 2
                Comp Always
                Pass Replace
            }
       }

        // check the buffer for values that are 2 and only render those which are greater or equal
        Pass
        {
            Cull Front

            Stencil
            {
                Ref 2
                Comp GEqual
                Pass Keep
            }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _ColorFrontFace;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return _ColorFrontFace;
            }
            ENDCG
        }
    }
}

As you can see there is still a problem with artefacts when objects intersect each other. Any tips on how to fix these is greatly appreciated! Also thanks again to Bgolus for directing me in the right direction!

This is unsolvable in the general case, which is why I commented about it being a problem for both methods B and C. Using SDFs is the only solution 100% solution. Though to realistically generate a proper SDF from arbitrary mesh data also requires an air tight mesh with no intersections… so yeah. If you’re only using simple shapes then I’d look at using analytical SDFs.
https://www.iquilezles.org/www/articles/distfunctions/distfunctions.htm

In the specific case of multiple meshes where each are air tight and have no intersecting geometry in of themselves, there is a away around this. You’d need to render each mesh and clear the depth buffer between. You can do this either by rendering the objects using a command buffer and manually clearing the entire depth buffer using the ClearRenderTarget() function, or you could have your shaders have a third pass which is also a depth only pass, but have it use ZTest Always and is always at the far depth plane.

This is what I got.
Again, this will work only with tight and not intersecting meshes only.
7786590--982965--m10.gif
Cubes have a ColorMask 0 material (plus this wireframe one for visualisation).
The cutting plane has two materials varying with stencil values.

Two pass shader for the cubes:

Shader "Custom/SectionOnly"
{
    Properties
    {
        _StencilMask("Stencil Mask", Range(0, 255)) = 0
        [Enum(None,0,Alpha,1,Red,8,Green,4,Blue,2,RGB,14,RGBA,15)] _ColorMask("Color Mask", Int) = 0
        _SectionPoint("_SectionPoint", Vector) = (0,0,0,1)
        _SectionPlane("_SectionPlane", Vector) = (1,0,0,1)
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque"  "Queue" = "Geometry+1" }     
        LOD 100

        Pass
   
        {
            ColorMask[_ColorMask]
            Cull Off
        Stencil
        {
            Ref[_StencilMask]
            Comp Always
            PassBack Replace
            PassFront Zero
        }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            uniform float3 _SectionPlane;
            uniform float3 _SectionPoint;

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 wpos : TEXCOORD1;
                UNITY_FOG_COORDS(0)
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
               UNITY_INITIALIZE_OUTPUT(v2f, o);
               o.vertex = UnityObjectToClipPos(v.vertex);
               o.wpos = mul(unity_ObjectToWorld, v.vertex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

           static const bool _frontSide = (dot(_WorldSpaceCameraPos - _SectionPoint, _SectionPlane) > 0);

            fixed4 frag (v2f i) : SV_Target
            {
               if ((dot((i.wpos - _SectionPoint), _SectionPlane) > 0)== _frontSide) discard;
               fixed4 col = fixed4(0.5,0.5,0.5,1);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

The plane shader:

Shader "Custom/CapUnlit"
{
    Properties
    {
        _Color("Main Color", Color) = (1,1,1,1)
        _MainTex("Base (RGB)", 2D) = "white" {}
        _StencilMask("Stencil Mask", Range(0, 255)) = 1
        [Enum(UnityEngine.Rendering.CompareFunction)] _StencilComp("Stencil Comparison", Int) = 3
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" "Queue" = "Geometry+2" }
        LOD 100

        Pass
        {
            Cull Off
            Stencil
            {
                Ref[_StencilMask]
                Comp[_StencilComp]
            }
           
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _Color;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv)*_Color;
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}