12.7 - Rendering Points

In preparation for a discussion of “particle systems” in the next lesson, this lesson explains how to render points using WebGL.

Points

A point is a single location in 3D space defined by a (x,y,z) value. By default, the rendering of a point sets the color of one pixel in an image. The color of multiple pixels can be set by specifying the gl_PointSize property of a point in a vertex shader. gl_PointSize is the side length of a square centered at the point’s raster location. It is a floating point value in pixel units. The exact definition from the OpenGl ES 2.0 specification is:

gl_PointSize

Point rasterization produces a fragment for each framebuffer pixel whose center lies inside a square centered at the point’s (xw, yw), with side length equal to the point size.

If the point (xw, yw) is not directly in the center of a pixel and the value of gl_PointSize is fractional, the area colored for a “point” will be rectangular.

Anti-aliasing

Anti-aliasing is a technique for minimizing the errors due to sampling a 3D scene only at the center of each pixel location. With no anti-aliasing, a pixel is either colored, or not colored, depending on whether the center of the pixel is inside a primitive’s boundaries. This produces a jagged edge at the boundaries of a triangle, line, or point rendering. When anti-aliasing is enabled, pixels receive a portion of the color of a primitive based on a percentage of coverage of a pixel. The images below show a point rendered with a gl_PointSize of 8.0 and enlarged to view individual pixels. With anti-aliasing disabled, a point rendered with a size of 8.0 will color exactly 64 pixels. With anti-aliasing enabled, the edges around the 64 pixels will potentially have partial coloring.

../_images/antialiasing.png

WebGL enables anti-aliasing by default. To disable it, the antialias option must be set to false when the WebGL context is initially created. The commands look something like this:

canvas = document.getElementById(canvas_id);
gl = canvas.getContext('webgl', {antialias : false} );

Anti-aliasing is rarely disabled. This discussion was primarily to help you understand why the borders of a multi-pixel “point” are typically lighter shades of the color assigned by a fragment shader.

Experiment with gl_PointSize

The following WebGL program renders eight points at the corners of a cube. Experiment with the program by varying the point size.

Show: Code   Canvas   Run Info
../_static/12_points1/points1.html

Experiment with gl_PointSize.

Please use a browser that supports "canvas"
Size = 2.0 1.0 100.0
Valid point sizes on this WebGL system:
(Use query: gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE))
Animate
Show: Process information    Warnings    Errors
Open this webgl program in a new tab or window

Experiments:

  • Disable anti-aliasing when the gl context is created. (See lines 107-108 in the points1_scene.js file.) To see the difference between anti-aliasing and no anti-aliasing use any tool on your computer that allows the screen to be “zoomed in”.

    • On a Mac: Enable “Zoom” in the “Accessibility” tools of the “System Preferences”.
    • On a PC: Use the “Magnifier” tool.

  • Enable anti-aliasing and change the point size while in a “zoomed” state to investigate the pixels at the boundaries of a point rendering.

  • Use the distance of a point from the camera to set its size. (Use the following code for the vertex shader.) Many variations on this idea are possible.

    // Transform the location of the vertex.
    gl_Position = u_Transform * vec4(a_Vertex, 1.0);
    
    // Set the size of a rendered point.
    gl_PointSize = u_Size - gl_Position.z;
    

    (Please note that the gl_Position.z value is the clipping-space z-component from a perspective projection which is a non-linear value. An accurate distance from the camera can be calculated using a camera-space transformation.)

Non-square Points

When points are rendered by a vertex shader, (and only for points), the vertex shader passes an additional variable to the fragment shader, gl_PointCoord, which is 2-component vector that gives the location of a fragment relative to it’s “point” square. The components are in the range 0.0 to 1.0, where [0.0,0.0] is the upper-left corner of the square, [1.0, 1.0] is the lower-right corner of the square, and [0.5,0.5] is the center of the square.

A fragment shader can use the gl_PointCoord values to determine how to manipulate a specific pixel. For example, it is straightforward to render a “point” as a circle instead of a square. The GLSL distance function can be used to calculate the distance from a fragment’s location, gl_PointCoord, to the center of the “point”, (0.5,0.5). If the distance is greater than 0.5, the fragment is not inside a circle centered at the point’s location. Note that the GLSL statement, discard, in a fragment shader prevents the graphics pipeline from performing any further processing of a fragment.

Experiment with size of the points in the following WebGL program and study the fragment shader.

Show: Code   Canvas   Run Info
../_static/12_points2/points2.html

Experiment with gl_PointCoord.

Please use a browser that supports "canvas"
Size = 2.0 1.0 100.0
Animate
Show: Process information    Warnings    Errors
Open this webgl program in a new tab or window

