Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

gl_PointSize Corresponding to World Space Size

Tags:

opengl

glsl

If you want to render an imposter geometry (say like a sphere), then the standard practice is to draw it using two triangles (say by passing one vertex and making a triangle strip with a geometry shader).

This is nice because it allows the extent of the billboard to be set fairly simply: you compute the actual world space positions directly.

Geometry shaders can alternately output point primitives, and I don't see a reason why they shouldn't. The only issue is finding some way to scale gl_PointSize so that you get that effect.

The only precedent I could find were this question (whose answer I am unsure is correct) and this question (which is unanswered).

It's worth noting that it's fairly simple to scale the point correctly with distance (by doing gl_PointSize = constant/length(gl_Position), but this isn't controllable; you can't say for example: I want this point to look like it is two world units across.

So: anyone know how to do this?

like image 799
imallett Avatar asked Sep 11 '14 06:09

imallett


1 Answers

A straight forward idea is to transform a point at the top and bottom of the particle into screen space and find the distance. This cancels very nicely and it's pretty simple to work with just the y coordinate.

The billboard is screen aligned, and view matrices generally don't scale, so the particle size in world space is the same as eye space. That just leaves the projection to get to NDC, the divide by w and scaling by the viewport size.

A typical projection matrix, P, might look something like this...

[ +1.2990 +0.0000 +0.0000 +0.0000 ]
[ +0.0000 +1.7321 +0.0000 +0.0000 ]
[ +0.0000 +0.0000 -1.0002 -0.0020 ]
[ +0.0000 +0.0000 -1.0000 +0.0000 ]

Starting with y_eye, a y coordinate in eye space, the image space coordinate y_image is obtained in pixels...

enter image description here

Plugging in the radius above/below the billboard and subtracting cancels to...

enter image description here

Or, in text, pixelSize = vpHeight * P[1][1] * radius / w_clip

For a perspective projection, P[1][1] = 1 / tan(fov_y / 2). w_clip is gl_Position.w, which is also -z_eye (from the -1 in the perspective matrix). To guarantee your point covers every pixel you want, this may need an additional small constant.


Side note: A sphere on a billboard will look OK in the middle of the screen. If you have a large field of view perspective projection, a true sphere should warp as it approaches the edges of the screen. You could implicitly raycast the virtual sphere for each pixel in the billboard to get a correct result, but the billboard boundary will need to be adjusted accordingly. Quick google results: 1 2 3 4


[EDIT]
Well, since I bothered to test this I'll throw my shaders here too...

Vertex:

#version 150

in vec4 osVert;

uniform mat4 projectionMat;
uniform mat4 modelviewMat;
uniform vec2 vpSize;

flat out vec2 centre;
flat out float radiusPixels;

const float radius = 1.0;

void main()
{
    gl_Position = projectionMat * modelviewMat * osVert;
    centre = (0.5 * gl_Position.xy/gl_Position.w + 0.5) * vpSize;
    gl_PointSize = vpSize.y * projectionMat[1][5] * radius / gl_Position.w;
    radiusPixels = gl_PointSize / 2.0;
}

Fragment:

#version 150

flat in vec2 centre;
flat in float radiusPixels;

out vec4 fragColour;

void main()
{
    vec2 coord = (gl_FragCoord.xy - centre) / radiusPixels;
    float l = length(coord);
    if (l > 1.0)
        discard;
    vec3 pos = vec3(coord, sqrt(1.0-l*l));
    fragColour = vec4(vec3(pos.z), 1.0);
}

enter image description here

(Note the visible gap at the bottom right is incorrect as described above)

like image 163
jozxyqk Avatar answered Oct 18 '22 21:10

jozxyqk