How to extract view depth from a camera render (with a shader)?

I am trying to simulate a LIDAR in Unity, i.e. I am trying to create a pointcloud based on the surfaces around the sensor. In other words, the distance of a large set of rays shot from some point.

I figured I can approach this with a regular camera. Cameras also compute view depth for each pixel, which is already very close to what I’m looking for.

Question: how can I extract the view depth information from a camera render and process it with a script?

Since most rendering things live on the GPU, I am almost certain I will need custom shaders to get anything done.

What I got so far: I have a scene with the main camera and a LIDAR camera. The LIDAR camera renders to a RenderTexture, which is used by a LIDARMaterial. The LIDARMaterial also has a LIDARShader for processing.
Then, at runtime I render the LIDARCamera and use Graphics.Blit to process the LIDARCamera result with the LIDARMaterial (effectively applying my custom shader) and store the result in a new texture.
For debugging purposes I put this texture on a test material which I apply to a ‘TV Screen’ inside the scene:

Now I figured I would modify the shader to simply render the view depth as e.g. grayscale, like:

Where I’m stuck: I cannot get any depth information from the shader. What I have now:

Shader "Unlit/LIDARShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

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

            sampler2D _MainTex;
            float4 _MainTex_ST;

            uniform sampler2D _CameraDepthTexture;

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

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                // col.r = 1.0;
                col.r = saturate(tex2D(_CameraDepthTexture, i.uv));
                return col;
            }
            ENDCG
        }
    }

    Fallback "ShadowCaster"
}

But the result from the depth texture samples is always 0. What’s going wrong here?

I am using Unity 2022.3.5f1 with the default rendering pipeline.
Full project is attached too.

Other reading:
There is the manual about Cameras and Depth: Unity - Manual: Cameras and depth textures
Pretty much the same question but from 2012: How to access rendered depth buffer properly?


9413600–1318526–LidarProject.unitypackage (15.6 KB)

this repo has one example for using depth also,

Thanks for your message. Nice repo, I imported it into my scene and added a fixed scan as example and it works!

Their approach sounds identical to mine, I’m going to put it side-by-side and I’ll see if I can fix my approach.

check also Frame Debugger, if any depth gets rendered in your scene?

you can also force depth rendering in camera (in case your scene doesnt need depth, like having lights with shadows)

Thanks, I got the Frame Debugger open, I’ll see how to recognize the different components.

What seems to be the main difference is the repo you linked uses Camera.SetReplacementShader, instead I use Graphics.Blit to put the rendered texture through a material and hence through a shader. I guess the depth info in the later case gets lost.

I have it working in my way, without SetReplacementShader, so this should work for any rendering pipeline.
I think the main trick in the end was to use a depth-only RenderTexture for the camera and use a shader to turn that into color:

The full project is attached below, should it be helpful to anyone else.

9418388–1319582–LidarProjectWorking.unitypackage (20.2 KB)

Spend a lot of time on this and I think I’ve really got it down now.
I have the following process to get 3D coordinates of all rendered pixels of a camera:

  • Render a camera to a depth-only texture. (No shaders or anything yet.)

  • Use Graphics.Blit() to apply a shader to the rendered texture. The shader takes the depth info from the input texture and computes the 3D points. The output is a new texture.

  • Use Rendering.AsyncGPUReadback() to copy this second texture into the CPU to make it accessible by a script.

Now depending on what you want exactly, the math can be pretty tricky. I was making a mock LIDAR sensor for ROS integration, so I need a pointcloud from evenly distributed scans. This involves calculating which pixel of the output best corresponds to a LIDAR ray.

I can’t share my full project anymore, but the final shader is in full here:

Shader code

