Error with custom line renderer and w homogeneus coordinate

#1

Hi! I’m currently working on a custom line renderer for my 3d editor. The idea is to get constant width lines in pixel units. I’m using a geometry shader to create a triangle strip from a line by offseting the vertices positions a little in screen space according to the desired width.

Vertex shader:

#version 330 core
layout (location = 0) in vec3 aPosition;

uniform mat4 uView;
uniform mat4 uProjection;

void main()
{
	gl_Position = uProjection * uView * vec4(aPosition, 1.0);
}

Geometry shader:

#version 330 core
layout (lines) in;
layout (triangle_strip, max_vertices = 4) out;

uniform float uThickness;
uniform vec2 uViewport;

vec2 toScreenSpace(vec4 vertex, vec2 viewport) {
    return vec2(vertex.xy / vertex.w) * viewport;
}

void main()
{
    vec2 p0 = toScreenSpace(gl_in[0].gl_Position, uViewport);
    vec2 p1 = toScreenSpace(gl_in[1].gl_Position, uViewport);

    vec2 dir = normalize(p1 - p0);
    vec2 normal = vec2(-dir.y, dir.x);

    gl_Position = vec4((p0 + uThickness * normal) / uViewport, 0.0, 1.0);
    EmitVertex();
    gl_Position = vec4((p0 - uThickness * normal) / uViewport, 0.0, 1.0);
    EmitVertex();
    gl_Position = vec4((p1 + uThickness * normal) / uViewport, 0.0, 1.0);
    EmitVertex();
    gl_Position = vec4((p1 - uThickness * normal) / uViewport, 0.0, 1.0);
    EmitVertex();  

    EndPrimitive();
}

Fragment shader:

#version 330 core
layout (location = 0) out vec4 FragColor;

uniform vec4 uColor;

void main()
{
    FragColor = uColor;
}

The line gets rendered correctly when I’m far from it and everything looks good but when I get too close to it and the line intersects (I think) with the near plane, I start getting errors. I assume it’s due to the division by the w component. I’ve been trying to fix it for two whole days now and I don’t know how to do make it work. Any ideas?

I recorded a video but I’m not allowed to embed it. This is the YouTube link:
https://youtu.be/y-JnEvyeIG0
Sorry for that.

Thank you very much in advance!

#2

It’s not the near plane, it’s the W=0 plane (i.e. the Z=0 plane in eye space). If you project a point that’s behind the viewpoint, it’s reflected in the viewpoint. E.g. (1,1,-1) has the same screen-space position as (-1,-1,1).

In short, you can’t (meaningfully) convert clip-space coordinates to screen space until after you’ve clipped the line.

You can obtain the screen-space direction vector directly as [w1.x2-w2.x1, w1.y2-w2.y1], then the perpendicular from that. Try adding/subtracting the perpendicular scaled by W to each point (in clip space). I.e.

vec2 thick = uThickness / uViewport;

vec4 p0 = gl_in[0].gl_Position;
vec4 p1 = gl_in[1].gl_Position;

vec2 dir = p0.w * p1.xy - p1.w * p0.xy;
vec2 normal = vec2(-dir.y, dir.x);

gl_Position = p0 + thick * normal * p0.w;
EmitVertex();
gl_Position = p0 - thick * normal * p0.w;
EmitVertex();
gl_Position = p1 + thick * normal * p1.w;
EmitVertex();
gl_Position = p1 - thick * normal * p1.w;
EmitVertex();

EndPrimitive();

Note: I haven’t tested that and it’s 1am here.

#3

Hi! Thank you very much @GClements! Your code worked almost perfectly, the only change I made was normalizing the dir vector:

#version 330 core
layout (lines) in;
layout (triangle_strip, max_vertices = 4) out;

uniform float uThickness;
uniform vec2 uViewport;