Experiments:

  • In the fragment shader, change the greater-than sign in the distance test to a less-than sign. (I.e., if (distance(center, gl_PointCoord) < 0.5).
    Predict the results and then verify that you predicted correctly.

  • Change the fragment shader to the single line:
    gl_FragColor = vec4(gl_PointCoord, 0.0, 1.0);
    Study the resulting output until it makes sense. (Note that the upper-left corner of each square is black, which means that gl_PointCoord must be (0,0) for the upper-left pixel.)

  • Change the fragment shader to this logic:

    float d = distance(center, gl_PointCoord);
    if (mod(floor(d*4.0), 2.0) == 0.0) {
      discard;
    }
    gl_FragColor = u_Color;
    

    Try various scalar values other than 4.0 and see what happens.

  • Change the fragment shader to this logic:

    float d = distance(center, gl_PointCoord);
    if (d >= 0.5) discard;
    float alpha = 1.0 - d*2.0; /* center has the largest alpha */
    gl_FragColor = vec4(u_Color.rgb, alpha);
    

    (Note that the alpha component of the fragment’s color is changing from 1.0 at the center of the circle to 0.0 at the edges.)

  • Change the fragment shader to this logic:

    float d = distance(center, gl_PointCoord);
    if (gl_PointCoord.t > 0.5 || d >= 0.5) discard;
    d = 1.0 - d*2.0;
    gl_FragColor = vec4(u_Color.rgb, d);
    

    (Note that the bottom half of the circle has been discarded.)

  • Come up with your own experiments. Your goal is to understand how to use gl_PointCoord to manipulate the shape and color of a “point” rendering.

Billboard (or Sprite)

A billboard in computer graphics is a texture mapped quad (four sided polygon) that always faces the camera, regardless of its location and 3D orientation. A sprite is a 2D image that can be rendered into a scene. The common practice in WebGL is to render billboards and sprites as “points” with an appropriate gl_PointSize and texture map image. Practical uses of these techniques are presented in the lesson on “particle systems.”

To texture map a “point” there are two basic scenarios:

  1. The entire texture map image is mapped to the point’s square. The implementation is straightforward – use gl_PointCoord as the texture coordinates for a texture2D lookup.

  2. A point has an associated texture coordinate that defines a location in a texture map image for the center pixel of a point rendering. Adjacent pixels of the texture map image are used to color adjacent pixels in the point’s square. This requires the calculation of fractional offsets within a texture map image to get to adjacent pixels and the conversion of gl_PointCoord into pixel offsets.

The following two WebGL programs use this tileable image, texture_image, as a texture map.

1) Map Entire Image to a “Point”

Show: Code   Canvas   Run Info
../_static/12_points3/points3.html

Experiment with billboards.

Please use a browser that supports "canvas"
Size = 30.0 1.0 100.0
Translate X = 30.0 -2.0 2.0
Animate
Show: Process information    Warnings    Errors
Open this webgl program in a new tab or window

If you look closely, the texture map image is flipped on the vertical axis. Why? Because the gl_PointCoord values use the upper-left corner for (0,0), while texture coordinates have their origin in the bottom-left of an image. The orientation of the texture map can be flipped by inverting the .t component like this:

vec2 coords = vec2(gl_PointCoord.s, 1.0 - gl_PointCoord.t);
gl_FragColor = texture2D(u_Texture_unit, coords);

Modify the fragment shader above to verify that this flips the texture map image to its original orientation.

Clipping Points

The clipping of points is not consistent across all browsers.

On Macintosh systems, clipping is based on the single (x,y,z) location of the point. This can cause the rendering of a point to “pop-in” and “pop-out” of a scene. If a billboard or sprite needs to be partially clipped against a viewing volume, it must be rendered using triangles.

On Windows and Linux systems, clipping is based on the entire square rendering of a point and any portion of a point’s square that is visible is rendered, even if the point itself is outside the clipping volume.

2) Map Adjacent Pixels to a “Point”

Texture coordinates are fractional percentages in the range 0.0 to 1.0. The fraction required to increment from one pixel to an adjacent pixel in a texture map image is based on the image’s dimensions. These increments can be calculated once and passed to a fragment shader:

texture_delta = [ (1.0 / (texture_map_image.width - 1.0)),
                  (1.0 / (texture_map_image.height - 1.0))];
gl.uniform2fv(program.u_Texture_delta, texture_delta);

(This technique was previously described in Lesson 11.10 on bump maps.)

To use the gl_PointCoord values to determine the relative position of a fragment compared to the center pixel of the point’s square, a fractional percentage is required, but based on the dimensions of the point’s square. A point rendering is always square, so a single fractional percentage is needed: 1.0/(point_size-1.0). Therefore, take gl_PointCoord and move it to the center of the square, divide it by the fractional percentage, and convert to an integer. This provides a relative integer offset from the center pixel. The correct adjacent pixel can now be retrieved from the texture map.

