Possible to set depth in a shader?

I have a volume based raytracing engine written in HLSL I want to port to Flax, essentially I render a cube and inside this cube I use raytracing to render 3D shapes.

So, to make sure the 3D shapes inside the volume is able to mix with regular 3D objects in the scene, I have to set the depth based on the model that is rendered inside the cube rather than the depth of the fragment on the cube face.

In Unity this is quite easy to achieve as the pipeline supports it, I can simply define

        struct frag_out
        {
            half4 color : SV_Target0;
            float depth : SV_Depth;
        };

And in the fragment shader I can set both the color and the depth, something like this.

                r = raytrace(ray,);
                o.color = r.color;
                o.depth = r.pos.z / r.pos.w;

Is something like this supported in Flax too?

1 Like

When drawing custo mgeometry you can export depth from the pixel shader just like in Unity via SV_Depth: Custom Geometry Drawing | Flax Documentation

When using Materials system you can only modify the vertex position - there is a task on a Roadmap to add depth offset support in material graph.

Thanks for your reply.

The shader sample you are linking to is what I use for testing already, it is running well and drawing my custom cube as expected.

However, when trying a basic test such as

META_PS(true, FEATURE_LEVEL_ES2)
struct frag_out
{
    float4 color : SV_Target0;
    float depth : SV_Depth;
};

META_PS(true, FEATURE_LEVEL_ES2)
frag_out PS_Custom(PixelInput input) : SV_Target
{
    frag_out o;
    o.color=float4(0, 1, 0, 1);
    o.depth=0.0; //Just for testing

    return o;
}

I get the expected green color on the cube, but no matter what I set depth to, the effect in the scene is that the cube is drawn behind or in front of the other regular cube in the scene as normal, like no custom depth was written.

Not sure what I am doing wrong, are there some other shader parameters I need to set to enable a custom depth as in the Unity HLSL equivalent?

Just wanted to mention I tried a few tweaks now.

I was thinking the issue is related to when in the pipeline the fragment shader is called, as the custom depth might be ignored if set at the wrong time, and I tried various settings like setting the sort order of the materials in the scene, adding transparency and various other similar things meant to force the objects to be in separate draw calls (at least I assume they would be). But still got the same result unfortunately.

Anyway, without detailed knowledge of the inner workings of the rendering pipeline I feel I am shooting in the dark. :sweat_smile:

Any advice would be greatly appreciated.

I think the Depth Buffer has to be set with Depth Write enabled via GPUPipelineState:

desc.DepthEnable = true;
desc.DepthWriteEnable = true;

Other than that you can use RenderDoc to diagnose this. Or if you have a small project to test this I can do it if you send me it (link or DM).

Thank you for the suggestions, unfortunately adding those lines still did not allow me to write to the depth buffer. I might be missing something basic, but I have been unable to figure it out.

My testing code is basically identical to the one in the custom geometry drawing code you linked to:

namespace Game;

using System;
using System.Runtime.InteropServices;
using FlaxEngine;

public class CustomGeometryDrawing : PostProcessEffect
{
    /// <summary>
    /// Shader constant buffer data structure that matches the HLSL source.
    /// </summary>
    [StructLayout(LayoutKind.Sequential)]
    private struct Data
    {
        public Matrix WorldMatrix;
        public Matrix ViewProjectionMatrix;
    }

    private static readonly Float3[] _vertices =
    {
        new Float3(0, 0, 0),
        new Float3(100, 0, 0),
        new Float3(100, 100, 0),
        new Float3(0, 100, 0),
        new Float3(0, 100, 100),
        new Float3(100, 100, 100),
        new Float3(100, 0, 100),
        new Float3(0, 0, 100),
    };

    private static readonly uint[] _triangles =
    {
        0, 2, 1, // Face front
        0, 3, 2,
        2, 3, 4, // Face top
        2, 4, 5,
        1, 2, 5, // Face right
        1, 5, 6,
        0, 7, 4, // Face left
        0, 4, 3,
        5, 4, 7, // Face back
        5, 7, 6,
        0, 6, 7, // Face bottom
        0, 1, 6
    };

    private GPUBuffer _vertexBuffer;
    private GPUBuffer _indexBuffer;
    private GPUPipelineState _psCustom;
    private Shader _shader;

    public Shader Shader
    {
        get => _shader;
        set
        {
            if (_shader != value)
            {
                _shader = value;
                ReleaseShader();
            }
        }
    }