void main()
{
    vec2 thick = uThickness / uViewport;

    vec4 p0 = gl_in[0].gl_Position;
    vec4 p1 = gl_in[1].gl_Position;

    normalize(p0.w * p1.xy - p1.w * p0.xy);
    vec2 normal = vec2(-dir.y, dir.x);

    vec2 offset = thick * normal;
    gl_Position = p0 + vec4(offset * p0.w, 0.0, 0.0);
    EmitVertex();
    gl_Position = p0 - vec4(offset * p0.w, 0.0, 0.0);
    EmitVertex();
    gl_Position = p1 + vec4(offset * p1.w, 0.0, 0.0);
    EmitVertex();
    gl_Position = p1 - vec4(offset * p1.w, 0.0, 0.0);
    EmitVertex();

    EndPrimitive();
}

Now it intersects with the camera correctly but when I’m too close to the line (I assume it’s still when it intersects the W=0 plane), the constant width stops working and it gets bigger and bigger on one side only.

Here’s a video showing what I mean:
https://youtu.be/8VgtWpe8p4s

After disabling face culling this is what I get:

I’m pretty lost at this point but I feel we’re getting close. I’ve been trying to fix it for a while but I can’t find the solution.

Thank you very much!

#4

Try this:

    vec4 offset = vec4(thick * normal, 0, 0);

    if (p0.w < 0) {
        gl_Position = (p0.w * p1 - p1.w * p0) / (p0.w - p1.w);
        EmitVertex();
    }
    else {
        gl_Position = p0 - offset * p0.w;
        EmitVertex();

        gl_Position = p0 + offset * p0.w;
        EmitVertex();
    }

    if (p1.w < 0) {
        gl_Position = (p0.w * p1 - p1.w * p0) / (p0.w - p1.w);
        EmitVertex();
    }
    else {
        gl_Position = p1 - offset * p1.w;
        EmitVertex();

        gl_Position = p1 + offset * p1.w;
        EmitVertex();
    }

    EndPrimitive();

In the case where an endpoint has W<0, it uses a single vertex at the point where the centre-line intersects the W=0 plane.

It still isn’t quite right; near lines look slightly tapered. It’s possible that this is related to clipping against the near plane.

The other option is to clip the line segment in the geometry shader then apply the thickening to the clipped line.

1 Like
#5

Thank you very much! I’m not sure I fully understand the solution but it worked wonderfully!
I’ll leave the final code here in case someone is trying to achieve something similar.

Vertex Shader:

    #version 330 core
    layout (location = 0) in vec3 aPosition;

    uniform mat4 uView;
    uniform mat4 uProjection;

    void main()
    {
        gl_Position = uProjection * uView * vec4(aPosition, 1.0);
    }

Geometry Shader:

    #version 330 core
    layout (lines) in;
    layout (triangle_strip, max_vertices = 4) out;
    
    uniform float uThickness;
    uniform vec2 uViewport;
    
    void main()
    {
        vec2 thick = uThickness / uViewport;
        
        vec4 p0 = gl_in[0].gl_Position;
        vec4 p1 = gl_in[1].gl_Position;
        
        vec2 dir = normalize(p0.w * p1.xy - p1.w * p0.xy);
        vec2 normal = vec2(-dir.y, dir.x);
        
        vec4 offset = vec4(thick * normal, 0.0, 0.0);
        if (p0.w < 0) {
            gl_Position = (p0.w * p1 - p1.w * p0) / (p0.w - p1.w);
            EmitVertex();
        } else {
            gl_Position = p0 + offset * p0.w;
            EmitVertex();
            gl_Position = p0 - offset * p0.w;
            EmitVertex();
        }
        if (p1.w < 0) {
            gl_Position = (p0.w * p1 - p1.w * p0) / (p0.w - p1.w);
            EmitVertex();
        } else {
            gl_Position = p1 + offset * p1.w;
            EmitVertex();
            gl_Position = p1 - offset * p1.w;
            EmitVertex();
            }
            
        EndPrimitive();
    }

Fragment Shader:

    #version 330 core
    layout (location = 0) out vec4 FragColor;

    uniform vec4 uColor;

    void main()
    {
        FragColor = uColor;
    }

This is the end result:
https://youtu.be/4JSkORLQnyg
Line Renderer

Thank you very much for your help!