Please experiment with the following WebGL program and study the fragment shader. Notice that when the size of a point increases, extra pixels from the texture map are used to fill the new area. This is different from the previous WebGL program where the entire texture map was used for the point’s square.

Show: Code   Canvas   Run Info
../_static/12_points4/points4.html

Experiment with billboards.

Please use a browser that supports "canvas"
Size = 30.0 1.0 100.0
Animate
Show: Process information    Warnings    Errors
Open this webgl program in a new tab or window

Summary

The previous two WebGL programs demonstrate billboards and sprites but not how they are typically used in practice. Rarely are billboards and sprites rendered as pure squares. By manipulating the alpha color value of individual pixels, a point’s square can become transparent in specific locations to render any shape desirable. Please try to combine some of the techniques described in the “Non-square Points” section with the billboards programs to investigate what is possible.

Glossary

point
A single location in 3D space.
gl_PointSize
A predefined output variable of a vertex shader when rendering points. It is the side length of a square, in pixels, that is centered about the location of a 3D point.
gl_PointCoord
A 2D vector where each component is a percentage value between 0.0 and 1.0. It gives the relative location of a fragment compared to the square that composes the entire point rendering. [0,0] is the upper-left corner.
billboard
A rendering of a texture map onto a quad surface that is always facing the camera.
sprite
The rendering of a 2D image into a 3D scene, where the image is always directly facing the camera.

Self Assessment

    Q-507: The default rendering of a point (when gl_PointSize is not set) colors how many pixels?

  • 1
  • Correct.
  • 2
  • Incorrect.
  • 4
  • Incorrect.
  • 8
  • Incorrect.

    Q-508: Anti-aliasing is a technique for …

  • partially coloring adjacent pixels to the edge of a graphic primitive if an edge pixel is only partially covered by the edge.
  • Correct.
  • correcting aliasing, which is sampling error.
  • (Kind of correct, but really meaningless jargon.)
  • correcting errors.
  • (What kind of errors? Too vague!)
  • fixing the color of pixels at the center of a graphic primitive.
  • Incorrect. Anti-aliasing works at the edges of a graphics primitive, not its center.

    Q-509: The vertex shader output variable, gl_PointSize, is a measure of what?

  • The dimensions of a square, in pixels.
  • Correct.
  • The number of pixels to color when rendering a point, in pixels.
  • Incorrect.
  • The dimensions of a square, in world units.
  • Incorrect. gl_PointSize is not in world units.
  • The radius of a circle centered at a 3D location, in pixels.
  • Incorrect.

    Q-510: The fragment shader input variable, gl_PointCoord, is a vec2 whose components are in what range?

  • [0.0, 1.0]
  • Correct. They are percentages that give the location of a fragment within it’s point rendering.
  • [0.0, gl_PointSize]
  • Incorrect.
  • [-1.0, 1.0]
  • Incorrect.
  • [-gl_PointSize, gl_PointSize]
  • Incorrect.

    Q-511: What is the value of the fragment shader input variable, gl_PointCoord, for the pixel that is at the center of a point’s rendering? (I.e., the exact location of a point?)

  • [0.5, 0.5]
  • Correct. This is the center of the point’s square.
  • [0.0, 0.0]
  • Incorrect. No, this is the upper-left corner pixel.
  • [1.0, 1.0]
  • Incorrect. No, this is the lower-right corner pixel.
  • [0.75, 0.25]
  • Incorrect. No, this pixel is to the right of center, and up from the center, of the point’s location.

    Q-512: What happens to the rendering of a point if the 3D location of the point is not inside the projection’s clipping volume?

  • None of the point’s square is rendered.
  • Correct. A point’s square is rendered or not rendered based on whether a single 3D location is inside the projection’s clipping volume.
  • The portion of the point’s square that is visible from the camera is rendered.
  • Incorrect.
  • The location of the point is modified in 3D space to guarantee its entire rendering.
  • Incorrect. The graphics pipeline never modifies geometric primitive data.
  • The alpha component of the point’s color is modified to make the point fade out.
  • Incorrect. The graphics pipeline never modifies geometric primitive data.

    Q-513: Given the following options, which is simplest for a fragment shader to calculate?

  • Map an entire texture map to a point’s square.
  • Correct. The fragment shader is a single line of code.
  • Map a small sub-area of a texture map to a point’s square.
  • Incorrect. Calculations must be performed to get offsets to adjacent pixels.
  • Render a point as a circle and texture map it.
  • Incorrect. Requires at least two lines of fragment shader code.
  • Render a point as a “bulls eye”.
  • Incorrect. Requires multiple lines of fragment shader code.
Next Section - 12.8 - Particle Systems