    public override unsafe void OnEnable()
    {
        UseSingleTarget = true; // This postfx overdraws the input buffer without using output
        Location = PostProcessEffectLocation.BeforeForwardPass; // Custom draw location in a pipeline

        // Create vertex buffer for custom geometry drawing
        _vertexBuffer = new GPUBuffer();
        fixed (Float3* ptr = _vertices)
        {
            var desc = GPUBufferDescription.Vertex(sizeof(Float3), _vertices.Length, new IntPtr(ptr));
            _vertexBuffer.Init(ref desc);
        }

        // Create index buffer for custom geometry drawing
        _indexBuffer = new GPUBuffer();
        fixed (uint* ptr = _triangles)
        {
            var desc = GPUBufferDescription.Index(sizeof(uint), _triangles.Length, new IntPtr(ptr));
            _indexBuffer.Init(ref desc);
        }

#if FLAX_EDITOR
        // Register for asset reloading event and dispose resources that use shader
        Content.AssetReloading += OnAssetReloading;
#endif

        // Register postFx to all game views (including editor)
        SceneRenderTask.AddGlobalCustomPostFx(this);
    }

#if FLAX_EDITOR
    private void OnAssetReloading(Asset asset)
    {
        // Shader will be hot-reloaded
        if (asset == Shader)
            ReleaseShader();
    }
#endif

    public override void OnDisable()
    {
        // Remember to unregister from events and release created resources (it's gamedev, not webdev)
        SceneRenderTask.RemoveGlobalCustomPostFx(this);
#if FLAX_EDITOR
        Content.AssetReloading -= OnAssetReloading;
#endif
        ReleaseShader();
        Destroy(ref _vertexBuffer);
        Destroy(ref _indexBuffer);
    }

    private void ReleaseShader()
    {
        // Release resources using shader
        Destroy(ref _psCustom);
    }

    public override bool CanRender()
    {
        return base.CanRender() && Shader && Shader.IsLoaded;
    }

    public override unsafe void Render(GPUContext context, ref RenderContext renderContext, GPUTexture input, GPUTexture output)
    {
        // Here we perform custom rendering on top of the in-build drawing

        // Setup missing resources
        if (!_psCustom)
        {
            _psCustom = new GPUPipelineState();
            var desc = GPUPipelineState.Description.Default;
            desc.VS = Shader.GPU.GetVS("VS_Custom");
            desc.PS = Shader.GPU.GetPS("PS_Custom");
            desc.DepthEnable = true;
            desc.DepthWriteEnable = true;
            desc.CullMode=CullMode.TwoSided;

            // Enable blending for transparency
            desc.BlendMode = BlendingMode.AlphaBlend;

            _psCustom.Init(ref desc);
        }

        // Set constant buffer data (memory copy is used under the hood to copy raw data from CPU to GPU memory)
        var cb = Shader.GPU.GetCB(0);
        if (cb != IntPtr.Zero)
        {
            var data = new Data();
            Matrix.Multiply(ref renderContext.View.View, ref renderContext.View.Projection, out var viewProjection);
            Actor.GetLocalToWorldMatrix(out var world);
            Matrix.Transpose(ref world, out data.WorldMatrix);
            Matrix.Transpose(ref viewProjection, out data.ViewProjectionMatrix);
            context.UpdateCB(cb, new IntPtr(&data));
        }

        // Draw geometry using custom Pixel Shader and Vertex Shader
        context.BindCB(0, cb);
        context.BindIB(_indexBuffer);
        context.BindVB(new[] {_vertexBuffer});
        context.SetState(_psCustom);
        context.SetRenderTarget(renderContext.Buffers.DepthBuffer.View(), input.View());
        context.DrawIndexed((uint)_triangles.Length);
    }
}

With a few minor changes as discussed above. Here is my super basic test shader, all it does is try to set o.depth to a constant. Essentially setting o.depth to 0 or 1 should make the object fully in front or behind everything else. But it is ignored, as you can see from the screenshot of the green cube above, where the green cube depth is written as normal no matter the value of o.depth.

#include "./Flax/Common.hlsl"

META_CB_BEGIN(0, Data)
float4x4 WorldMatrix;
float4x4 ViewProjectionMatrix;
META_CB_END

// Geometry data passed to the vertex shader
struct ModelInput
{
    float3 Position : POSITION;
};

// Interpolants passed from the vertex shader
struct VertexOutput
{
    float4 Position : SV_Position;
    float3 WorldPosition : TEXCOORD0;
};

// Interpolants passed to the pixel shader
struct PixelInput
{
    float4 Position : SV_Position;
    float3 WorldPosition : TEXCOORD0;
};

struct PixelShaderOutput
{
    float4 Color : SV_Target; // Existing color output
    float Depth : SV_Depth;   // Additional depth output
};

// Vertex shader function for custom geometry processing
META_VS(true, FEATURE_LEVEL_ES2)
META_VS_IN_ELEMENT(POSITION, 0, R32G32B32_FLOAT, 0, 0, PER_VERTEX, 0, true)
VertexOutput VS_Custom(ModelInput input)
{
    VertexOutput output;
    output.WorldPosition = mul(float4(input.Position.xyz, 1), WorldMatrix).xyz;
    output.Position = mul(float4(output.WorldPosition.xyz, 1), ViewProjectionMatrix);
    return output;
}

