12.3 - Selecting Objects

Interactive applications and games allow users to select objects from a 3D rendered scene. There are two commonly used algorithms:

  1. Cast a ray starting at the camera, (0,0,0), going into the scene through the location of the user mouse click. Then intersect the ray with every triangle in the scene and remember the closest one. The model associated with the selected triangle is the user selection.

  2. Render the scene twice. On the first rendering, use a unique color with no lighting calculations to render each object in the scene. Read the color of the pixel at the location of the user mouse click. The unique color identifies the selected object. Then immediately clear the draw buffers and re-render the scene normally. The user never sees the first rendering, only the second one.

The second algorithm is easier to implement and is explained in detail in this lesson.

A “Selection” Algorithm

The following assumptions are made about a scene:

  • The scene is composed of multiple models which can be accessed from an array.
  • A unique identifier for each model is its array index.

The scene is rendered twice. The shader program used for the first rendering puts a unique identifier into every pixel rendered for each unique model. Let’s call this shader program selection_program. The second rendering creates the image seen by the user. Let’s call this shader program visible_program. Here are the steps to the selection algorithm, assuming that the user has clicked on the canvas location (mouse_x, mouse_y).

  1. gl.useProgram(selection_program)
  2. Clear the color buffer and the depth buffer.
  3. Render the scene.
  4. Read the color of the pixel at location (mouse_x,mouse_y) from the color buffer.
  5. Use the color to identify the selected object.
  6. gl.useProgram(visible_program)
  7. Clear the color buffer and the depth buffer.
  8. Render the scene.
  9. The contents of the color buffer becomes visible in the canvas window because the JavaScript code terminates its execution. (See the discussion on double-buffering in lesson 8.1.)

There are several technical issues to discuss before looking at a complete implementation of this algorithm.

  • How can values be retrieved from the color buffer after a rendering is finished?
  • How can a unique identifier be converted into a color value suitable for use in a fragment shader?
  • How can a color value retrieved from a color buffer be converted into a unique identifier?
  • How can a model be rendered using two different shader programs?

Reading Pixels From the Color Buffer

After a rendering operation, you can access the resulting color buffer using the gl.readPixels() function. This allows you to read a rectangular section of the image into a 1D array of color component values. WebGL 1.0 only supports one image format: 8 bits per component with 4 components per pixel (RGBA). To retrieve a rectangular section of the color buffer that has dimensions width by height starting at the lower-left corner (x,y), you would use this command:

pixels = new Uint8Array(width*height*4); // RGBA values for each pixel
gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);

The pixel data is stored in row-major order in the 1D array.

The selection algorithm only needs the color of one pixel from the color buffer. Therefore, the command is,

pixel_color = new Uint8Array(4); // A single RGBA value
gl.readPixels(mouse_x, mouse_y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel_color);

However, the coordinate system for the canvas window and the color buffer are not the same. The canvas window has its Y axis starting at the top-left corner and going down the screen. The color buffer’s image coordinate system has its Y axis starting at the bottom-left corner and going up the image. The value of mouse_y can be converted to the color buffer’s coordinate system by subtracting it from the buffer’s height:

mouse_y = buffer_height - mouse_y;

Note that pixels is an array of UNSIGNED_BYTEs, not floats. Each component is an 8-bit unsigned integer in the range 0 to 255.

Converting an Integer Identifier to a Color

Caveat

From the previous discussion we know that a WebGL 1.0 color buffer always uses 32 bits (8 bits per component) to represent a color. However, this might not be true for future WebGL versions. The following discussion assumes that the number of bits per component value can vary.

Setting a color in the color buffer requires four floating point values in the range [0.0, 1.0] like this:

gl_FragColor = vec4(red, green, blue, alpha);

The color component values are represented as floating point percentages to make their values hardware independent. However, at the hardware level, the color component values are stored as integers to a precision defined by the underlying hardware. The number of bits used for individual component values in a color buffer can be queried using these JavaScript calls:

red_bits   = gl.getParameter(gl.RED_BITS);
green_bits = gl.getParameter(gl.GREEN_BITS);
blue_bits  = gl.getParameter(gl.BLUE_BITS);
alpha_bits = gl.getParameter(gl.ALPHA_BITS);
total_bits = red_bits + green_bits + blue_bits + alpha_bits;

../_images/bit_arrangement.png

The diagram to the right shows a conceptual representation of a color value in bit format. If an integer identifier is conceptualized in the same format then the integer can be divided into four parts and each part converted to a floating point percentage. Dividing the integer into four parts can be efficiently performed using bit-wise shift, >>, and and, & operations, as shown in the following code:

let red_max   = Math.pow(2,red_bits) - 1;
let green_max = Math.pow(2,green_bits) - 1;
let blue_max  = Math.pow(2,blue_bits) - 1;
let alpha_max = Math.pow(2,alpha_bits) - 1;

let red_shift   = green_bits + blue_bits + alpha_bits;
let green_shift = blue_bits + alpha_bits;
let blue_shift  = alpha_bits;

