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_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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
1 2 3 4 5 6 7 8 9 10
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
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”.
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 $:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
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.
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
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