WebGL rendering of solid trails

Aug. 05, 2012

If you need to render trails you can use particles. These give nice puffy effect. At other times you'd like to have a more well defined line (like say for missile trails). The following post shows one technique render trails with a single triangle strip optimized not to use too many triangles and does not lead to the puffy look of particles.



You can try the live demo for yourself.



You'll find the sourcecode for this library on github

How it works

The trail consists of a single triangle strip in the following Format for each vertex

  • vec4 last: the last center trail position in xyz and the offset (-1 or 1) in w
  • vec4 current: the current center trail position in xyz and the offset (-1 or 1) in w
  • vec4 next: the next center trail position in xyz and the offset (-1 or 1) in w
  • float texoff: continous texture offset along the trails direction
  • vec3 barycentric: Barycentric Coordinates used for debugging

The layout of the buffer makes efficient use of the sparse triangle strip position by reusing data. The pointers are overlapping and offset by 2 vertices each (i.e. last has offset 0 vertices, current has offset 2 vertices and next has offset 4 vertices). See illustration.

This lets us get access to the last and next vertexes data without having to duplicate the data in the buffer.

Center offset

The w member of the position is the offset. This is used to know if we're in the top or bottom vertex of the trail. In order to use this offset. We need to figure out the right direction to offset to. We project the vertices into screen space, take the direction from the last vertex to the current and from the current vertex to the next, average this normal and rotate by 90° clockwise.

vec2 sLast = project(transform(last.xyz));
vec2 sNext = project(transform(next.xyz));

vec4 dCurrent = transform(current.xyz);
vec2 sCurrent = project(dCurrent);

vec2 normal1 = normalize(sLast - sCurrent);
vec2 normal2 = normalize(sCurrent - sNext);
vec2 normal = normalize(normal1 + normal2);
float off = current.w;
float angle = atan(normal.x, normal.y)+pi*0.5;
vec2 dir = vec2(sin(angle), cos(angle))*off;

Size Estimation

To figure out how broad the trail should be in screenspace we use an estimation by offsetting by the desired width in view space towards the center of the screen, and projecting to screenspace, then measure the length.

float estimateScale(vec3 position, vec2 sPosition){
    vec4 view_pos = view * vec4(position, 1.0);
    vec4 scale_pos = view_pos - vec4(
        normalize(view_pos.xy)*width, 0.0, 0.0
    vec2 screen_scale_pos = project(proj * scale_pos);
    return distance(sPosition, screen_scale_pos);

Outputting the Result

Finally we offset the screenspace position by the computed vector and unproject this position to device space and substitute z and w from the original center vertex of the current position.

vec2 pos = sCurrent + dir*scale;
gl_Position = unproject(pos, dCurrent.z, dCurrent.w);

Why do it in Screenspace?

A similar algorithm can be used in 3d space. The advantage of doing it in screenspace is that the spatial curvature of a trail does not equals the screen curvature of the trail. The estimation of the outline direction from the center of the trail does not match the optimal position as the vertices will be projected on screen.