A Circular Reference

Adventures of a vagabond electron.

Drawing Anti-aliased Circular Points Using OpenGL/WebGL

| Comments

This article explores points in OpenGL, and I use WebGL to illustrate the concepts. The demos may not render in Internet Explorer as I use new ECMA Script 6 syntax for strings (storing the shader code) which apparently is not supported by IE.

Drawing a Point

If you discount the WebGL boilerplate code, drawing a point is easy. Attributes to the vertex shader are a vec3 point position, vec4 color of the point and a float point size. The shader updates the OpenGL built-in variables gl_Position and gl_PointSize from this and passes the color as a varying to the fragment shader. The fragment shader simply outputs the color it got from the vertex shader. Note that the maximum Point Size for WebGL is only guaranteed to be at least equal to 1 pixel, though in most cases it’s more than one.

[Our simple vertex shader] [lang : c] (vs-points.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var vs_points = `
attribute vec3 vPosition;
attribute vec4 vRgbaColor;
attribute float vPointSize;
varying  vec4 color;

void
main()
{
    gl_Position =  vec4(vPosition, 1.0);
	//gl_PointSize = 10.0;
	gl_PointSize = vPointSize;
    color = vRgbaColor;
	//color = vec4(1.0,0.0,0.0,1.0);
}`;
[Our simple fragment shader] [lang : c] (fs_point_default.js) download
1
2
3
4
5
6
7
8
9
10
var fs_point_default = `
precision mediump float;
varying  vec4 color;

void
main()
{
    float alpha = 1.0;
    gl_FragColor = color * (alpha);
}`;

I will ignore the boilerplate code as there’s nothing special about it. You can figure it out by just going through the points.js script. The relevant high-level code is shown below:

1
2
3
4
5
6
7
8
9
10
11
  var point = [0.0, 0.0, 0.0];
  var color = [1.0, 0.0, 0.0, 1.0];
  var size = [400.0];

  var canvas = document.getElementById("point-canvas");
  initGL(canvas);
  shaderPointDefault = initShaders(0);
  initBuffers(point, color, size);
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  gl.enable(gl.DEPTH_TEST);
  drawScene(shaderPointDefault);

The result of this is - a nice big square!

Drawing a disk

To get a circle out of this square we need to first understand the square better. An important parameter is gl_PointCoord. This is available at the Fragment Shader and tells the location of the current fragment inside the current point. The idea is that depending on the value of gl_PointSize, a point will be actually composed of many pixel fragments and this parameter allows us to identify each of these fragments. Using this information we can draw any figure inside a “point”.

Now gl_PointCoord has an X and Y coordinate and varies from $ [0,1] $ just like a 2D texture. Recall that the equation of a circle with origin at the center and radius 1 unit is

This is the same as the dot product of the 2D vector with itself. Since GLSL provides us a dot() function, we can draw a circle in the square by coloring the region in space where the dot product of a vector with itself is $ <= 1 $:

[Extract a circle from a point] [lang : c] (fs_point_alias.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var fs_point_alias = `
precision mediump float;
varying  vec4 color;

void
main()
{
    float r = 0.0, delta = 0.0, alpha = 1.0;
    vec2 cxy = 2.0 * gl_PointCoord - 1.0;
    r = dot(cxy, cxy);
    if (r > 1.0) {
        discard;
    }
    gl_FragColor = color * (alpha);
}`;

All pixels outside the radius of 1 are discarded. The result is:

Since we discard all pixels outside the radius, you can see that the edges are rough and aliasing is pretty bad. Use the CNTL + keys to zoom into the web page to see the distortion better.

Anti-aliasing the disk

Instead of abruptly discarding the pixels at the fragments where $ r > 1 $, we need to smooth out the colors at the edge pixels. One way to do this is to use the smoothstep() function provided by GLSL that allows you to interpolate the value of a step function in the region where the independent variable lies between the min and max values. In the case of a normal step function that we indirectly used before, the min and max was $ [1,1] $ respectively - anything greater than 1 is clipped to 1 and anything less than 1 is clipped to 0. To use smoothstep, we need to find an $ \epsilon $ so that we can define a range of $ [1+\epsilon, 1-\epsilon] $ overwhich the color of the fragments can be smoothed.

The GLSL fwidth() function can give us this information. It returns the maximum change in a given fragment shader variable in the neighbourhood of the current pixel (8 surrounding pixels). So fwidth(r) gives us the $ \epsilon $ and we can have a shader as:

[Our simple vertex shader] [lang : c] (fs_point_anti_alias.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var fs_point_anti_alias = `
#ifdef GL_OES_standard_derivatives
#extension GL_OES_standard_derivatives : enable
#endif

precision mediump float;
varying  vec4 color;

void main()
{
    float r = 0.0, delta = 0.0, alpha = 1.0;
    vec2 cxy = 2.0 * gl_PointCoord - 1.0;
    r = dot(cxy, cxy);
#ifdef GL_OES_standard_derivatives
    delta = fwidth(r);
    alpha = 1.0 - smoothstep(1.0 - delta, 1.0 + delta, r);
#endif

gl_FragColor = color * alpha;

}`;

And now we finally have a lovely circle.

A demo to put it together

The below demo draws such point-circles of randomly varying sizes, color, alpha and position. The idea for the demo is from the Gamedueno2 book The rendering code uses callbacks registered with the browser requestAnimationFrame API insead of a timer based rendering. Use the sliders to vary the maximum size of the circles and their total number and see the impact on the fps. The point size slider is clipped to the max point size obtained by querying ALIASED_POINT_SIZE_RANGE.

Max Size:
Count:
fps:


References

Comments