/** ---------------------------------------------------------------------
 * Given an integer identifier, convert it to an RGBA color value.
 * @param id {number} an integer identifier
 * @returns {Float32Array} A array containing four floats.
 */
function createColor(id) {
  var red, green, blue, alpha;

  red   = ((id >> red_shift)   & red_max)   / red_max;
  green = ((id >> green_shift) & green_max) / green_max;
  blue  = ((id >> blue_shift)  & blue_max)  / blue_max;
  alpha = ((id                 & alpha_max) / alpha_max;

  return new Float32Array([ red, green, blue, alpha ]);
};

Converting a Color to an Integer Identifier

When a color is retrieved from the color buffer using gl.readPixels(), the value is an array of four integers. To convert the four integers into a single integer identifier, the values are shifted and added like this:

let red_max   = Math.pow(2,red_bits) - 1;
let green_max = Math.pow(2,green_bits) - 1;
let blue_max  = Math.pow(2,blue_bits) - 1;
let alpha_max = Math.pow(2,alpha_bits) - 1;

let red_shift   = green_bits + blue_bits + alpha_bits;
let green_shift = blue_bits + alpha_bits;
let blue_shift  = alpha_bits;

/** ---------------------------------------------------------------------
 * Given a RGBA color value from a color buffer, calculate and return
 * a single integer.
 * @param red   {number} component in the range [0,red_max  ]
 * @param green {number} component in the range [0,green_max]
 * @param blue  {number} component in the range [0,blue_max ]
 * @param alpha {number} component in the range [0,alpha_max]
 * @returns {number} An integer identifier.
 */
function getID(red, green, blue, alpha) {
  // Shift each component to its bit position in the final integer
  return ( (red   << red_shift)
         + (green << green_shift)
         + (blue  << blue_shift)
         + alpha );
};

A JavaScript Conversion Class

The two functions, createColor() and getID() can be combined into a single JavaScript class to avoid recalculating the configuration values each time they are needed. In addition, the continual creation of new objects, such as creating a new color object for each call to createColor(), should be avoided to minimize garbage collection. Please study the details of the following JavaScript class.

/** =======================================================================
 * @param gl {WebGLRenderingContext}
 * @constructor
 */
window.ColorToID = function (gl) {

  let self = this;

  let red_bits   = gl.getParameter(gl.RED_BITS);
  let green_bits = gl.getParameter(gl.GREEN_BITS);
  let blue_bits  = gl.getParameter(gl.BLUE_BITS);
  let alpha_bits = gl.getParameter(gl.ALPHA_BITS);
  let total_bits = red_bits + green_bits + blue_bits + alpha_bits;

  let red_max   = Math.pow(2,red_bits) - 1;
  let green_max = Math.pow(2,green_bits) - 1;
  let blue_max  = Math.pow(2,blue_bits) - 1;
  let alpha_max = Math.pow(2,alpha_bits) - 1;

  let red_shift   = green_bits + blue_bits + alpha_bits;
  let green_shift = blue_bits + alpha_bits;
  let blue_shift  = alpha_bits;

  let color = new Float32Array(4);

  /** ---------------------------------------------------------------------
   * Given an integer identifier, convert it to an RGBA color value.
   * @param id {number} an integer identifier
   * @returns {Float32Array} A array containing four floats.
   */
  self.createColor = function (id) {

    color[0] = ((id >> red_shift)   & red_max)   / red_max;
    color[1] = ((id >> green_shift) & green_max) / green_max;
    color[2] = ((id >> blue_shift)  & blue_max)  / blue_max;
    color[3] = ( id                 & alpha_max) / alpha_max;

    return color;
  };

  /** ---------------------------------------------------------------------
   * Given a RGBA color value from a color buffer, calculate and return
   * a single integer.
   * @param red   {number} component in the range [0,red_max  ]
   * @param green {number} component in the range [0,green_max]
   * @param blue  {number} component in the range [0,blue_max ]
   * @param alpha {number} component in the range [0,alpha_max]
   * @returns {number} An integer identifier.
   */
  self.getID = function (red, green, blue, alpha) {
    // Shift each component to its bit position in the final integer
    return ( (red   << red_shift)
           + (green << green_shift)
           + (blue  << blue_shift)
           + alpha );
  };

};

Rendering a Model with Different Shader Programs

For the WebGL program below, the scene is created by rendering one model of a cube at different positions, different scales, and different colors. The cube model is defined by a single set of vertex object buffers that are created by a call to ModelArraysGPU(). That is:

gpuModel = new ModelArraysGPU(gl, models["cube2"], out);

Two separate shader programs are created using these statements:

select_program = download.createProgram(gl,
                   vshaders_dictionary["uniform_color"],
                   fshaders_dictionary["uniform_color"]);
visible_program = download.createProgram(gl,
                    vshaders_dictionary["uniform_color_with_lighting"],
                    fshaders_dictionary["uniform_color_with_lighting"]);

The select_program sets every fragment that composes a rendered model to the same color. For selection, this is a color that represents a unique identifier. The visible_program performs lighting calculations when it renders a model and therefore every fragment is potentially a different color.

Two separate classes use these different shader programs to render the cube model. These are created with these calls:

select_cube = new RenderUniformColor(gl, select_program, gpuModel, download.out);
cube = new RenderUniformColorWithLighting(gl, visible_program, gpuModel, download.out);

During rendering, the appropriate object is used to render a cube. The rendering function’s parameter, select_mode, is used to pick the correct rendering function. (Note that JavaScript allows for default parameter values. Since the parameter select_mode is set to false in the function heading, a call to render() is equivalent to render(false)).

Error Warning

GLSL variables of type uniform can only be initialized for the active shader program. Calling any version of gl.uniform[1234]f[v] for a variable that is not part of the active shader program will generate WebGL errors.

Experiments

Experiment with the following WebGL program and then modify the code as described below. (Use right button mouse clicks to select a model.)

Show: Code   Canvas   Run Info
../_static/12_select/select.html

Experiment with object selection.

  • Right-click to select an object. The currently selected object is rendered in red.
  • Left-click and drag rotates your view.
Please use a browser that supports "canvas"
Animate
Show: Process information    Warnings    Errors
Open this webgl program in a new tab or window

Experiments:

  • Study the select function in lines 142-161. Note that it calls the render() function twice.
  • Don’t render the scene twice on selection by commenting out lines 161. (Disable animation to keep the “selection rendering” visible on the canvas.) The “color” rendered for each cube was created from the cube’s integer identifier (see line 126).
  • Lines 157-158 display the color of a “clicked-on” pixel and the identifier value it represents. Try to select various cubes and note the output to the console window (or the “Run Info” window below the canvas). Note that a color of (249,249,249,255), which translates to an identifier of -101058049, is the color of the background and therefore no cube is selected. The background color should be a value that does not translate into a valid model identifier.
  • Note that rendering the scene twice for selection has no effect on the frame rate of the animation. What happens if you increase the number of cubes’s from 30 to 300 in line 54? How about 3000? How about 30000? (Looks kind of like a Borg spaceship from Star Trek – don’t you think?)

Glossary

selection algorithm
An algorithm for selecting one model (or geometric primitive) from a large collection of models using a user’s mouse click.
gl.readPixels()
A function in the WebGL API that allows a JavaScript program to access the output image of a rendering.
bitwise shift operators: << and >>
JavaScript operators that shift the bits in a binary value left or right. For example, 00101101 >> 2 results in 00001011.
bitwise logical-and operator: &
A JavaScript operator that performs a logical “and” operation on a bit-by-bit basis. For example, 01001101 & 15 results in 00001101 because 15 in binary is 00001111.

Self Assessment

    Q-406: What WebGL command will retrieve values from a color buffer?

  • gl.readPixels()
  • Correct.
  • gl.readColorBuffer()
  • Incorrect. There is no such function.
  • gl.getPixelColor()
  • Incorrect. There is no such function.
  • gl.atPixel()
  • Incorrect. There is no such function.

    Q-407: When a color is read from a WebGL color buffer, what type of data is returned?

  • Four unsigned bytes in the range 0 to 255.
  • Correct.
  • Four floating point values in the range 0.0 to 1.0.
  • Incorrect. This is how colors are specified in a fragment shader, but not how they are actually stored in the color buffer.
  • One unsigned integer in the range 0 to 232-1
  • Incorrect.
  • Three floating point values, RGB, in the range 0.0 to “max_color”.
  • Incorrect.

    Q-408: The coordinate system used for a canvas is different from the coordinate system used for a color buffer. Which of the following is correct?

  • canvas: Y axis down, origin in upper-left corner
            
    color buffer: Y axis up, origin in lower-left corner
  • Correct.
  • canvas: Y axis up, origin in lower-left corner
            
    color buffer: Y axis down, origin in upper-left corner
  • Incorrect.
  • canvas: Y axis up, origin in center
            
    color buffer: Y axis up, origin in lower-left corner
  • Incorrect.
  • canvas: Y axis down, origin in center
            
    color buffer: Y axis up, origin in center
  • Incorrect.

    Q-409: If a has the value of 6, what is a << 2. (Six in binary is 110.)

  • 11000 (24)
  • Correct. It is a left shift by 2 bits.
  • 00110 (6)
  • Incorrect.
  • 1100 (12)
  • Incorrect.
  • 11011 (27)
  • Incorrect.

    Q-410: If a has the value of 187, what is a >> 5. (187 in binary is 10111011.)

  • 101 (5)
  • Correct. It is a right shift by 5 bits.
  • 11011 (27)
  • Incorrect.
  • 10111 (23)
  • Incorrect.
  • 1110 (14)
  • Incorrect.

    Q-411: Why are two separate shader programs needed to accomplish a selection? (Select all that apply.)

  • The shader program that puts a unique identifier into the color buffer must not perform lighting calculations because the lighting calculations would change the ID.
  • Correct.
  • The shader program that creates the visible rendering should create a realistic scene.
  • Correct.
  • The shader program that puts a unique identifier into the color buffer must ignore the color properties of the model.
  • Correct.
  • The shader program that puts a unique identifier into the color buffer must deal with light attenuation.
  • Incorrect.
Next Section - 12.4 - Transparency