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 thegl.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:
- 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.
- 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.”)
- 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.
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.
Animate
Border Size = 0.20 | 0.0 1.0 |
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:
- Render a model.
- Render a flat plane with the stencil test enabled. This remembers which pixels the flat plane covers.
- 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.
Example using the Stencil Buffer.
Animate
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 inn
where there is a0
in the bit-mask there is a0
in the result.
Self Assessment¶
- 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-433: What is the size 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-434: What type of data is each element of a stencil buffer?
- 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-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?
- 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-436: How many rendering passes does it typically take to use a stencil buffer to control which color buffer pixels can be modified?
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-437: How do you guarantee that a rendering pass does not modify the stencil buffer?
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.
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?