Search code examples
c++directxshaderhlsl

Geometry shader output filling Z-Buffer with closest value possible? (DirectX)


I need lines of a certain thickness in my CAD viewer and found out that I should use a geometry shader to accomplish this.
I then went ahead and found demo code of a geometry shader that creates lines at a given thickness.

Here: https://github.com/paulhoux/Cinder-Samples/blob/master/GeometryShader/assets/shaders/lines1.geom
Discussion about it here: https://forum.libcinder.org/topic/smooth-thick-lines-using-geometry-shader

This shader was written in OpenGL but I translated it to DirectX.
The issue I got now is that the faces generated by the shader are ignoring my Z-Buffer (Depth-Buffer), did I miss something really important?

My full shader code:

//Input for the vertex shader
struct VS_IN
{
    float4 pos : POSITION;
    float4 col : COLOR;
};

//Input for the geometry shader, as provided by the vertex shader
struct GS_IN
{
    float4 pos : SV_POSITION;
    float4 col : COLOR;
};

//Input for the pixel shader, as provided by the geometry shader
struct PS_IN
{
    float4 pos : SV_POSITION;
    float4 col : COLOR;
};

//Only used in the vertex shader, to give the vertex its final position on screen
cbuffer viewProj : register (b0)
{
    matrix viewProj;
}

//The vertex shader
GS_IN VS(VS_IN input)
{
    GS_IN output = (GS_IN)0;

    //Should multiply it by a world matrix of identity, so the input vertices should be using world coordinates...
    output.pos = mul(input.pos, viewProj);
    output.col = input.col;

    return output;
};

//The geometry shader

cbuffer windowSize : register (b1)
{
    float2 WIN_SCALE;
}

float MITER_LIMIT = -1;
float THICKNESS = 10;

float2 screen_space(float4 vertex)
{
    return float2(vertex.xy / vertex.w) * WIN_SCALE;
}

[maxvertexcount(7)]
void GS(lineadj GS_IN input[4], inout TriangleStream<PS_IN> OutputStream)
{
    // get the four vertices passed to the shader:
    float2 p0 = screen_space(input[0].pos); // start of previous segment
        float2 p1 = screen_space(input[1].pos); // end of previous segment, start of current segment
        float2 p2 = screen_space(input[2].pos); // end of current segment, start of next segment
        float2 p3 = screen_space(input[3].pos); // end of next segment

        // perform naive culling
        /*
        float2 area = WIN_SCALE * 1.2;
        if (p1.x < -area.x || p1.x > area.x) return;
        if (p1.y < -area.y || p1.y > area.y) return;
        if (p2.x < -area.x || p2.x > area.x) return;
        if (p2.y < -area.y || p2.y > area.y) return;
        */

        // determine the direction of each of the 3 segments (previous, current, next)
        float2 v0 = normalize(p1 - p0);
        float2 v1 = normalize(p2 - p1);
        float2 v2 = normalize(p3 - p2);

        // determine the normal of each of the 3 segments (previous, current, next)
        float2 n0 = float2(-v0.y, v0.x);
        float2 n1 = float2(-v1.y, v1.x);
        float2 n2 = float2(-v2.y, v2.x);

        // determine miter lines by averaging the normals of the 2 segments
        float2 miter_a = normalize(n0 + n1);    // miter at start of current segment
        float2 miter_b = normalize(n1 + n2);    // miter at end of current segment

        // determine the length of the miter by projecting it onto normal and then inverse it
        float length_a = THICKNESS / dot(miter_a, n1);
    float length_b = THICKNESS / dot(miter_b, n1);

    PS_IN output = (PS_IN)0;
    if (dot(v0, v1) < -MITER_LIMIT) {
        miter_a = n1;
        length_a = THICKNESS;

        // close the gap
        if (dot(v0, n1) > 0) {
            output.col = input[1].col;
            output.pos = float4((p1 + THICKNESS * n0) / WIN_SCALE, 0.0, 1.0);
            OutputStream.Append(output);

            output.col = input[1].col;
            output.pos = float4((p1 + THICKNESS * n1) / WIN_SCALE, 0.0, 1.0);
            OutputStream.Append(output);

            output.col = input[1].col;
            output.pos = float4(p1 / WIN_SCALE, 0.0, 1.0);
            OutputStream.Append(output);
        }
        else {
            output.col = input[1].col;
            output.pos = float4((p1 - THICKNESS * n1) / WIN_SCALE, 0.0, 1.0);
            OutputStream.Append(output);

            output.col = input[1].col;
            output.pos = float4((p1 - THICKNESS * n0) / WIN_SCALE, 0.0, 1.0);
            OutputStream.Append(output);

            output.col = input[1].col;
            output.pos = float4(p1 / WIN_SCALE, 0.0, 1.0);
            OutputStream.Append(output);
        }
    }

    if (dot(v1, v2) < -MITER_LIMIT) {
        miter_b = n1;
        length_b = THICKNESS;
    }

    // generate the triangle strip
    output.col = input[1].col;
    output.pos = float4((p1 + length_a * miter_a) / WIN_SCALE, 0.0, 1.0);
    OutputStream.Append(output);

    output.col = input[1].col;
    output.pos = float4((p1 - length_a * miter_a) / WIN_SCALE, 0.0, 1.0);
    OutputStream.Append(output);

    output.col = input[2].col;
    output.pos = float4((p2 + length_b * miter_b) / WIN_SCALE, 0.0, 1.0);
    OutputStream.Append(output);

    output.col = input[2].col;
    output.pos = float4((p2 - length_b * miter_b) / WIN_SCALE, 0.0, 1.0);
    OutputStream.Append(output);
};

//The pixel shader
float4 PS(PS_IN input) : SV_Target
{
    return input.col;
};


technique10 Render
{
    pass P0
    {
        SetVertexShader(CompileShader(vs_4_0, VS()));
        SetGeometryShader(CompileShader(gs_4_0, GS()));
        SetPixelShader(CompileShader(ps_4_0, PS()));
    }
}

If I disable the Geometry shader I get:
Without Geometry Shader
If I enable the Geometry shader I get:
With Geometry Shader
As you can see the thick line suddenly is in front of everything else.

Can this be fixed? Should I use a different approach? Are geometry shaders really the solution?


Edit 1: I found out that it's not ignoring the Z-Buffer, it's filling it with the closest depth-value possible (black in debugging). Why is this happening?


Solution

  • I think that geometry buffers are a good and fast solution for your case.

    Your error happens because your geometry buffer sets all z-values to 0.0, so any depth information is dropped. The outputted triangles are all on the same depth plane in screen space. To fix this, they should inherit the depth value from the original lines.

    Keep in mind that this could lead to intersection issues, e.g. if your line is near a ground, the inflated line would penetrate the ground and can look thinner than other ones.