/*
* This shader is used to take the depth info from an already rendered texture
* and from it calculate the 3D points corresponding to a LIDAR output.
* The RGB values corresponds to XYZ positions. The A channel is left at 0.
*
* Camera depth information itself is not used directly!
*
* This code is Unity's own blend of HLSL (which is like C).
*
* Note that the output of this shader can be a different resolution than the
* input texture from the camera.The output corresponds to equally spaced rays,
* while the input is equally spaced pixels from a render.
*/
Shader "Custom/LidarShader"
{
    /*
     * Properties are inputs to the shader, they will show up in the Unity
     * inspector for instance.
     */
    Properties
    {
        /*
         * Depth texture (camera output). The color format should be set to `None` and
         * depth format to something non-zero.
         * The name has to be "_MainTex" such that `Graphics.Blit()` can input the RenderTexture.
         */
        _MainTex ("Depth Texture (no color)", 2D) = "white" {}

        /*
         * Create inputs for camera clipping plane.
         * Camera info is also available in a shader, except we are processing a texture
         * that's already rendered, i.e. the camera that made it is not available here!
         */
        _NearPlane ("Camera Near Clipping Plane", float) = 0.01
        _FarPlane ("Camera Far Clipping Plane", float) = 100.0

        /*
         * Create inputs for the FOV of the camera.
         * Angles should be in radians, as sin/cos/tan here rely on radians.
         */
        _MaxAngleVer ("Camera Vertical Angle (from focal)", float) = 0.78
        _MaxAngleHor ("Camera Horizontal Angle (from focal)", float) = 0.78

        /*
         * Bounds of the near-plane (in Unity meters) of the camera.
         * These can be computed directly from the FOV angles, but we save on the computation this way.
         */
        _NearPlaneRight ("Right Edge Of Near Plane", float) = 1.0
        _NearPlaneTop ("Top Edge Of Near Plane", float) = 1.0

        /*
         * Y- and x-rotation at rest for this camera. Needed to create 3D position for each point.
         */
        _CameraAngleHor ("Camera Horizontal Angle Offset", float) = 0.0
        _CameraAngleVer ("Camera Vertical Angle Offset", float) = 0.0

        /*
         * Constant offset in the camera position (e.g. w.r.t. the link parent).
         * Offset is camear frame (_after_ camera rotation offset).
         */
        _PositionOffset ("Camera Position Offset", Vector) = (0.0, 0.0, 0.0, 0.0)
    }

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

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            sampler2D_float _MainTex;
            float4 _MainTex_ST;

            float _NearPlane;
            float _FarPlane;
            float _MaxAngleVer;
            float _MaxAngleHor;
            float _CameraAngleHor;
            float _CameraAngleVer;
            float _NearPlaneRight;
            float _NearPlaneTop;
            float4 _PositionOffset;

            struct appdata
            {
                /*
                 * Vertex original position in 3D (4th component is 1.0):
                 */
                float4 vertex : POSITION;
            };

            struct v2f {
                /*
                 * Position of the vertex in the camera clip-space.
                 */
                float4 vertex : SV_POSITION;
   
                /*
                 * Position on the screen as x,y in the range from 0.0 to 1.0.
                 */
                float2 screenPos : TEXCOORD0;
            };

            /*
             * Vertex shader. Runs on each vertex in the 3D scene.
             */
            v2f vert (appdata v)
            {
                v2f o;
   
                // Basically gets the projection of the object coordinate unto the near-clip
                // plane of the camera (= in clip space):
                o.vertex = UnityObjectToClipPos(v.vertex);
   
                // Get position on the screen corresponding to position in clip space:
                o.screenPos = ComputeScreenPos(o.vertex);
                return o;
            }

            /*
             * Convert non-linear depth in 0.0 - 1.0 range to real distance.
             */
            float DepthToDistance(float depth)
            {
                // This is just how Unity seems to have encoded the depth:
                return _FarPlane * _NearPlane / (_FarPlane - depth * (_FarPlane - _NearPlane));
            }

            /*
             * Fragment shader. Runs on each pixel to-be rendered on the screen.
             */
            float4 frag(v2f i) : SV_Target
            {
                // This output pixel of the resulting texture really corresponds to a single LIDAR ray.
                // So we need to calculate which location on the input texture (= camera output) corresponds
                // to this ray.
                i.screenPos = (i.screenPos - 0.5) * 2.0; // Re-map [0.0, 1.0] to [-1.0, 1.0]
   
                // Angles of this specific ray:
                float angleHor = _MaxAngleHor * i.screenPos.x; // Yaw, negative left from the focal
                float angleVer = _MaxAngleVer * i.screenPos.y; // Pitch, negative below the focal
   
                // Map from [-1.0, 1.0], [-1.0, 1.0] to [-nearRight, nearRight], [-nearTop, -nearTop]
                i.screenPos.x *= _NearPlaneRight;
                i.screenPos.y *= _NearPlaneTop;

                // Calculate screen position, based on intersection of rays with a plane at `planeDist`
                // in front of the viewpoint.
                float planeDist = _NearPlane / (cos(angleHor) * cos(angleVer));
               
                i.screenPos.x = planeDist * sin(angleHor) * cos(angleVer);
                i.screenPos.y = planeDist * sin(angleVer);
   
                // Revert map to [-1.0, 1.0]
                i.screenPos.x /= _NearPlaneRight;
                i.screenPos.y /= _NearPlaneTop;

                i.screenPos = (i.screenPos / 2.0) + 0.5; // Revert map to [0.0, 1.0]

                // Read depth directly from the texture (in the R-channel), as number between 0.0 and 1.0:
                float depth = tex2D(_MainTex, i.screenPos);
                // This function will interpolate based on the screen position of the pixel and also works if the resolutions are different.

                // Some graphics APIs invert the meaning of depth, flip it:
                #ifdef UNITY_REVERSED_Z
                    depth = 1.0 - depth;
                #endif

                if (depth >= 0.999999)
                {
                    const float nan = 0.0 / 0.0; // Hacky way to get a NaN float
                    return float4(nan, nan, nan, 0.0); // Mark as 'miss'
                }
       
                // However, the scaling is not linear - make it so:
                depth = DepthToDistance(depth);
       
                // Depth values are really perpendicular to the camera, i.e. the z-value we look for.
                // Compute the other coordinates from the spherical coordinates:
                depth = depth / (cos(angleHor) * cos(angleVer));

                // Account for the camera orientation w.r.t. its parent:
                angleHor += _CameraAngleHor;
                angleVer += _CameraAngleVer;
   
                // 3D position in ROS2 (!) coordinates:
                float4 pos = {
                    cos(angleHor) * cos(angleVer), // z
                    -sin(angleHor) * cos(angleVer), // -x
                    sin(angleVer), // y
                    0.0
                };
                // By already transforming to ROS coordinates we skip a post-processing step
   
                pos = pos * depth + _PositionOffset;
                pos.a = 1.0; // Mark this point as 'hit'
                return pos;
            }
            ENDCG
        }
    }
}

I want to use this camera with the third person asset of unity
Is it compatible with that or do I need to make some changes to the code and camera settings?

I’m not sure, I’m not all that familiar with Unity in general. I imagine it would work, and if it doesn’t work out of the box you might need to change some camera settings. I can’t be of much more help there.