11.4 - Transparency (and Alpha Blending)¶
The z-buffer algorithm for performing hidden surface removal stores the color of the object that is closest to the camera in the color buffer of the frame buffer. This is the desired behavior when solid objects block the view of other solid objects. But what about objects that are partially transparent that allow the objects behind them to be partially visible? This lesson explains the standard technique for rendering objects that contain transparent surfaces.
Transparency¶
If an object allows light to pass through it, a viewer sees some of the light reflected from the object and some of the light reflected from the object that is behind the surface. Therefore, transparency requires the combining of light from two sources. Let’s review the z-buffer algorithm – which looks like this:
void renderPixel(x, y, z, color) {
if (z < zbuffer[x][y]) {
zbuffer[x][y] = z;
colorbuffer[x][y] = color;
}
}
Notice that we have two colors represented in this algorithm: 1) the color
already in the color buffer, colorbuffer[x][y]
, and 2) the color
of the object being rendered, color
. If we set up the rendering
context carefully, the graphics pipeline can combine the colors using
the amount of transparency of the object. The rendering algorithm changes
to this:
void renderPixel(x, y, z, color) {
if (z < zbuffer[x][y]) {
zbuffer[x][y] = z;
colorbuffer[x][y] = (colorbuffer[x][y] * percent_left_over) + (color * percent_of_reflection);
}
}
Where do the “percentages” come from? Given an RGBA (red, green, blue, alpha)
color value, the “alpha” value represents the amount of light that is reflected.
If alpha is 1.0, all light is reflected and the object is “opaque”. If the alpha
value is 0.75, the object reflects 75% of the light that strikes it, which means
25% of the light passes through. So the percentage values come from the color
value
like this:
percent_of_reflection = color.alpha; // or color[3]
percent_left_over = 1.0 - percent_of_reflection;
To setup the graphics pipeline to perform this “blending of colors”, you call two JavaScript functions: the first enables color blending, and the second specifies the blending percentages using pre-defined enumerated constants. Hopefully the names of the constants are self explanatory.
gl.enable(gl.BLEND)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
Note that transparent objects are not visible if they are behind other objects. Therefore, we don’t want to turn hidden surface removal off. However, what if there are multiple transparent objects in a scene and light travels through more than one of them? In such a situation we need to perform the color combination for each object the light passes through. To calculate the correct final color we need to process the objects furthest from the camera first, and the objects closest to the camera last! That means we need to sort the transparent objects! We’ll talk more about sorting in a few minutes. Taking all of these issues into account, here are the major steps in a rendering algorithm the handles transparent surfaces:
Clear the color buffer and the z-buffer -
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
Render all of the objects in a scene that are opaque. (The order does not matter.)
Sort the transparent objects in the scene based on their distance from the camera. (Greatest to least distance.)
Sort the graphic primitives in a model based on their distance from the camera. (Greatest to least distance.)
Enable blending and set the blending percentages.
gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
Keep the z-buffer algorithm active, but disable changes to the zbuffer array. (
gl.depthMask(false)
) (This applies only if transparent models occupy the same 3D space. This is discussed below in more detail.)Render the sorted transparent objects and primitives, greatest distance from the camera to the least distance.
What makes this algorithm very inefficient is the sorting. But before we discuss sorting, recognize that prior knowledge about a specific scene might allow you to ignore sorting altogether. Here are some specific scenarios where you can safely ignore sorting:
- There is only one transparent model in a scene. The primitives in the model must be sorted, but you can simply render the transparent model last in the scene.
- There are multiple transparent objects in a scene, but you know that none of them overlap each other from a particular camera angle. Therefore, you can simply render these models last (but in any order).
- If you have a model that contains some opaque surfaces and some transparent
surfaces, then the following situations might apply:
- You know that the transparent surfaces never face the camera. Therefore you don’t have to worry about the transparent surfaces.
- The model defines a totally enclosed 3D area and the surfaces behind any transparent surfaces are faces of the same model. Therefore you only have to be concerned with ordering the faces in that particular model, assuming that other models do not intersect in 3D space.
To summarize, if you can use knowledge of a scene to avoid sorting, it is worth the trouble. This is a general principle of all computer graphics – if something is not visible, don’t worry about rendering it correctly.
Sorting for Transparency¶
To render a scene we render a set of models, where each model is composed of triangles. When we discuss sorting we have two issues:
- sorting models relative to each other, and
- sorting primitives (points, lines, and triangles) within models.
Sorting the Primitives of a Model¶
Given the primitives of a model, we need to sort them based on their distance from the camera. This is problematic because the big idea behind fast rendering is to place the primitive data into a GPU object buffer that never changes. Typically a model is rendered in different sizes, locations, and orientations in a scene using a transformation matrix, while the model data remains static. But now the data has to be re-ordered. We have two basic options:
- Leave the model data in a GPU object buffer unchanged, and render each
primitive (triangle) using a separate call to
gl.drawArrays()
, or - Re-order the triangle data in the GPU object buffer.
The first method produces slower rendering speeds, but is simpler to implement. The second method will render faster, but requires more JavaScript code to implement. The demonstration programs in this lesson use the first method.
For the actual sorting, you should use an insertion sort. Why? Note that you must sort on every rendering operation; you are not sorting just once. If a scene changes very little from one rendering to the next, the relative ordering of models in a scene will not change much. Therefore, assuming you re-use your previous sorting results, you need to sort a list of primitives that is almost sorted. An insertion sort is the fastest way to sort a list that is already almost sorted. (Don’t ever use quick sort or merge sort for a soring task like this. These sorting algorithms are the fastest general purpose sorting methods in common use, but they can’t “quit early” and they don’t have good run-time behaviour on sorted data.)
To sort the triangles that make up a model, we need the vertices of the triangles after the model and view transforms have been applied to it. In addition, we don’t want to move the data in memory, we just want to find their sorted order. Therefore we can perform an “index sort”, where we use indexes into an array of values to keep track of the sorted order, but never actually rearrange the data values. Here is a general algorithm for sorting the triangles that compose a model:
- For each triangle of a model:
- Transform each vertex of the triangle by the current ModelView transformation.
- Determine the vertex that is farthest from the camera. (Since the camera is looking down the -Z axis, this is the minimum value of z.)
- Store this vertex’s z-component as the distance of the triangle from the camera.
- Perform an insertion sort on the triangles, using the z-component of the vertex that is farthest from the camera as the sorting key.
- Render the model:
- If you leave the GPU object buffer unchanged, loop through the
triangles and call
gl.drawArrays()
once for each triangle. - If you create a new 1D array of model data in sorted order and copy it
to a GPU object buffer, then make a single call to
gl.drawArrays()
.
- If you leave the GPU object buffer unchanged, loop through the
triangles and call
The following function initializes an array of index values in preparation for sorting.
var sort_indexes = null;
//-----------------------------------------------------------------------
/**
* Initialize the sort_indexes array for sorting the model's triangles.
* This array is re-sorted before each render of a transparent model.
* @private
*/
function _initialize_sorting() {
var j;
if (number_triangles > 0) {
sort_indexes = new Array(number_triangles);
for (j = 0; j < number_triangles; j += 1) {
sort_indexes[j] = [j, 0.0]; // [index to triangle, distance from camera]
}
}
}
Let’s assume that a model is defined by a set of triangles whose vertices are stored in a 1D array of floats – 9 floats per triangle, 3 floats per vertex. The array is organized like this:
vertices = [v1x, v1y, v1z, v2x, v2y, v2z, v3x, v3y, v3z, ...]
The following function sorts indexes into this array of floats. The model’s vertices are multiplied by a ModelView transform that puts the camera at the origin looking down the -Z axis. For a given triangle, the vertex with the smallest z-value is the farthest from the camera.
//-----------------------------------------------------------------------
/**
* Sort the triangles of a model, back to front, based on their distance
* from the camera.
* @param vm_transform Float32Array The transformation to apply to the model vertices.
*/
function _sort_triangles (vm_transform) {
var j, k, n, which_triangle, vertices, max_z, temp;
// Step 1: Transform each vertex in a model by the current *ModelView* transformation.
// Step 2: For each triangle, determine its maximum distance from the camera.
vertices = model.triangles.vertices;
for (j = 0; j < number_triangles; j += 1) {
which_triangle = sort_indexes[j][0];
k = which_triangle * 3 * 3;
max_z = 10e10;
for (n = 0; n < 3; n += 1, k += 3) {
one_vertex[0] = vertices[k];
one_vertex[1] = vertices[k + 1];
one_vertex[2] = vertices[k + 2];
matrix.multiply(transformed_vertex, vm_transform, one_vertex);
if (transformed_vertex[2] < max_z) {
max_z = transformed_vertex[2];
}
}
// Remember this triangle's distance from the camera
sort_indexes[j][1] = max_z;
}
// Step 3: Perform an insertion sort on the triangles, using the vertex
// that is farthest from the camera as the sorting key.
for (j = 0; j < number_triangles; j += 1) {
temp = sort_indexes[j];
k = j - 1;
while (k >= 0 && sort_indexes[k][1] > temp[1]) {
sort_indexes[k + 1] = sort_indexes[k];
k -= 1;
}
sort_indexes[k + 1] = temp;
}
}
Sorting Models¶
We need to render the transparent models in a scene from back to front. If the models do not overlap in 3D space, this is just a matter of sorting the models based on their distance from the camera. Since the models do not overlap, you can use any vertex on the model, or a model’s center point, to calculate the distance. For the WebGL demonstration program below, which displays spheres, the distances are calculated using the center point of each sphere.
If two or more transparent models overlap in 3D space, it is not possible to render them correct as independent models. To render them correctly you must combine the models, sort their combined triangles, and then render the triangles from back to front. This is a quandary! We keep models as separate entities so that they can be transformed independently. But for rendering, we need the models to be combined into a single list of primitives. If you combine the models as a preprocessing step, the models can’t be transformed independently. If you combine the models at render time it can greatly slow down your rendering frame rate.
Experimentation 1 (Non-overlapping models)¶
Please experiment with the following WebGL demonstration program by disabling the animation and rotating the models to study the transparency. Rotate to a view that allows you to see through multiple transparent models with an opaque model in the background. Is the rendering correct? Do you see any models that are rendered incorrectly? (There will be errors if any of the spheres overlap.)
/**
* TransparencyExampleRender.js, By Wayne Brown, Spring 2016
*/
/**
* The MIT License (MIT)
*
* Copyright (c) 2015 C. Wayne Brown
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
"use strict";
var console, Learn_webgl_point4;
//-------------------------------------------------------------------------
// Build, create, copy and render 3D objects specific to a particular
// model definition and particular WebGL shaders.
//-------------------------------------------------------------------------
window.TransparencyExampleRender = function (learn, vshaders_dictionary,
fshaders_dictionary, models, controls) {
// Private variables
var self = this;
var canvas_id = learn.canvas_id;
var out = learn.out;
var gl = null;
var program = null;
var a_sphere = null;
var number_spheres = 10;
var number_transparent_spheres = 5;
var number_opaque_spheres = number_spheres - number_transparent_spheres;
var all_spheres = new Array(number_spheres);
var matrix = new window.Learn_webgl_matrix();
var transform = matrix.create();
var view_model_transform = matrix.create();
var projection = matrix.createPerspective(45.0, 1.0, 0.1, 100.0);
var camera = matrix.create();
var scale_matrix = matrix.create();
var translate_matrix = matrix.create();
var eye_x;
var eye_y;
var eye_z;
// Public variables that will possibly be used or changed by event handlers.
self.canvas = null;
self.angle_x = 0.0;
self.angle_y = 0.0;
self.scale = 1.0;
self.animate_active = false;
// Light model
var P4 = new window.Learn_webgl_point4();
var V = new window.Learn_webgl_vector3();
self.light_position = P4.create(10, 10, 10, 1);
self.light_color = V.create(1, 1, 1); // white light
self.shininess = 30;
self.ambient_color = V.create(0.2, 0.2, 0.2); // low level white light
var transformed_center = P4.create();
var sort_indexes = null;
//-----------------------------------------------------------------------
/**
* Initialize the sort_indexes array for sorting the transparent models.
* This array is re-sorted before each render of the scene.
* @private
*/
function _initialize_sorting() {
var j;
sort_indexes = new Array(number_transparent_spheres);
for (j = 0; j < number_transparent_spheres; j += 1) {
sort_indexes[j] = [j, 0.0]; // [index to triangle, distance from camera]
}
}
/** ---------------------------------------------------------------------
* Update the distance from each transparent sphere to the camera. This
* uses the center of the sphere for the distance, which causes problems
* if two transparent spheres overlap in 3D space.
* @private
*/
function _update_distances_to_camera () {
var j, index, position;
for (j = 0; j < number_transparent_spheres; j += 1) {
index = sort_indexes[j][0];
position = all_spheres[index].position;
matrix.translate(translate_matrix, position[0], position[1], position[2]);
// Combine the camera and sphere translation transforms.
matrix.multiplySeries(view_model_transform, camera, translate_matrix);
// Calculate where the sphere is in relationship to the camera.
matrix.multiply(transformed_center, view_model_transform, position);
// Set this model's distance from the camera
sort_indexes[j][1] = transformed_center[2];
}
}
/** ---------------------------------------------------------------------
* Sort the models, back to front, based on their distance from the camera.
* @private
*/
function _sort_spheres () {
var j, k, temp;
// Perform an insertion sort on the models.
for (j = 0; j < number_transparent_spheres; j += 1) {
temp = sort_indexes[j];
k = j - 1;
while (k >= 0 && sort_indexes[k][1] > temp[1]) {
sort_indexes[k + 1] = sort_indexes[k];
k -= 1;
}
sort_indexes[k + 1] = temp;
}
}
//-----------------------------------------------------------------------
self.render = function () {
var j, size, position, x_radians, y_radians, scale, index;
// Clear the entire canvas window background with the clear color
// out.display_info("Clearing the screen");
gl.depthMask(true);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Build the camera transform to circle the origin.
x_radians = matrix.toRadians(self.angle_x);
y_radians = matrix.toRadians(self.angle_y);
scale = Math.cos(x_radians);
eye_x = Math.cos(y_radians) * scale * 20.0;
eye_z = Math.sin(y_radians) * scale * 20.0;
eye_y = Math.sin(x_radians) * 20.0;
matrix.lookAt(camera, eye_x, eye_y, eye_z, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Set the location of the light source
gl.uniform3f(program.u_Light_position, self.light_position[0],
self.light_position[1],
self.light_position[2]);
gl.uniform3fv(program.u_Light_color, self.light_color);
gl.uniform3fv(program.u_Ambient_color, self.ambient_color);
gl.uniform1f(program.u_Shininess, self.shininess);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Render the opaque spheres first.
gl.disable(gl.BLEND);
for (j = number_transparent_spheres; j < number_spheres; j += 1) {
size = all_spheres[j].size;
position = all_spheres[j].position;
matrix.scale(scale_matrix, size, size, size);
matrix.translate(translate_matrix, position[0], position[1], position[2]);
// Combine the transforms into a single transformation
matrix.multiplySeries(view_model_transform, camera,
translate_matrix, scale_matrix);
matrix.multiplySeries(transform, projection, view_model_transform);
// Assign a color to the sphere. Make it red if it is the selected object.
a_sphere.setColor(all_spheres[j].color);
a_sphere.render(transform, view_model_transform);
}
// Sort the transparent models, back to front, in reference to the camera
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Render the transparent spheres in sorted order.
// Enable alpha blending and set the percentage blending factors
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// Turn off updating of the z-buffer
gl.depthMask(false);
// Update the distance of each sphere from the camera
_update_distances_to_camera();
// Sort the models based on their distance from the camera. The center
// point of each sphere is used for the distance.
_sort_spheres();
// Render the transparent spheres
for (j = 0; j < number_transparent_spheres; j += 1) {
index = sort_indexes[j][0];
size = all_spheres[index].size;
position = all_spheres[index].position;
matrix.scale(scale_matrix, size, size, size);
matrix.translate(translate_matrix, position[0], position[1], position[2]);
// Combine the transforms into a single transformation
matrix.multiplySeries(view_model_transform, camera,
translate_matrix, scale_matrix);
matrix.multiplySeries(transform, projection, view_model_transform);
// Set the sphere's color.
a_sphere.setColor(all_spheres[index].color);
a_sphere.render(transform, view_model_transform);
}
};
//-----------------------------------------------------------------------
self.delete = function () {
// Clean up shader programs
gl.deleteShader(program.vShader);
gl.deleteShader(program.fShader);
gl.deleteProgram(program);
program = null;
// Delete each model's VOB
a_sphere.delete();
// Delete all of the events associated with this canvas
events.removeAllEventHandlers();
// Disable any animation
self.animate_active = false;
};
//-----------------------------------------------------------------------
// Object constructor. One-time initialization of the scene.
// Get the rendering context for the canvas
self.canvas = learn.getCanvas(canvas_id);
if (self.canvas) {
gl = learn.getWebglContext(self.canvas);
}
if (!gl) {
return;
}
// Set up the rendering program and set the state of webgl
program = learn.createProgram(gl, vshaders_dictionary.shader41, fshaders_dictionary.shader41);
gl.useProgram(program);
gl.enable(gl.DEPTH_TEST);
gl.clearColor(0.9, 0.9, 0.9, 1.0);
// Create Vertex Object Buffers for the models
a_sphere = new window.Learn_webgl_model_render_41(gl, program,
models.Icosphere, out);
// Create a set of random positions, colors, and sizes for a group of spheres.
var j, position_x, position_y, position_z, color;
for (j = 0; j < number_spheres; j += 1) {
position_x = Math.random() * 10 - 5.0;
position_y = Math.random() * 10 - 5.0;
position_z = Math.random() * 10 - 5.0;
if (j < number_transparent_spheres) {
color = new Float32Array([Math.random(), Math.random(), Math.random(), Math.random()]);
} else {
color = new Float32Array([Math.random(), Math.random(), Math.random(), 1.0]);
}
all_spheres[j] = { position: P4.create(position_x, position_y, position_z),
size: Math.random() * 5,
color: color
};
}
// Set up callbacks for user and timer events
var events;
events = new window.TransparencyExampleEvents(self, controls);
events.animate();
// Initialize the array of model indexes that will be used for sorting.
_initialize_sorting();
};
/**
* simple_model_render_40.js, By Wayne Brown, Fall 2015
*/
/**
* The MIT License (MIT)
*
* Copyright (c) 2015 C. Wayne Brown
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
"use strict";
//-------------------------------------------------------------------------
/**
* Given a model description, create the buffer objects needed to render
* the model. This is very closely tied to the shader implementations.
* @param gl Object The WebGL state and API
* @param select_program Object The shader program the will render the model for selection.
* @param visible_program Object The shader program the will render the model for selection.
* @param model Simple_model The model data.
* @param out Object Can display messages to the webpage.
* @constructor
*/
window.Learn_webgl_model_render_41 = function (gl, program, model, out) {
var self = this;
// Variables to remember so the model can be rendered.
var number_triangles = 0;
var triangles_vertex_buffer_id = null;
var triangles_color_buffer_id = null;
var triangles_normal_buffer_id = null;
var triangles_smooth_normal_buffer_id = null;
var number_lines = 0;
var lines_vertex_buffer_id = null;
var lines_color_buffer_id = null;
var number_points = 0;
var points_vertex_buffer_id = null;
var points_color_buffer_id = null;
var model_color = null;
var contains_transparent_surfaces = false;
// For sorting the vertices of a triangle
var matrix = new window.Learn_webgl_matrix();
var p4 = new window.Learn_webgl_point4();
var one_vertex = p4.create();
var transformed_vertex = p4.create();
var sort_indexes = null;
//-----------------------------------------------------------------------
/**
* Initialize the sort_indexes array for sorting the model's triangles.
* This array is re-sorted before each render of a transparent model.
* @private
*/
function _initialize_sorting() {
var j;
if (number_triangles > 0) {
sort_indexes = new Array(number_triangles);
for (j = 0; j < number_triangles; j += 1) {
sort_indexes[j] = [j, 0.0]; // [index to triangle, distance from camera]
}
}
}
//-----------------------------------------------------------------------
function _createBufferObject(data) {
// Create a buffer object
var buffer_id;
buffer_id = gl.createBuffer();
if (!buffer_id) {
out.displayError('Failed to create the buffer object for ' + model.name);
return null;
}
// Make the buffer object the active buffer.
gl.bindBuffer(gl.ARRAY_BUFFER, buffer_id);
// Upload the data for this buffer object to the GPU.
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
return buffer_id;
}
//-----------------------------------------------------------------------
/**
* Create the buffer objects needed and upload the data to the GPU
* @private
*/
function _buildBufferObjects() {
// Build the buffers for the triangles
if (model.triangles !== null && model.triangles.vertices.length > 0) {
number_triangles = model.triangles.vertices.length / 3 / 3;
triangles_vertex_buffer_id = _createBufferObject(model.triangles.vertices);
triangles_color_buffer_id = _createBufferObject(model.triangles.colors);
triangles_normal_buffer_id = _createBufferObject(model.triangles.flat_normals);
triangles_smooth_normal_buffer_id = _createBufferObject(model.triangles.smooth_normals);
sort_indexes = new Array(number_triangles);
}
// Build the buffers for the lines
if (model.lines !== null && model.lines.vertices.length > 0) {
number_lines = model.lines.vertices.length / 3 / 2;
lines_vertex_buffer_id = _createBufferObject(model.lines.vertices);
lines_color_buffer_id = _createBufferObject(model.lines.colors);
}
// Build the buffers for the points
if (model.points !== null && model.points.vertices.length > 0) {
number_points = model.points.vertices.length / 3; // 3 components per vertex
points_vertex_buffer_id = _createBufferObject(model.points.vertices);
points_color_buffer_id = _createBufferObject(model.points.colors);
}
}
//-----------------------------------------------------------------------
/**
* Get the location of the shader variables in the shader program.
* @private
*/
function _getLocationOfShaderVariables() {
// Get the location of the shader variables
program.u_PVM_transform = gl.getUniformLocation(program, "u_PVM_transform");
program.u_VM_transform = gl.getUniformLocation(program, "u_VM_transform");
program.u_Light_position = gl.getUniformLocation(program, "u_Light_position");
program.u_Light_color = gl.getUniformLocation(program, "u_Light_color");
program.u_Ambient_color = gl.getUniformLocation(program, "u_Ambient_color");
program.u_Shininess = gl.getUniformLocation(program, "u_Shininess");
program.a_Vertex = gl.getAttribLocation(program, 'a_Vertex');
//visible_program.a_Color = gl.getAttribLocation(visible_program, 'a_Color');
program.a_Vertex_normal = gl.getAttribLocation(program, 'a_Vertex_normal');
program.u_Model_color = gl.getUniformLocation(program, "u_Model_color");
}
//-----------------------------------------------------------------------
// These one-time tasks set up the rendering of the models.
_buildBufferObjects();
_getLocationOfShaderVariables();
_initialize_sorting();
//-----------------------------------------------------------------------
/**
* Set the model's uniform color.
* @param color Array An RGBA array used for the object's selection ID
*/
self.setColor = function (color) {
model_color = color;
if (color[3] !== 1.0) {
contains_transparent_surfaces = true;
}
};
//-----------------------------------------------------------------------
/**
* Delete the Buffer Objects associated with this model.
*/
self.delete = function () {
if (number_triangles > 0) {
gl.deleteBuffer(triangles_vertex_buffer_id);
gl.deleteBuffer(triangles_color_buffer_id);
gl.deleteBuffer(triangles_normal_buffer_id);
gl.deleteBuffer(triangles_smooth_normal_buffer_id);
}
if (number_lines > 0) {
gl.deleteBuffer(lines_vertex_buffer_id);
gl.deleteBuffer(lines_color_buffer_id);
}
if (number_points > 0) {
gl.deleteBuffer(points_vertex_buffer_id);
gl.deleteBuffer(points_color_buffer_id);
}
};
//-----------------------------------------------------------------------
/**
* Render the triangles in the model.
* @private
*/
function _renderTriangles() {
var j;
if (number_triangles > 0) {
// Activate the model's triangle vertex object buffer (VOB)
gl.bindBuffer(gl.ARRAY_BUFFER, triangles_vertex_buffer_id);
// Bind the vertices VOB to the 'a_Vertex' shader variable
//var stride = self.vertices3[0].BYTES_PER_ELEMENT*3;
gl.vertexAttribPointer(program.a_Vertex, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(program.a_Vertex);
// Activate the model's triangle color object buffer
//gl.bindBuffer(gl.ARRAY_BUFFER, triangles_color_buffer_id);
// Bind the colors VOB to the 'a_Color' shader variable
//gl.vertexAttribPointer(visible_program.a_Color, 3, gl.FLOAT, false, 0, 0);
//gl.enableVertexAttribArray(visible_program.a_Color);
// Activate the model's triangle normal vector object buffer
gl.bindBuffer(gl.ARRAY_BUFFER, triangles_smooth_normal_buffer_id);
// Bind the normal vectors VOB to the 'a_Normal' shader variable
gl.vertexAttribPointer(program.a_Vertex_normal, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(program.a_Vertex_normal);
if (contains_transparent_surfaces) {
// Draw all of the triangles in sorted order
for (j = 0; j < number_triangles; j += 1) {
gl.drawArrays(gl.TRIANGLES, sort_indexes[j][0] * 3, 3);
}
} else {
gl.drawArrays(gl.TRIANGLES, 0, number_triangles * 3);
}
}
}
//-----------------------------------------------------------------------
/**
* Sort the triangles of a model, back to front, based on their distance
* from the camera.
* @param vm_transform Float32Array The transformation to apply to the model vertices.
*/
function _sort_triangles (vm_transform) {
var j, k, n, which_triangle, vertices, max_z, temp;
// Step 1: Transform each vertex in a model by the current *ModelView* transformation.
// Step 2: For each triangle, determine its maximum distance from the camera.
vertices = model.triangles.vertices;
for (j = 0; j < number_triangles; j += 1) {
which_triangle = sort_indexes[j][0];
k = which_triangle * 3 * 3;
max_z = 10e10;
for (n = 0; n < 3; n += 1, k += 3) {
one_vertex[0] = vertices[k];
one_vertex[1] = vertices[k + 1];
one_vertex[2] = vertices[k + 2];
matrix.multiply(transformed_vertex, vm_transform, one_vertex);
if (transformed_vertex[2] < max_z) {
max_z = transformed_vertex[2];
}
}
// Remember this triangle's distance from the camera
sort_indexes[j][1] = max_z;
}
// Step 3: Perform an insertion sort on the triangles, using the vertex
// that is farthest from the camera as the sorting key.
for (j = 0; j < number_triangles; j += 1) {
temp = sort_indexes[j];
k = j - 1;
while (k >= 0 && sort_indexes[k][1] > temp[1]) {
sort_indexes[k + 1] = sort_indexes[k];
k -= 1;
}
sort_indexes[k + 1] = temp;
}
}
//-----------------------------------------------------------------------
/**
* Render the model.
* @param pvm_transform Float32Array The projection, view, model transform.
* @param vm_transform Float32Array The view, model transform.
*/
self.render = function (pvm_transform, vm_transform) {
if (contains_transparent_surfaces) {
_sort_triangles (vm_transform);
}
// Set the transform for all the faces, lines, and points
gl.uniformMatrix4fv(program.u_PVM_transform, false, pvm_transform);
gl.uniformMatrix4fv(program.u_VM_transform, false, vm_transform);
gl.uniform4fv(program.u_Model_color, model_color);
_renderTriangles();
};
};
An example of object transparency.
Animate
Experiments on the transparency_example_render_js
code:
- If you restart the program you will get different combinations of random spheres.
- In lines 49-50 you can set the number of spheres to render and the
number of spheres that are transparent. Try different combinations of
models.
- If you increase the numbers you will probably see spheres that overlap and therefore render incorrectly.
- Set the numbers back to 10 and 5 before continuing.
- In line 200, comment out
gl.enable(gl.BLEND);
. Notice that all transparency is now gone. Without color “blending” there is no transparency. (Enable blending before continuing.) - Don’t sort the models by commenting out line 211. You will get some strange visual effects because the the motion of the transparent spheres will not match your mental understanding of their position in the 3D world. (Turn the sorting back on before continuing.)
Experiments on the learn_webgl_model_render_41.js
code:
- In line 299, comment out the call to the
_sort_triangles
function. Notice the “blotching” effect on the rendered spheres. This is because the triangles are being rendered in the order they are defined in the buffer object. This causes some of the triangles that are closest to the camera to be rendered before the object behind the triangle has the correct color. This causes the wrong colors to be blended together.
Experimentation 2 (Overlapping models)¶
This demonstration program allows you visualize what happens when transparent models overlap. Disable the animation and manually rotate the view. Notice that the rendering has dramatic changes when one sphere gets closer to the camera than the other spheres. It is not possible to render each sphere independently and render all of the triangles in the correct back-to-front order.
/**
* TransparencyExampleRender.js, By Wayne Brown, Spring 2016
*/
/**
* The MIT License (MIT)
*
* Copyright (c) 2015 C. Wayne Brown
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
"use strict";
var console, Learn_webgl_point4;
//-------------------------------------------------------------------------
// Build, create, copy and render 3D objects specific to a particular
// model definition and particular WebGL shaders.
//-------------------------------------------------------------------------
window.TransparencyExample2Render = function (learn, vshaders_dictionary,
fshaders_dictionary, models, controls) {
// Private variables
var self = this;
var canvas_id = learn.canvas_id;
var out = learn.out;
var gl = null;
var program = null;
var a_sphere = null;
var number_spheres = 3;
var number_transparent_spheres = 3;
var number_opaque_spheres = number_spheres - number_transparent_spheres;
var all_spheres = new Array(number_spheres);
var matrix = new window.Learn_webgl_matrix();
var transform = matrix.create();
var view_model_transform = matrix.create();
var projection = matrix.createPerspective(45.0, 1.0, 0.1, 100.0);
var camera = matrix.create();
var scale_matrix = matrix.create();
var translate_matrix = matrix.create();
var eye_x;
var eye_y;
var eye_z;
// Public variables that will possibly be used or changed by event handlers.
self.canvas = null;
self.angle_x = 0.0;
self.angle_y = 0.0;
self.scale = 1.0;
self.animate_active = false;
// Light model
var P4 = new window.Learn_webgl_point4();
var V = new window.Learn_webgl_vector3();
self.light_position = P4.create(10, 10, 10, 1);
self.light_color = V.create(1, 1, 1); // white light
self.shininess = 30;
self.ambient_color = V.create(0.2, 0.2, 0.2); // low level white light
var transformed_center = P4.create();
var sort_indexes = null;
//-----------------------------------------------------------------------
/**
* Initialize the sort_indexes array for sorting the transparent models.
* This array is re-sorted before each render of the scene.
* @private
*/
function _initialize_sorting() {
var j;
sort_indexes = new Array(number_transparent_spheres);
for (j = 0; j < number_transparent_spheres; j += 1) {
sort_indexes[j] = [j, 0.0]; // [index to triangle, distance from camera]
}
}
/** ---------------------------------------------------------------------
* Update the distance from each transparent sphere to the camera. This
* uses the center of the sphere for the distance, which causes problems
* if two transparent spheres overlap in 3D space.
* @private
*/
function _update_distances_to_camera () {
var j, index, position;
for (j = 0; j < number_transparent_spheres; j += 1) {
index = sort_indexes[j][0];
position = all_spheres[index].position;
matrix.translate(translate_matrix, position[0], position[1], position[2]);
// Combine the camera and sphere translation transforms.
matrix.multiplySeries(view_model_transform, camera, translate_matrix);
// Calculate where the sphere is in relationship to the camera.
matrix.multiply(transformed_center, view_model_transform, position);
// Set this model's distance from the camera
sort_indexes[j][1] = transformed_center[2];
}
}
/** ---------------------------------------------------------------------
* Sort the models, back to front, based on their distance from the camera.
* @private
*/
function _sort_spheres () {
var j, k, temp;
// Perform an insertion sort on the models.
for (j = 0; j < number_transparent_spheres; j += 1) {
temp = sort_indexes[j];
k = j - 1;
while (k >= 0 && sort_indexes[k][1] > temp[1]) {
sort_indexes[k + 1] = sort_indexes[k];
k -= 1;
}
sort_indexes[k + 1] = temp;
}
}
//-----------------------------------------------------------------------
self.render = function () {
var j, size, position, x_radians, y_radians, scale, index;
// Clear the entire canvas window background with the clear color
// out.display_info("Clearing the screen");
gl.depthMask(true);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Build the camera transform to circle the origin.
x_radians = matrix.toRadians(self.angle_x);
y_radians = matrix.toRadians(self.angle_y);
scale = Math.cos(x_radians);
eye_x = Math.cos(y_radians) * scale * 20.0;
eye_z = Math.sin(y_radians) * scale * 20.0;
eye_y = Math.sin(x_radians) * 20.0;
matrix.lookAt(camera, eye_x, eye_y, eye_z, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Set the location of the light source
gl.uniform3f(program.u_Light_position, self.light_position[0],
self.light_position[1],
self.light_position[2]);
gl.uniform3fv(program.u_Light_color, self.light_color);
gl.uniform3fv(program.u_Ambient_color, self.ambient_color);
gl.uniform1f(program.u_Shininess, self.shininess);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Render the opaque spheres first.
gl.disable(gl.BLEND);
for (j = number_transparent_spheres; j < number_spheres; j += 1) {
size = all_spheres[j].size;
position = all_spheres[j].position;
matrix.scale(scale_matrix, size, size, size);
matrix.translate(translate_matrix, position[0], position[1], position[2]);
// Combine the transforms into a single transformation
matrix.multiplySeries(view_model_transform, camera,
translate_matrix, scale_matrix);
matrix.multiplySeries(transform, projection, view_model_transform);
// Assign a color to the sphere. Make it red if it is the selected object.
a_sphere.setColor(all_spheres[j].color);
a_sphere.render(transform, view_model_transform);
}
// Sort the transparent models, back to front, in reference to the camera
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Render the transparent spheres in sorted order.
// Enable alpha blending and set the percentage blending factors
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// Turn off updating of the z-buffer
gl.depthMask(false);
// Update the distance of each sphere from the camera
_update_distances_to_camera();
// Sort the models based on their distance from the camera. The center
// point of each sphere is used for the distance.
_sort_spheres();
// Render the transparent spheres
for (j = 0; j < number_transparent_spheres; j += 1) {
index = sort_indexes[j][0];
size = all_spheres[index].size;
position = all_spheres[index].position;
matrix.scale(scale_matrix, size, size, size);
matrix.translate(translate_matrix, position[0], position[1], position[2]);
// Combine the transforms into a single transformation
matrix.multiplySeries(view_model_transform, camera,
translate_matrix, scale_matrix);
matrix.multiplySeries(transform, projection, view_model_transform);
// Set the sphere's color.
a_sphere.setColor(all_spheres[index].color);
a_sphere.render(transform, view_model_transform);
}
};
//-----------------------------------------------------------------------
self.delete = function () {
// Clean up shader programs
gl.deleteShader(program.vShader);
gl.deleteShader(program.fShader);
gl.deleteProgram(program);
program = null;
// Delete each model's VOB
a_sphere.delete();
// Delete all of the events associated with this canvas
events.removeAllEventHandlers();
// Disable any animation
self.animate_active = false;
};
//-----------------------------------------------------------------------
// Object constructor. One-time initialization of the scene.
// Get the rendering context for the canvas
self.canvas = learn.getCanvas(canvas_id);
if (self.canvas) {
gl = learn.getWebglContext(self.canvas);
}
if (!gl) {
return;
}
// Set up the rendering program and set the state of webgl
program = learn.createProgram(gl, vshaders_dictionary.shader41, fshaders_dictionary.shader41);
gl.useProgram(program);
gl.enable(gl.DEPTH_TEST);
gl.clearColor(0.9, 0.9, 0.9, 1.0);
// Create Vertex Object Buffers for the models
a_sphere = new window.Learn_webgl_model_render_41(gl, program,
models.Icosphere, out);
all_spheres[0] = { position: P4.create(-2, 0, 0),
size: 3,
color: new Float32Array([1.0, 0.0, 0.0, 0.4])
};
all_spheres[1] = { position: P4.create(2, 0, 0),
size: 3,
color: new Float32Array([0.0, 1.0, 0.0, 0.4])
};
all_spheres[2] = { position: P4.create(0, 0, -2),
size: 3,
color: new Float32Array([0.0, 0.0, 1.0, 0.4])
};
// Set up callbacks for user and timer events
var events;
events = new window.TransparencyExample2Events(self, controls);
events.animate();
// Initialize the array of model indexes that will be used for sorting.
_initialize_sorting();
};
An example of three transparent objects that overlap. Notice how the rendering changes when the sorted ordering of the objects change.
Animate
Please experiment with enabling and disabling “writing to the zbuffer” by
commenting out gl.depthMask(false)
in line 204. In normal operation,
the z-buffer algorithm updates the zbuffer to hold the distance of the
closest object to the camera from a particular pixel. So with “writing to the zbuffer”
enabled, gl.depthMask(true)
, the color buffer will only be updated with a
new color if an object closer to the camera is being rendered. If you have
sorted your models from back to front and are rendering them in that order,
you can leave “writing to the zbuffer” enabled and everything works fine,
except when two or more transparent models overlap in 3D space.
When “writing to the zbuffer” is disabled, gl.depthMask(false)
, you
get a reasonable rendering, but the rendering is wrong and the objects will
be rendered differently as the models change their relative location to the
camera.
When “writing to the zbuffer” is enabled, gl.depthMask(true)
, you
get a more accurate rendering of the model’s intersections, but you lose some
of the interior surfaces because the zbuffer does not allow the “behind” surfaces
to be rendered.
In summary, when transparent models overlap, you get the wrong results whether you enable or disable “writing to the zbuffer.” For a particular situation you need to decide which result gives the “better” visual results.
Experimentation 3 (Combined models)¶
This demonstration program displays a correct rendering of three overlapping spheres. It is created by combining the models into a single model that renders all triangles from back-to-front. To achieve this rendering each vertex must store a unique RGBA value. Notice that as you rotate the single model there is some visual artifacts at the intersections of the three spheres. This is because there are some triangles at the intersection locations that are being rendered in the wrong order. The visual artifacts could be eliminated by subdividing the triangles around the intersection locations – at the cost of slower rendering.
/**
* TransparencyExampleRender.js, By Wayne Brown, Spring 2016
*/
/**
* The MIT License (MIT)
*
* Copyright (c) 2015 C. Wayne Brown
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
"use strict";
var console, Learn_webgl_point4;
//-------------------------------------------------------------------------
// Build, create, copy and render 3D objects specific to a particular
// model definition and particular WebGL shaders.
//-------------------------------------------------------------------------
window.TransparencyExample3Render = function (learn, vshaders_dictionary,
fshaders_dictionary, models, controls) {
// Private variables
var self = this;
var canvas_id = learn.canvas_id;
var out = learn.out;
var gl = null;
var program = null;
var b_sphere = null;
var matrix = new window.Learn_webgl_matrix();
var transform = matrix.create();
var projection = matrix.createPerspective(45.0, 1.0, 0.1, 100.0);
var camera = matrix.create();
var eye_x;
var eye_y;
var eye_z;
// Public variables that will possibly be used or changed by event handlers.
self.canvas = null;
self.angle_x = 0.0;
self.angle_y = 0.0;
self.scale = 1.0;
self.animate_active = false;
// Light model
var P4 = new window.Learn_webgl_point4();
var V = new window.Learn_webgl_vector3();
self.light_position = P4.create(10, 10, 10, 1);
self.light_color = V.create(1, 1, 1); // white light
self.shininess = 30;
self.ambient_color = V.create(0.2, 0.2, 0.2); // low level white light
//-----------------------------------------------------------------------
self.render = function () {
var x_radians, y_radians, scale;
// Clear the entire canvas window background with the clear color
// out.display_info("Clearing the screen");
gl.depthMask(true);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Build the camera transform to circle the origin.
x_radians = matrix.toRadians(self.angle_x);
y_radians = matrix.toRadians(self.angle_y);
scale = Math.cos(x_radians);
eye_x = Math.cos(y_radians) * scale * 20.0;
eye_z = Math.sin(y_radians) * scale * 20.0;
eye_y = Math.sin(x_radians) * 20.0;
matrix.lookAt(camera, eye_x, eye_y, eye_z, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Set the location of the light source
gl.uniform3f(program.u_Light_position, self.light_position[0],
self.light_position[1],
self.light_position[2]);
gl.uniform3fv(program.u_Light_color, self.light_color);
gl.uniform3fv(program.u_Ambient_color, self.ambient_color);
gl.uniform1f(program.u_Shininess, self.shininess);
// Enable alpha blending and set the percentage blending factors
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// Turn off updating of the z-buffer
//gl.depthMask(false);
// Render the transparent spheres
matrix.multiplySeries(transform, projection, camera);
b_sphere.render(transform, camera);
};
//-----------------------------------------------------------------------
self.delete = function () {
// Clean up shader programs
gl.deleteShader(program.vShader);
gl.deleteShader(program.fShader);
gl.deleteProgram(program);
program = null;
// Delete each model's VOB
a_sphere.delete();
// Delete all of the events associated with this canvas
events.removeAllEventHandlers();
// Disable any animation
self.animate_active = false;
};
//-----------------------------------------------------------------------
// Object constructor. One-time initialization of the scene.
// Get the rendering context for the canvas
self.canvas = learn.getCanvas(canvas_id);
if (self.canvas) {
gl = learn.getWebglContext(self.canvas);
}
if (!gl) {
return;
}
// Set up the rendering program and set the state of webgl
program = learn.createProgram(gl, vshaders_dictionary.shader42, fshaders_dictionary.shader42);
gl.useProgram(program);
gl.enable(gl.DEPTH_TEST);
gl.clearColor(0.9, 0.9, 0.9, 1.0);
// Create Vertex Object Buffers for the models
b_sphere = new window.Learn_webgl_model_render_42(gl, program,
models.Sphere, out);
// Set up callbacks for user and timer events
var events;
events = new window.TransparencyExample3Events(self, controls);
events.animate();
};
An example of three transparent spheres (modeled as a single object) that overlap.
Animate
Alpha Blending (All the details)¶
The concept of blending the color that is already in the color buffer with a new color from a rendered model has been generalized to allow for a variety of blending factors. When you enable blending in the graphics pipeline, the rendering algorithm looks like this:
void renderPixel(x, y, z, color) {
if (z < zbuffer[x][y]) {
zbuffer[x][y] = z;
colorbuffer[x][y] = (colorbuffer[x][y] * percent1) + (color * percent2);
}
}
The color in the color buffer is called the “destination color”. The color of the object to be rendered is called the “source color”. And the percentage values are called “factors”. So the highlighted equation in the above pseudocode becomes:
colorbuffer[x][y] = (colorbuffer[x][y] * dst_factor) + (color * src_factor);
where dst_factor
and src_factor
are each 3-component factors and
the multiplication is component-wise. For example, if the color
is (0.2, 0.3, 0.4) and the src_factor
is (0.5, 0.6, 0.7), then the result of
the multiplication color * src_factor
is (0.10, 0.18, 0.28). Hopefully
is it obvious, but when you see src
it refers to the “source color”
or the “source factor”. Likewise, dst
refers to the “destination color”
or the “destination factor”.
I don’t like the names “source” and “destination”, but we need to use them so you will understand the constants that are used to specify the percentages. You don’t specify the factors explicitly; you specify an equation for calculating the factors from the color values. We will use the following names for the components of the color values:
colorbuffer[x][y] --> (dst_red, dst_green, dst_blue, dst_alpha)
color --> (src_red, src_green, src_blue, src_alpha)
You can select from the following equations for calculating a “factor”. Any
of these can be used for the dst_factor
and/or the src_factor
.
WebGL ENUM constant | Resulting factor | Alpha value |
---|---|---|
gl.ZERO | (0.0, 0.0, 0.0) | 0.0 |
gl.ONE | (1.0, 1.0, 1.0) | 1.0 |
gl.SRC_COLOR | (src_red, src_green, src_blue) | src_alpha |
gl.ONE_MINUS_SRC_COLOR | (1 - src_red, 1 - src_green, 1 - src_blue) | 1 - src_alpha |
gl.DST_COLOR | (dst_red, dst_green, dst_blue) | dst_alpha |
gl.ONE_MINUS_DST_COLOR | (1 - dst_red, 1- dst_green, 1- dst_blue) | 1 - dst_alpha |
gl.SRC_ALPHA | (src_alpha, src_alpha, src_alpha) | src_alpha |
gl.ONE_MINUS_SRC_ALPHA | (1 - src_alpha, 1- src_alpha, 1 - src_alpha) | 1 - src_alpha |
gl.DST_ALPHA | (dst_alpha, dst_alpha, dst_alpha) | dst_alpha |
gl.ONE_MINUS_DST_ALPHA | (1 - dst_alpha, 1 - dst_alpha, 1 - dst_alpha) | 1 - dst_alpha |
gl.CONSTANT_COLOR | (constant_red, constant_green, constant_blue) | constant_alpha |
gl.ONE_MINUS_CONSTANT_COLOR | (1 - constant_red, 1 - constant_green, 1 - constant_blue) | 1 - constant_alpha |
gl.CONSTANT_ALPHA | (constant_alpha, constant_alpha, constant_alpha) | constant_alpha |
gl.ONE_MINUS_CONSTANT_ALPHA | (1 - constant_alpha, 1 - constant_alpha, 1- constant_alpha) | 1 - constant_alpha |
gl.SRC_ALPHA_SATURATE | a = min(src_alpha, 1 - dst_alpha); (a,a,a) | 1.0 |
You set the blending factors in JavaScript using a call to blendFunc
like this:
gl.blendFunc(enum src_factor, enum dst_factor);
For the factors that use a constant color, you set that color using this function:
void glBlendColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha);
To complicate things further, you can also change the addition of the colors
to subtraction using the blendEquation
function. The three options are:
gl.blendEquation(gl.FUNC_ADD);
gl.blendEquation(gl.FUNC_SUBTRACT);
gl.blendEquation(gl.FUNC_REVERSE_SUBTRACT);
which makes the pipeline’s calculation be one of:
colorbuffer[x][y] = (colorbuffer[x][y] * dst_factor) + (color * src_factor);
colorbuffer[x][y] = (colorbuffer[x][y] * dst_factor) - (color * src_factor);
colorbuffer[x][y] = (color * src_factor) - (colorbuffer[x][y] * dst_factor);
To add even more complexity, you can separate the blending of the color components from the blending of the alpha values. If you use the functions:
gl.blendFunc(enum src_factor, enum dst_factor);
gl.blendEquation(enum equation_mode);
then the color components and the alpha values are treated the same way. If you use the functions:
gl.blendFuncSeparate(enum src_factor, enum dst_factor, enum src_alpha, enum dst_alpha);
gl.blendEquationSeparate(enum equation_rgb_mode, enum equation_alpha_mode);
then the color components and the alpha values are treated separately. All of these options can be very confusing, so let’s put it all in pseudocode to attempt to make it clearer. (Remember that this is implemented inside the graphics pipeline. You can’t change this implementation and you can’t implement this functionality in your fragment shader either.)
vec3 getColorFactor(mode, src_color, dst_color, constant_color) {
switch (mode) {
case gl.ZERO: factor = (0.0, 0.0, 0.0);
case gl.ONE: factor = (1.0, 1.0, 1.0);
case gl.SRC_COLOR: factor = ( src_color[0], src_color[1], src_color[2]);
case gl.ONE_MINUS_SRC_COLOR: factor = (1.0-src_color[0], 1.0-src_color[1], 1.0-src_color[2]);
case gl.DST_COLOR: factor = ( dst_color[0], dst_color[1], dst_color[2]);
case gl.ONE_MINUS_DST_COLOR: factor = (1.0-dst_color[0], 1.0-dst_color[1], 1.0-dst_color[2]);
case gl.SRC_ALPHA: factor = ( src_color[3], src_color[3], src_color[3]);
case gl.ONE_MINUS_SRC_ALPHA: factor = (1.0-src_color[3], 1.0-src_color[3], 1.0-src_color[3]);
case gl.DST_ALPHA: factor = ( dst_color[3], dst_color[3], dst_color[3]);
case gl.ONE_MINUS_DST_ALPHA: factor = (1.0-dst_color[3], 1.0-dst_color[3], 1.0-dst_color[3]);
case gl.CONSTANT_COLOR: factor = (constant_color[0], constant_color[1], constant_color[2]);
case gl.ONE_MINUS_CONSTANT_COLOR: factor = (1.0-constant_color[0], 1.0-constant_color[1], 1.0-constant_color[2]);
case gl.CONSTANT_ALPHA: factor = (constant_color[3], constant_color[3], constant_color[3]);
case gl.ONE_MINUS_CONSTANT_ALPHA: factor = (1.0-constant_color[3], 1.0-constant_color[3], 1.0-constant_color[3]);
case gl.SRC_ALPHA_SATURATE: a = min(src_color[3], 1.0-dst_color[3]);
factor = (a,a,);
}
return factor;
}
vec3 getAlphaFactor(mode, src_color, dst_color, constant_color) {
switch (mode) {
case gl.ZERO: alpha_factor = 0.0;
case gl.ONE alpha_factor = 1.0;
case gl.SRC_COLOR alpha_factor = src_color[3];
case gl.ONE_MINUS_SRC_COLOR alpha_factor = 1.0-src_color[3]);
case gl.DST_COLOR alpha_factor = dst_color[3];
case gl.ONE_MINUS_DST_COLOR alpha_factor = 1.0-dst_color[3];
case gl.SRC_ALPHA alpha_factor = src_color[3];
case gl.ONE_MINUS_SRC_ALPHA alpha_factor = 1.0-src_color[3];
case gl.DST_ALPHA alpha_factor = dst_color[3];
case gl.ONE_MINUS_DST_ALPHA alpha_factor = 1.0-dst_color[3];
case gl.SRC_ALPHA_SATURATE alpha_factor = 1.0;
}
return alpha_factor;
}
void renderPixel(x, y, z, color) {
if (z < zbuffer[x][y]) {
zbuffer[x][y] = z;
dst_color = colorbuffer[x][y];
src_color = color;
dst_factor[0,1,2] = getColorFactor(dst_mode, src_color, dst_color, constant_color);
dst_factor[3] = getAlphaFactor(dst_mode, src_color, dst_color, constant_color);
src_factor[0,1,2] = getColorFactor(src_mode, src_color, dst_color, constant_color);
src_factor[3] = getAlphaFactor(src_mode, src_color, dst_color, constant_color);
switch (blendEquation) {
case gl.FUNC_ADD: dst_color = dst_color * dst_factor + src_color * src_factor;
case gl.FUNC_SUBTRACT: dst_color = dst_color * dst_factor - src_color * src_factor;
case gl.FUNC_REVERSE_SUBTRACT: dst_color = src_color * src_factor - dst_color * dst_factor;
}
colorbuffer[x][y] = dst_color;
}
}
Experimentation 4 (Alpha Blending Percentages)¶
Please experiment with the following WebGL demonstration program by selecting various combinations of blending factors.
/**
* TransparencyExample4Render.js, By Wayne Brown, Spring 2016
*/
/**
* The MIT License (MIT)
*
* Copyright (c) 2015 C. Wayne Brown
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
"use strict";
var console, Learn_webgl_point4;
//-------------------------------------------------------------------------
// Build, create, copy and render 3D objects specific to a particular
// model definition and particular WebGL shaders.
//-------------------------------------------------------------------------
window.TransparencyExample4Render = function (learn, vshaders_dictionary,
fshaders_dictionary, models, controls) {
// Private variables
var self = this;
var canvas_id = learn.canvas_id;
var out = learn.out;
var gl = null;
var program = null;
var a_sphere = null;
var number_spheres = 10;
var number_transparent_spheres = 5;
var number_opaque_spheres = number_spheres - number_transparent_spheres;
var all_spheres = new Array(number_spheres);
var matrix = new window.Learn_webgl_matrix();
var transform = matrix.create();
var view_model_transform = matrix.create();
var projection = matrix.createPerspective(45.0, 1.0, 0.1, 100.0);
var camera = matrix.create();
var scale_matrix = matrix.create();
var translate_matrix = matrix.create();
var eye_x;
var eye_y;
var eye_z;
// Public variables that will possibly be used or changed by event handlers.
self.canvas = null;
self.angle_x = 0.0;
self.angle_y = 0.0;
self.scale = 1.0;
self.animate_active = false;
// Light model
var P4 = new window.Learn_webgl_point4();
var V = new window.Learn_webgl_vector3();
self.light_position = P4.create(10, 10, 10, 1);
self.light_color = V.create(1, 1, 1); // white light
self.shininess = 30;
self.ambient_color = V.create(0.2, 0.2, 0.2); // low level white light
var transformed_center = P4.create();
var sort_indexes = null;
self.src_factor = null;
self.dst_factor = null;
//-----------------------------------------------------------------------
/**
* Initialize the sort_indexes array for sorting the transparent models.
* This array is re-sorted before each render of the scene.
* @private
*/
function _initialize_sorting() {
var j;
sort_indexes = new Array(number_transparent_spheres);
for (j = 0; j < number_transparent_spheres; j += 1) {
sort_indexes[j] = [j, 0.0]; // [index to triangle, distance from camera]
}
}
/** ---------------------------------------------------------------------
* Update the distance from each transparent sphere to the camera. This
* uses the center of the sphere for the distance, which causes problems
* if two transparent spheres overlap in 3D space.
* @private
*/
function _update_distances_to_camera () {
var j, index, position;
for (j = 0; j < number_transparent_spheres; j += 1) {
index = sort_indexes[j][0];
position = all_spheres[index].position;
matrix.translate(translate_matrix, position[0], position[1], position[2]);
// Combine the camera and sphere translation transforms.
matrix.multiplySeries(view_model_transform, camera, translate_matrix);
// Calculate where the sphere is in relationship to the camera.
matrix.multiply(transformed_center, view_model_transform, position);
// Set this model's distance from the camera
sort_indexes[j][1] = transformed_center[2];
}
}
/** ---------------------------------------------------------------------
* Sort the models, back to front, based on their distance from the camera.
* @private
*/
function _sort_spheres () {
var j, k, temp;
// Perform an insertion sort on the models.
for (j = 0; j < number_transparent_spheres; j += 1) {
temp = sort_indexes[j];
k = j - 1;
while (k >= 0 && sort_indexes[k][1] > temp[1]) {
sort_indexes[k + 1] = sort_indexes[k];
k -= 1;
}
sort_indexes[k + 1] = temp;
}
}
//-----------------------------------------------------------------------
self.render = function () {
var j, size, position, x_radians, y_radians, scale, index;
// Clear the entire canvas window background with the clear color
// out.display_info("Clearing the screen");
gl.depthMask(true);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Build the camera transform to circle the origin.
x_radians = matrix.toRadians(self.angle_x);
y_radians = matrix.toRadians(self.angle_y);
scale = Math.cos(x_radians);
eye_x = Math.cos(y_radians) * scale * 20.0;
eye_z = Math.sin(y_radians) * scale * 20.0;
eye_y = Math.sin(x_radians) * 20.0;
matrix.lookAt(camera, eye_x, eye_y, eye_z, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Set the location of the light source
gl.uniform3f(program.u_Light_position, self.light_position[0],
self.light_position[1],
self.light_position[2]);
gl.uniform3fv(program.u_Light_color, self.light_color);
gl.uniform3fv(program.u_Ambient_color, self.ambient_color);
gl.uniform1f(program.u_Shininess, self.shininess);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Render the opaque spheres first.
gl.disable(gl.BLEND);
for (j = number_transparent_spheres; j < number_spheres; j += 1) {
size = all_spheres[j].size;
position = all_spheres[j].position;
matrix.scale(scale_matrix, size, size, size);
matrix.translate(translate_matrix, position[0], position[1], position[2]);
// Combine the transforms into a single transformation
matrix.multiplySeries(view_model_transform, camera,
translate_matrix, scale_matrix);
matrix.multiplySeries(transform, projection, view_model_transform);
// Assign a color to the sphere. Make it red if it is the selected object.
a_sphere.setColor(all_spheres[j].color);
a_sphere.render(transform, view_model_transform);
}
// Sort the transparent models, back to front, in reference to the camera
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Render the transparent spheres in sorted order.
// Enable alpha blending and set the percentage blending factors
gl.enable(gl.BLEND);
gl.blendFunc(self.src_factor, self.dst_factor);
// Turn off updating of the z-buffer
gl.depthMask(false);
// Update the distance of each sphere from the camera
_update_distances_to_camera();
// Sort the models based on their distance from the camera. The center
// point of each sphere is used for the distance.
_sort_spheres();
// Render the transparent spheres
for (j = 0; j < number_transparent_spheres; j += 1) {
index = sort_indexes[j][0];
size = all_spheres[index].size;
position = all_spheres[index].position;
matrix.scale(scale_matrix, size, size, size);
matrix.translate(translate_matrix, position[0], position[1], position[2]);
// Combine the transforms into a single transformation
matrix.multiplySeries(view_model_transform, camera,
translate_matrix, scale_matrix);
matrix.multiplySeries(transform, projection, view_model_transform);
// Set the sphere's color.
a_sphere.setColor(all_spheres[index].color);
a_sphere.render(transform, view_model_transform);
}
};
//-----------------------------------------------------------------------
self.delete = function () {
// Clean up shader programs
gl.deleteShader(program.vShader);
gl.deleteShader(program.fShader);
gl.deleteProgram(program);
program = null;
// Delete each model's VOB
a_sphere.delete();
// Delete all of the events associated with this canvas
events.removeAllEventHandlers();
// Disable any animation
self.animate_active = false;
};
//-----------------------------------------------------------------------
// Object constructor. One-time initialization of the scene.
// Get the rendering context for the canvas
self.canvas = learn.getCanvas(canvas_id);
if (self.canvas) {
gl = learn.getWebglContext(self.canvas);
}
if (!gl) {
return;
}
// Set up the rendering program and set the state of webgl
program = learn.createProgram(gl, vshaders_dictionary.shader41, fshaders_dictionary.shader41);
gl.useProgram(program);
gl.enable(gl.DEPTH_TEST);
gl.clearColor(0.9, 0.9, 0.9, 1.0);
self.src_factor = gl.SRC_ALPHA;
self.dst_factor = gl.ONE_MINUS_SRC_ALPHA;
// Create Vertex Object Buffers for the models
a_sphere = new window.Learn_webgl_model_render_41(gl, program,
models.Icosphere, out);
// Create a set of random positions, colors, and sizes for a group of spheres.
var j, position_x, position_y, position_z, color;
for (j = 0; j < number_spheres; j += 1) {
position_x = Math.random() * 10 - 5.0;
position_y = Math.random() * 10 - 5.0;
position_z = Math.random() * 10 - 5.0;
if (j < number_transparent_spheres) {
color = new Float32Array([Math.random(), Math.random(), Math.random(), Math.random()]);
} else {
color = new Float32Array([Math.random(), Math.random(), Math.random(), 1.0]);
}
all_spheres[j] = { position: P4.create(position_x, position_y, position_z),
size: Math.random() * 2.5,
color: color
};
}
// Set up callbacks for user and timer events
var events;
events = new window.TransparencyExample4Events(self, controls, gl);
events.animate();
// Initialize the array of model indexes that will be used for sorting.
_initialize_sorting();
};
Alpha blending experimentation.
Animate
Source blending factor (Percentage of the object's color to render.) |
Destination blending factor (Percentage of color already in the color buffer) |
gl.ZERO gl.ONE gl.SRC_COLOR gl.ONE_MINUS_SRC_COLOR gl.DST_COLOR gl.ONE_MINUS_DST_COLOR gl.SRC_ALPHA gl.ONE_MINUS_SRC_ALPHA gl.DST_ALPHA gl.ONE_MINUS_DST_ALPHA gl.CONSTANT_COLOR gl.ONE_MINUS_CONSTANT_COLOR gl.CONSTANT_ALPHA gl.ONE_MINUS_CONSTANT_ALPHA gl.SRC_ALPHA_SATURATE |
gl.ZERO gl.ONE gl.SRC_COLOR gl.ONE_MINUS_SRC_COLOR gl.DST_COLOR gl.ONE_MINUS_DST_COLOR gl.SRC_ALPHA gl.ONE_MINUS_SRC_ALPHA gl.DST_ALPHA gl.ONE_MINUS_DST_ALPHA gl.CONSTANT_COLOR gl.ONE_MINUS_CONSTANT_COLOR gl.CONSTANT_ALPHA gl.ONE_MINUS_CONSTANT_ALPHA |
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); |
Summary¶
Simple visual effects related to transparency can be achieved using alpha blending. Accurate rendering of transparent models that intersect in 3D space requires a combination of techniques that involve the definition of the models, sorting, and configuration of the graphics pipeline. You would typically implement the minimum functionality needed to achieve the results your require for a particular scene.
Glossary¶
- transparency
- Some of the light that strikes an object passes through the object and surfaces behind the object are partially visible.
- opaque
- All light that strikes a surface is reflected. Opaque means no transparency.
- insertion sort algorithm
- The fastest, general purpose algorithm for sorting data that is already close to being sorted.
- index sort
- A set of data values is sorted without ever moving the data. The sort order is described as an array of indexes into the array that holds the data.
- destination color
- A color value stored in the color buffer of the frame buffer.
- source color
- A color value to be rendered for a surface.
- alpha blending
- The color of a pixel is calculated as a combination of two colors: the the destination color and the source color.