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?
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...
Plugging in the radius above/below the billboard and subtracting cancels to...
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);
}
(Note the visible gap at the bottom right is incorrect as described above)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With