struct frag_out
{
    float4 color : SV_Target0;
    float depth : SV_Depth;
};

META_PS(true, FEATURE_LEVEL_ES3)
frag_out PS_Custom(PixelInput input) : SV_Target
{
    frag_out o;
    o.color=float4(0, 1, 0, 0.5);
    o.depth=0.0; //Just for testing. No matter what value (0-1) I use, the result is the normal cube depth is written 

    return o;
}

I tried to play around with other settings in the shader, but unfortunately Flax crashed a lot when trying more exotic things so in the end I got stuck.

For the full context, I wrote a simple shader in Unity that illustrates what I am trying to achieve:

Shader "Unlit/TestShader"
{
    Properties
    {
    }
    SubShader
    {
        Tags { "Queue"="AlphaTest" }
        
        Cull Front

        Pass
        {
            Tags { "RenderType"="AlphaTest" }
            ZWrite On

            CGPROGRAM   

            #include "UnityCG.cginc"
            #define PI 3.14159265358979323846

            #pragma vertex vert
            #pragma fragment frag
            
            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 world_pos : TEXCOORD0;
	            float3 ray_origin : TEXCOORD2;
	            float3 ray_direction : TEXCOORD3;
            };

            struct frag_out
            {
                half4 color : SV_Target0;
                float depth : SV_Depth;
            };
            
            struct ray
            {
                float3 origin;
                float3 dir;
            };

            v2f vert(appdata_base v)
            {
                v2f o;
                o.world_pos = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.pos = mul(unity_MatrixVP, float4(o.world_pos, 1));
	            o.ray_origin = mul(unity_WorldToObject, _WorldSpaceCameraPos);
	            o.ray_direction = v.vertex - o.ray_origin;
                return o;
            }
            
            bool cast_ray(ray r, out float3 hit_pos, out float4 color)
            {
                float sphere_radius = 0.5;
                float3 oc = r.origin;
                float a = dot(r.dir, r.dir);
                float b = 2.0 * dot(oc, r.dir);
                float c = dot(oc, oc) - sphere_radius * sphere_radius;
                float discriminant = b*b - 4*a*c;

                if (discriminant > 0)
                {
                    float temp = (-b - sqrt(discriminant)) / (2.0 * a);
                    if (temp < 0)
                    {
                        temp = (-b + sqrt(discriminant)) / (2.0 * a);
                    }
                    if (temp < 0)
                    {
                        return false;
                    }
                    hit_pos = r.origin + temp * r.dir;
                    float3 normal = hit_pos / sphere_radius;
                    color=float4(normal.x,normal.y,normal.z,1);
                    return true;
                }
                return false;
            }

            frag_out frag(v2f i)
            {
                frag_out o;

                ray ray;
                // These are initially in world space
                ray.origin = _WorldSpaceCameraPos;
                ray.dir = normalize(i.world_pos - ray.origin);

                // Transform the ray into the object's local space
                ray.origin = mul(unity_WorldToObject, float4(ray.origin, 1.0)).xyz;
                ray.dir = mul(unity_WorldToObject, ray.dir);
                ray.dir = normalize(ray.dir);

                float3 hit_obj_pos;
                float4 color;

                if (cast_ray(ray, hit_obj_pos, color))
                {
                    //Return from objects local space to world space
                    const float3 hit_world_pos = mul(unity_ObjectToWorld, float4(hit_obj_pos, 1.0)).xyz;
                    o.color = color;
                    const float4 clip_pos = UnityWorldToClipPos(hit_world_pos);
                    o.depth = clip_pos.z / clip_pos.w;
                    return o;
                }
                discard;
                return o;
            }
            
            ENDCG
        }
    }
}

The idea is by using this technique we can add pathtraced/raymarched/raytraced objects and insert them into the scene like any other object.

image

This image shows the raytraced colored sphere interacting with a regular cube in the Unity editor. The sphere is in fact a cube mesh, with the raytracing-material on each face. But as you see, it does appear like a true sphere object since it modifies the depth in the fragment shader.

It is not a mainstream effect, but it is quite powerful as it enables raytracing effects on non-RTX hardware, in parts of the scene. The new game I am researching will be dependent on this mechanism.

Just remove leftover SV_Target from that pixel shader as it’s already defined in the output structure:

struct frag_out
{
    float4 color : SV_Target;
    float depth : SV_Depth;
};

META_PS(true, FEATURE_LEVEL_ES3)
frag_out PS_Custom(PixelInput input)
{
..

Without it, D3D11/D3D12 fails to properly bind render targets. Surprisingly on Vulkan it works. After that change, it works everywhere.

1 Like

That did it! Thanks :slight_smile: