12.9 - Stencils

A WebGL draw buffer is composed of three buffers: 1) a color buffer, 2) a depth buffer, and 3) an optional stencil buffer. This lesson describes how stencil buffers can be manipulated by the graphics pipeline and demonstrates their use in two example WebGL programs.

This lesson is longer than most. Sorry! Understanding stencil buffers will take some time and require attention to detail. Therefore, please take your time and study carefully.

The Big Idea

A stencil buffer provides fine-grain control over which pixels are processed by the graphics pipeline after they have been processed by a fragment shader. Stencil buffer functionality is hard-coded into the graphics pipeline and is controlled by a programmer through stencil buffer parameters. Understanding the parameter settings is non-trivial, so they will be explained in the simplest possible terms and then details will be added to provide the “total picture.”

There are two fundamental ideas:

  • Using a “stencil test” to determine if a fragment should be allowed to modify the color buffer.
  • Setting the stencil buffer values to create a “stencil mask.”

Before discussing these two issues, let’s discuss the structure and nature of a stencil buffer.

The Stencil Buffer

A stencil buffer must be created when a WebGL context is created for a canvas. It is a 2D array of unsigned bytes that must have the same dimensions as the draw buffer’s color buffer and depth buffer. Here is an example of creating a WebGLRenderingContext that contains a stencil buffer:

canvas = document.getElementById(canvas_id);
context = canvas.getContext('webgl', {"stencil" : true} );

A true or false value can be represented using a single bit. Typically 0 means false and 1 means true. Since a stencil buffer stores an unsigned byte for each pixel, it can potentially represent eight different “stencils” at any given time. WebGL allows a programmer to specify a “mask” that determines which bits are tested when a stencil test is performed. When a value from a stencil buffer is retrieved, a bit-wise AND operation is always performed with a “mask” before it is used. A “mask” is typically specified in hexadecimal notation by starting the value with 0x. The following table shows the eight masks that can be used to differentiate the eight bits in an unsigned byte:

binary 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000
mask 0x01 0x02 0x04 0x08 0x10 0x20 0x40 0x80

If each stencil buffer value is treated as a single true/false value, the “mask” can be set to 0xFF (all ones). Other division of the bits are possible. For example, two stencils could be created, where the high order four bits (using a mask of 0xF0) represent one stencil and the low order fours bits (using a mask of 0x0F) represents the other stencil. You can conceptualize the stencil buffer as a single “mask”, as eight separate “masks”, or any number in-between.

The Stencil Test

When the stencil test is enabled, pixels are allowed to modify the color buffer and the depth buffer only if they pass a “stencil test.” The following pseudocode describes the internal logic of the graphics pipeline and demonstrates that the stencil test happens before the depth test (assuming that the “depth test” has been enabled by gl.enable(gl.DEPTH_TEST)).

No Stenciling gl.disable(gl.STENCIL_TEST) With Stenciling gl.enable(gl.STENCIL_TEST)
void clearBuffers() {
  for (x = 0; x < image_width; x++) {
    for (y = 0; y < image_height; y++) {
      depth_buffer[x][y] = maximum_z_value;
      color_buffer[x][y] = background_color;
     }
  }
}

void renderPixel(x, y, z, color) {
  if (z < depth_buffer[x][y]) {
    depth_buffer[x][y] = z;
    color_buffer[x][y] = color;
  }
}
void clearBuffers() {
  for (x = 0; x < image_width; x++) {
    for (y = 0; y < image_height; y++) {
      depth_buffer[x][y]   = maximum_z_value;
      color_buffer[x][y]   = background_color;
      stencil_buffer[x][y] = stencil_value;
    }
  }
}

void renderPixel(x, y, z, color) {
  if (passes_stencil_test(x, y)) {
    if (z < depth_buffer[x][y]) {
      depth_buffer[x][y] = z;
      color_buffer[x][y] = color;
    }
  }
}

The “stencil test” is a comparison between a “reference value” and the current value stored in the stencil buffer at a pixel’s (x,y) location. A programmer does not implement this logic, but rather specifies the type of comparison, the reference value, and a mask using a call to gl.stencilFunc( enum func, int ref, uint mask ). The following pseudocode demonstrates the functionality.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// -------------------------------------------------------------
// STENCIL TEST - does a pixel pass the stencil test?
int stencil_func = ALWAYS;  // DEFAULT
int reference    = 0;       // DEFAULT
int bit_mask     = 0xFF;    // DEFAULT - all bits are ones

// Sets the STENCIL TEST parameters:
void gl.stencilFunc( enum func, int ref, uint mask ) {
  stencil_func = func;
  reference    = ref;
  bit_mask     = mask;
}

// Performs the STENCIL TEST:
boolean passes_stencil_test(x, y) {
  condition = TRUE;
  if (STENCIL_TEST_IS_ENABLED) {

    stencil_value   = stencil_buffer[x][y] & bit_mask;  // bit-wise AND
    reference_value = reference            & bit_mask;  // bit-wise AND

    switch (stencil_func) {
      case NEVER:    condition = false;
      case ALWAYS:   condition = true;  // DEFAULT
      case LESS:     condition = (reference_value <  stencil_value);
      case LEQUAL:   condition = (reference_value <= stencil_value);
      case EQUAL:    condition = (reference_value == stencil_value);
      case GREATER:  condition = (reference_value >  stencil_value);
      case GEQUAL:   condition = (reference_value >= stencil_value);
      case NOTEQUAL: condition = (reference_value != stencil_value);
    }
  }
  return condition;
}

For example, gl.stencilFunc( gl.EQUAL, 2, 0x02 ) would configure the stencil test to be true for a pixel at (x,y), if the value at stencil_buffer[x][y] has its low order 2nd bit set to 1. Note that performing a bit-wise AND operation using a mask of 0x02 will produce either a value of 2 or 0.

For another example, gl.stencilFunc( gl.GREATER, 15, 0xF0 ) would configure the stencil test to be true for a pixel at (x,y), if the value at stencil_buffer[x][y] has any of its four high order bits set to one. Note that performing a bit-wise AND operation using a mask of 0xF0 will produce one of the following 16 values: 0, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240.

As the above pseudocode indicates, the stencil test can be configured to perform one of eight possible comparisons. In addition, it makes sure that only certain bits are used in the comparison.

FRONT vs. BACK Faces

By default, WebGL considers a triangle whose vertices are ordered counter-clockwise as “front-facing” and triangles whose vertices are ordered clockwise as “back-facing”. The graphics pipeline always passes a boolean input variable called gl_FrontFacing to a fragment shader. If gl_FrontFacing is true, the pipeline should render the “front” side of a triangle, otherwise the “back” side. (The gl_FrontFacing value can be used or ignored by a fragment shader.)

Note that OpenGL ES 2.0 allows stencil testing to be performed differently for front and back facing triangles, but WebGL does not. Therefore calls to gl.stencilFuncSeparate() should not be used in WebGL.

Creating a Stencil

Each pixel in a stencil buffer is assigned a value by clearing the buffer with a specific value and then rendering a scene. As with the “stencil test,” the work of defining a stencil is hard-coded into the graphics pipeline. The programmer’s responsibilities is to assign appropriate parameters to the stencil operation parameters before a rendering is performed.

Please study the following pseudocode which describes the internal workings of the graphics pipeline and shows when a stencil operation is performed and the data it uses. Please notice the following:

  • To simplify the pseudocode, tests to determine if the gl.STENCIL_TEST is enabled have been left out. However, stencil operations are only performed when the gl.STENCIL_TEST has been enabled.
  • The stencil buffer is updated once by each invocation of a fragment shader. Stencil operation parameters define a separate operation for fragments that failed the “stencil test,” or that passed the “stencil test” but either passed or failed the “depth test.”
  • Updating the stencil buffer is performed after the stencil test and the depth test have been completed. This takes some contemplation! A stencil buffer’s value is used, and then updated to possibly a different value! In typical usage this rarely happens. The “stencil test” parameters and the “stencil operation” parameters are typically configured to perform one or the other, but not both at the same time.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void renderPixel(x, y, z, color) {
  if (passes_stencil_test(x,y)) {
    if (z < z_buffer[x][y]) {
      depth_buffer[x][y] = z;
      color_buffer[x][y] = color;
      status = DEPTH_TEST_PASSED;
    } else {
      status = DEPTH_TEST_FAILED;
    }
  } else {
    status = STENCIL_TEST_FAILED;
  }
  update_stencil_buffer(x, y, status);
}

Updating the stencil buffer is described by the following pseudocode. Please study the pseudocode carefully. (Note that the gl.REPLACE operation uses the reference value set by gl.stencilFunc( func, reference, mask ). In addition, the mask limits the bits that can be modified in the stencil buffer’s value.

// -------------------------------------------------------------
// STENCIL OPERATION - sets a pixel's value in a stencil buffer.
int stencil_test_failed_func = KEEP;  // DEFAULT
int depth_test_failed_func   = KEEP;  // DEFAULT
int depth_test_passed_func   = KEEP;  // DEFAULT

int MAX_VALUE = 255; // for an 8-bit stencil buffer
int MIN_VALUE = 0;

void gl.stencilOp( enum sfail, enum dpfail, enum dppass ) {
  stencil_test_failed_func = sfail;
  depth_test_failed_func   = dpfail;
  depth_test_passed_func   = dppass;
}

void update_stencil_buffer(x, y, status) {
  if (STENCIL_TEST_IS_ENABLED) {

    switch (status) {
      case DEPTH_TEST_PASSED:   stencil_func = depth_test_passed_func;
      case DEPTH_TEST_FAILED:   stencil_func = depth_test_failed_func;
      case STENCIL_TEST_FAILED: stencil_func = stencil_test_failed_func;
    }

    stencil_value   = stencil_buffer[x][y] & bit_mask;  // bit-wise AND
    reference_value = reference            & bit_mask;  // bit-wise AND

    switch (stencil_func) {
      case KEEP:      value = stencil_value;
      case ZERO:      value = 0;
      case REPLACE:   value = reference_value;
      case INCR:      value = stencil_value + 1;
                      if (value > MAX_VALUE) value = MAX_VALUE;
      case DECR:      value = stencil_value - 1;
                      if (value < MIN_VALUE) value = MIN_VALUE;
      case INVERT:    value = ~ stencil_value;
      case INCR_WRAP: value = stencil_value + 1;
                      if (value > MAX_VALUE) value = MIN_VALUE;
      case DECR_WRAP: value = stencil_value - 1;
                      if (value < MIN_VALUE) value = MAX_VALUE;
    }
    stencil_buffer[x][y] = (value & bit_mask) |
                           (stencil_buffer[x][y] & ~bit_mask);
  }
}

FRONT vs. BACK Faces

Stencil operations that change the values in a stencil buffer are more complex than described above because they can be set to process front-facing and back-facing triangles differently using the function StencilOpSeparate( enum face, enum sfail, enum dpfail, enum dppass ). Passing gl.FRONT for the face parameter sets the stencil operations for front-facing triangles, while passing gl.BACK sets the stencil operations for back-facing triangles. The following pseudocode describes the full range of stencil operations. The variables prefixed with back_ are for processing back-facing triangles.

// -------------------------------------------------------------
// STENCIL OPERATION - sets a pixel's value in a stencil buffer.
int stencil_test_failed_func = KEEP;       // DEFAULT
int depth_test_failed_func   = KEEP;       // DEFAULT
int depth_test_passed_func   = KEEP;       // DEFAULT

int back_stencil_test_failed_func = KEEP;  // DEFAULT
int back_depth_test_failed_func   = KEEP;  // DEFAULT
int back_depth_test_passed_func   = KEEP;  // DEFAULT

int MAX_VALUE = 255; // for an 8-bit stencil buffer
int MIN_VALUE = 0;

void gl.stencilOp( enum sfail, enum dpfail, enum dppass ) {
  stencil_test_failed_func = sfail;
  depth_test_failed_func   = dpfail;
  depth_test_passed_func   = dppass;
}

void gl.stencilOpSeparate( enum face, enum sfail, enum dpfail, enum dppass ) {
  if (face == gl.FRONT || face == gl.FRONT_AND_BACK) {
    stencil_test_failed_func = sfail;
    depth_test_failed_func   = dpfail;
    depth_test_passed_func   = dppass;
  }

  if (face == gl.BACK || face == gl.FRONT_AND_BACK) {
    back_stencil_test_failed_func = sfail;
    back_depth_test_failed_func   = dpfail;
    back_depth_test_passed_func   = dppass;
  }
}

void update_stencil_buffer(x, y, status, gl_FrontFacing) {
  if (STENCIL_TEST_IS_ENABLED) {

    if (gl_FrontFacing) {
      switch (status) {
        case DEPTH_TEST_PASSED:   stencil_func = depth_test_passed_func;
        case DEPTH_TEST_FAILED:   stencil_func = depth_test_failed_func;
        case STENCIL_TEST_FAILED: stencil_func = stencil_test_failed_func;
      }
    } else { // ! gl_FrontFacing --> back-facing
      switch (status) {
        case DEPTH_TEST_PASSED:   stencil_func = back_depth_test_passed_func;
        case DEPTH_TEST_FAILED:   stencil_func = back_depth_test_failed_func;
        case STENCIL_TEST_FAILED: stencil_func = back_stencil_test_failed_func;
      }
    }

    stencil_value   = stencil_buffer[x][y] & bit_mask;  // bit-wise AND
    reference_value = reference            & bit_mask;  // bit-wise AND

    switch (stencil_func) {
      case KEEP:      value = stencil_value;
      case ZERO:      value = 0;
      case REPLACE:   value = reference_value;
      case INCR:      value = stencil_value + 1;
                      if (value > MAX_VALUE) value = MAX_VALUE;
      case DECR:      value = stencil_value - 1;
                      if (value < MIN_VALUE) value = MIN_VALUE;
      case INVERT:    value = ~ stencil_value;
      case INCR_WRAP: value = stencil_value + 1;
                      if (value > MAX_VALUE) value = MIN_VALUE;
      case DECR_WRAP: value = stencil_value - 1;
                      if (value < MIN_VALUE) value = MAX_VALUE;
    }
    stencil_buffer[x][y] = (value & bit_mask) |
                           (stencil_buffer[x][y] & ~bit_mask);
  }
}

Caveat

The OpenGL ES 2.0 and WebGL 1.0 specifications do not specify whether the INCR, DECR, INCR_WRAP, and DECR_WRAP functionality is based on a MIN_VALUE and MAX_VALUE of an 8-bit unsigned integer, or limited by the minimum and maximum values based on the bit_mask.

Multi-pass Rendering

When using a stencil buffer, a rendering is typically performed by a series of “rendering passes.” The first rendering pass creates a desired stencil buffer while follow-on renderings use the stencil buffer to control which pixels in a color buffer are modified. WebGL provides fine-grain control of the draw buffers to allow a scene to be “rendered” but only change specific buffers. The gl.colorMask(bool red, bool green, bool blue, bool alpha) function determines whether individual components of a color buffer can be updated. The gl.depthMask(bool flag) function determines whether the depth buffer can be modified. And the stencilMask(unit mask) function determines which bits in a stencil buffer value can be modified.

Our final pseudocode to describe the internal logic of the graphics pipeline demonstrates this fine-grain control.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
boolean write_red   = TRUE;       // Default
boolean write_green = TRUE;       // Default
boolean write_blue  = TRUE;       // Default
boolean write_alpha = TRUE;       // Default
boolean write_depth = TRUE;       // Default
unsigned_int stencil_mask = 0xFF; // Default

void gl.colorMask(bool red, bool green, bool blue, bool alpha) {
  write_red   = red;
  write_green = green;
  write_blue  = blue;
  write_alpha = alpha;
}

void gl.depthMask(bool flag) {
  write_depth = flag;
}

void stencilMask(unit mask) {
  stencil_mask = mask;
}

void renderPixel(x, y, z, color) {
  if (passes_stencil_test(x,y)) {
    if (z < z_buffer[x][y]) {
      if (write_depth) depth_buffer[x][y] = z;
      if (write_red)   color_buffer[x][y].r = color.r;
      if (write_green) color_buffer[x][y].g = color.g;
      if (write_blue)  color_buffer[x][y].b = color.b;
      if (write_alpha) color_buffer[x][y].a = color.a;
      status = DEPTH_TEST_PASSED;
    } else {
      status = DEPTH_TEST_FAILED;
    }
  } else {
    status = STENCIL_TEST_FAILED;
  }
  update_stencil_buffer(x, y, status, stencil_mask);
}

When a stencil buffer value is modified, which bits are changed is controlled by the stencil_mask. Pseudocode that simulates this might look like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void update_stencil_buffer(x, y, status, gl_FrontFacing) {
  if (STENCIL_TEST_IS_ENABLED) {
    ...
    new_value = (value & bit_mask) | (stencil_buffer[x][y] & ~bit_mask);

    if (stencil_mask.bit[0] == 1) stencil_buffer[x][y].bit[0] = new_value.bit[0];
    if (stencil_mask.bit[1] == 1) stencil_buffer[x][y].bit[1] = new_value.bit[1];
    if (stencil_mask.bit[2] == 1) stencil_buffer[x][y].bit[2] = new_value.bit[2];
    if (stencil_mask.bit[3] == 1) stencil_buffer[x][y].bit[3] = new_value.bit[3];
    if (stencil_mask.bit[4] == 1) stencil_buffer[x][y].bit[4] = new_value.bit[4];
    if (stencil_mask.bit[5] == 1) stencil_buffer[x][y].bit[5] = new_value.bit[5];
    if (stencil_mask.bit[6] == 1) stencil_buffer[x][y].bit[6] = new_value.bit[6];
    if (stencil_mask.bit[7] == 1) stencil_buffer[x][y].bit[7] = new_value.bit[7];
  }
}

Using a Stencil Buffer

The following two WebGL programs provide examples of tasks that use a stencil buffer.

Example 1 - Model Outline

The stencil buffer can be used to mark the pixels that surround a model and then color those pixels to indicate that the model has been selected by a user (or “marked” for some other reason). The basic steps to render a border around a model are:

  1. Enable the stencil buffer, disable changes to the color buffer, render the model scaled to a slightly larger size, and “mark” each pixel that is rendered in the stencil buffer.
  2. Render the model at its normal size and “un-mark” each pixel that is rendered. (This leaves only the pixels surrounding the model as “marked.”)
  3. Render the model at its larger size and simply color each pixel that is “marked” in the stencil buffer.

Experiment with the following WebGL program and study the _renderSelected() function in the stencil_outline_scene.js code file.

Show: Code   Canvas   Run Info
../_static/12_stencil_outline/stencil_outline.html

Experiment with "Model Outlining" using the Stencil Buffer.

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

Note that the size of the border is in “world units”. Given a different scene with models of different sizes, the border size would need to be adjusted accordingly.

The following is a detailed description of the program and specifically the _renderSelected() function. The details are non-trivial and require studying.

Lines Description
245 A stencil buffer is created when the WebGLRenderingContext is created for the canvas.
145 Clear all three draw buffers, including the stencil buffer.
187-189 If a model has been selected by the user using a mouse right-click, the function _renderSelected() is called to render a border around the model.
95 gl.enable(gl.STENCIL_TEST) enables the stencil buffer functionality.
96 gl.colorMask(false, false, false, false) disables changes to the color buffer so that the renderings that create the stencil do not change the visible image.
97 gl.depthMask(false) disables changes to the depth buffer. Why? A larger model will be rendered to mark pixels in the stencil buffer and then the original sized model will be rendered to “unmark” the pixels in the interior of the masked region. The larger model will have pixels closer to the camera and depth testing will prevent the original model from being rendered. In addition, changing the depth buffer so that other models can be rendered correctly is not needed because the entire scene has already been rendered.
98 gl.stencilFunc(gl.ALWAYS, 1, 0xFF) makes the “stencil test” always true, (gl.ALWAYS), because this rendering pass is creating the stencil mask. The 1 parameter is the “reference value” to set pixels in the stencil buffer. The 0xFF mask allows all bits in the stencil buffer values to be changed.
99 gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE) sets a stencil buffer pixel every time the “depth test” is true. The pixels are set to 1 because of the “reference value” parameter in the previous function call. For every pixel that is colored by the rendering, its associated stencil buffer value will be set to 1.
100 gl.stencilMask(0xFF) allows the stencil buffer values to be changed. This is important because the first rendering pass is creating the mask in the stencil buffer.
102-108 Render the model at a slightly larger scale than its original size.
112 In preparation for the 2nd model rendering, gl.stencilFunc(gl.EQUAL, 1, 0xFF) makes the “stencil test” pass if the pixel’s stencil value is equal to 1. This makes sure that all pixels from the previous rendering are processed.
113 In preparation for the 2nd model rendering, gl.stencilOp(gl.KEEP, gl.DECR, gl.DECR) decrements a stencil buffer’s value whether the “depth test” passes or fails. This guarantees that all interior pixels are returned back to zero in the stencil buffer
115-120 Render the model again, but this time at its normal scale.
124 In preparation for the 3rd rendering pass, gl.colorMask(true, true, true, true) enables modifications to the color buffer to allow the border to be rendered.
125 The stencil test is set to only process pixels if their associated stencil buffer value is equal to 1. (gl.stencilFunc(gl.EQUAL, 1, 0xFF))
126 This rendering pass should not modify the stencil buffer. Therefore clear the stencil mask, gl.stencilMask(0x00).
128-133 On the 3rd and final rendering pass, render the model at its larger size and color every pixel that has a stencil buffer value of 1.
136-137 Restore the graphic pipeline’s state to normal rendering modes.

Now that you partially understand the program, experiment with it to verify your understanding.

Example 2 - Restricted Rendering

The following WebGL program simulates the reflection of a model in a flat plane. This is accomplished by rendering a “mirror” of the model and restricting the drawing to only those pixels that compose the flat plane. Restricting the drawing to the flat plane, which can take on various shapes based on the scene’s camera location, is done using the stencil buffer. The basic steps are:

  1. Render a model.
  2. Render a flat plane with the stencil test enabled. This remembers which pixels the flat plane covers.
  3. Render the model mirrored about the flat plane, but with the stencil test enabled. This prevents the mirrored version from being rendered outside the plane’s pixels.

Please experiment with the following WebGL program.

Show: Code   Canvas   Run Info
../_static/12_stencil_reflect/stencil_reflect.html

Example using the Stencil Buffer.

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

The following is a detailed, line-by-line description of the stencil_reflect_scene.js rendering function. The details are non-trivial and require studying.

Lines Description
93 Clear all three draw buffers, including the stencil buffer.
97 The stencil buffer is not needed to render the model, so disable it.
98 Writing to the depth buffer is turned on and off during the rendering. Make sure the depth buffer can be modified so that hidden surface removal is performed properly.
100-109 Render the model normally.
113-123 Render the plane and “mark” every pixel that it colors by setting its associated value in the stencil buffer.
113 gl.enable(gl.STENCIL_TEST) enables the “stencil test”.
114-116 gl.stencilFunc(gl.ALWAYS, 1, 0xFF) sets the “stencil test” parameters. gl.ALWAYS makes the stencil test always true. This basically disables the “stencil test”. Why? Because this rendering pass does not want to use the stencil buffer to control which pixels are rendered. It wants to set the pixels of the stencil buffer. The parameter 1 is the value that will be placed in the stencil buffer when it is changed. Control of changing the stencil buffer is done by the parameters to gl.stencilOp in line 117. The mask, 0xFF, allows all bits of each stencil buffer pixel value to be modified. For this example the mask could have been set to 0x01 because only the low order bit is actually used.
117-119 gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE) sets the “stencil update” parameters. The first two gl.KEEP parameters say “leave the stencil buffer alone when the “stencil test” fails or the “depth test” fails. The gl.REPLACE parameter says to “replace the stencil buffer value using the ‘reference value’” set in the gl.stencilFunc call – when the “depth test” passed and the color buffer is modified. (The “reference value” is used for both the “stencil test” and the “stencil updating” functionality, but it is only specified in the gl.stencilFunc call.)
120 gl.stencilMask(0xFF) allows all bits of each stencil buffer value to be modified. Since this example only uses the lower order bit, 0x01 would have worked as well. Why is this needed? When the gl.STENCIL_TEST is enabled, both “stencil testing” and “stencil updating” is enabled. But some rendering passes only want to set the stencil buffer, while other rendering passes only want to use the stencil buffer for a “stencil test.” When the stencil mask is set to 0xFF, the rendering pass can set the stencil buffer values. When the stencil mask is set to 0x00, the rendering pass can’t set values in the stencil buffer and is therefore only “stencil testing.”
121 gl.depthMask(false) disables modifications to the depth buffer – but the “depth test” still happens. Therefore, when the plane is rendered, it will only be rendered in pixels that are closer to the camera than the model that is already in the scene. This allows the plane to “wrap around” the model correctly using the “depth test”. But it prevents the depth buffer values from being updated so that at a later time the “mirrored” model can be rendered over the plane’s pixels.
126 For this specfic example, the reflection of the model should only be visible when the top of the plane is visible. Therefore, the “mirrored” model is only rendered when the top of the flat plane is visible. The angle of viewing rotation is an easy test. The direction of the normal vector for the plane could have also been used, but that test would require more calculations.
129-131 gl.stencilFunc(gl.EQUAL, 1, 0xFF) changes the “stencil test” to be: “Is the stencil buffer value equal to 1?”. This restricts rendering to only those pixels whose color was modified when the plane was rendered, since that is the rendering pass that set the values of the stencil buffer.
132 gl.stencilMask(0x00) disables modifications to the stencil buffer. The next rendering pass is going to use a “stencil test” but not modify the stencil buffer.
133 gl.depthMask(true) enables modifications to the depth buffer. The next rendering pass needs hidden surface removal and this allows the “mirrored” model to be rendered correctly.
135-136 When the “mirrored” model is rendered as a “reflection” the colors of the model should not be as bright as the original model. By lowering the lighting ambient intensities and by using a light color that has a lower intensity the reflected model appears darker. Modifying the light values could produce a wide range of “reflected” effects.

You could experiment with this WebGL program in many ways. Comment out some of the settings, or change some of the settings and investigate what happens. For example, comment out the if-statement that tests for viewing above the plane in line 126. What happens?

Summary

In summary, the stencil buffer can be used to control which pixels can possibly have their color modified. Using the stencil buffer typically requires multiple renderings. One rendering will set the stencil; another rendering will update the color buffer based on the stencil.

To create a stencil, make the “stencil test” always true, enable writing to the stencil buffer, and set how the stencil value is updated based on the status of the stencil and depth tests.

// Rendering pass to create a stencil.
gl.stencilFunc(gl.ALWAYS, reference_value, mask);
gl.stencilMask(0xFF);  // Stencil buffer can be changed
gl.stencilOp(stencil_failed_func, depth_test_failed_func, depth_test_passed_func);

To use a stencil, set an appropriate “stencil test”, disable writing to the stencil buffer:

// Rendering pass to use a stencil.
gl.stencilFunc(compare_func, reference_value, mask);
gl.stencilMask(0x00);  // Stencil buffer can't be changed

The stencil buffer has many possible uses, as you will discover as you investigate more advanced rendering techniques.

Glossary

stencil
An image or pattern that allows some areas of a surface to receive color while preventing other areas from receiving color. It is also referred to as a “stencil mask” because a facial “mask” covers parts of a face while allowing other parts to be visible.
color buffer
A 2D array of color values. Each color value is typically a RGBA color.
depth buffer
A 2D array of “distance from the camera” values. The dimensions of the array must be identical to the dimensions of its associated color buffer.
stencil buffer
A 2D array of “mask” values. The dimensions of the array must be identical to the dimensions of its associated color buffer.
stencil test
Logic that produces a true or false result. If true, a fragment is allowed further processing by the graphics pipeline. If false, the fragment is discarded from the graphics pipeline.
stencil operation
Logic that sets the value of a single pixel in a stencil buffer.
bit-mask
An integer number with specific bits set to one. Performing a bit-wise logical AND operation between a number, n, and a bit-mask guarantees that all bits in n where there is a 0 in the bit-mask there is a 0 in the result.

Self Assessment

    Q-433: What is the size of a stencil buffer?

  • it is a 2D array with the same dimensions as the color buffer and the depth buffer.
  • Correct.
  • it is always a 2D array that is 1024 by 512.
  • Incorrect.
  • it is a 2D array with the same dimensions as its associated canvas.
  • Incorrect. (It is typically the same dimensions as the canvas, but it is not required to be.)
  • it is a 1D array of pixels.
  • Incorrect. (Not even close!)

    Q-434: What type of data is each element of a stencil buffer?

  • unsigned byte
  • Correct. It allows 8 bits to be used in any combination desired.
  • integer
  • Incorrect.
  • RGBA color
  • Incorrect.
  • floating point number
  • Incorrect.

    Q-435: A stencil buffer is desired that defines two separate “stencils”. The low-order bit of each element in the stencil buffer will be used to define one “stencil”, while the high-order bit will be used to define the second one. What two values should be used for the mask parameter of the gl.stencilFunc function to keep the stencils separate?

  • 0x01 and 0x80.
  • Correct.
  • 0xFF and 0x01.
  • Incorrect. 0xFF enables all 8 bits in the stencil values.
  • 0x02 and 0x04.
  • Incorrect. This uses the 2nd and 3rd low-order bits.
  • 0x0F and 0xF0
  • Incorrect. This uses 4 bits for each mask – instead of one bit.

    Q-436: How many rendering passes does it typically take to use a stencil buffer to control which color buffer pixels can be modified?

  • two
  • Correct. One rendering pass to set the stencil buffer values, and a second rendering pass to use the stencil buffer to control writing to the color buffer.
  • one
  • Incorrect.
  • none
  • Incorrect.
  • four
  • Incorrect. Certainly four passes might be used, but that is not typical.

    Q-437: How do you guarantee that a rendering pass does not modify the stencil buffer?

  • gl.stencilMask(0x00)
  • Correct.
  • gl.stencilMask(0xFF)
  • Incorrect. This makes every bit in the stencil buffer changeable.
  • gl.stencilMask(0x01)
  • Incorrect. This allows the low-order bit of every stencil buffer element to be changed.
  • gl.stencilMask(0xF0)
  • Incorrect. This allows any of the four high-order bits of every stencil buffer element to be changed.

    Q-438: A rendering pass is desired that increments a stencil buffer element if the “depth test” fails on a fragment. Which of the following parameters to gl.stencilOp would accomplish this?

  • gl.stencilOp( gl.KEEP, gl.INCR, gl.KEEP )
  • Correct.
  • gl.stencilOp( gl.KEEP, gl.KEEP, gl.INCR )
  • Incorrect. The parameters are in the wrong position.
  • gl.stencilOp( gl.ZERO, gl.INCR, gl.ZERO )
  • Incorrect. This also changes a stencil buffer element to zero when the “stencil test” fails or the “depth test” passes – actions that are not desired.
  • gl.stencilOp( gl.KEEP, gl.REPLACE, gl.KEEP )
  • Incorrect. This replaces the value of a stencil buffer element, it doesn’t increment it.
Next Section - 12.10 - 3D Text