跳转至

6.2 第一个例子

First Examples

我们已经准备好开始我们的第一批WebGL程序。本节首先介绍一些关于WebGL图形上下文的更多细节,然后是对GLSL(WebGL着色器的编程语言)的简短介绍。有了这些知识,我们可以转向标准的第一个示例:RGB颜色三角形。

We are ready to start working towards our first WebGL programs. This section begins with a few more details about the WebGL graphics context, followed by a short introduction to GLSL, the programming language for WebGL shaders. With that in hand, we can turn to the standard first example: the RGB color triangle.

6.2.1 WebGL 上下文选项

WebGL Context Options

6.1.1小节中我们看到了,WebGL图形上下文是通过函数canvas.getContext创建的,其中canvas是对将要绘制图形上下文的<canvas>元素的引用。这个函数接受一个可选的第二个参数,该参数可以用来设置图形上下文中某些选项的值。只有当你想要为至少一个选项设置非默认值时,才需要第二个参数。该参数是一个JavaScript对象,其属性是选项的名称。下面是一个带选项的上下文创建示例:

let options = {
    alpha: false,
    depth: false
};
gl = canvas.getContext("webgl", options);  // 或者 "webgl2"

所有选项都是布尔值。我将在这里讨论最有用的几个:

alpha — 决定绘图缓冲区是否有alpha分量。这是整个图像画布的alpha分量。如果有alpha分量,那么画布上的像素就可以是透明的或半透明的,允许背景(在画布后面的网页)透过来。默认值是true。如果你想让画布完全不透明,将值设置为false是安全的。将值设置为false不会阻止你进行绘图颜色与图像颜色的alpha混合;RGB颜色分量仍然可以通过混合计算出来。然而,只有在你的程序输出alpha分量小于1.0的像素,并且你不希望图像与画布的背景混合时,设置为false才有必要。(但请注意,具有alpha分量的图形上下文可能处理得更高效,因为网页使用RGBA颜色进行显示。)

depth — 决定是否分配深度缓冲区。默认值是true。只有在启用深度测试时才需要深度缓冲区。深度缓冲区通常对2D图形不是必需的。如果你的应用程序不需要它,消除深度缓冲区可以节省GPU中的一些内存。

antialias — 用于请求对图像应用反锯齿。WebGL实现可能会忽略请求,例如如果GPU不支持反锯齿。默认值是true。反锯齿可以提高图像质量,但它也可能显著增加计算时间。

preserveDrawingBuffer — 决定在图像被复制到网页后,是否丢弃绘图缓冲区的内容。默认值是false。绘图缓冲区是WebGL内部的。只有当Web浏览器将图像复制到网页上时,它的内容才会在屏幕上变得可见。preserveDrawingBuffer的默认值意味着一旦发生这种情况,WebGL可以丢弃它自己的图像副本,这允许GPU释放资源以供其他操作使用。只要你的渲染函数每次被调用时都完全重绘图像,默认值就可以了。只有在你需要保留图像以便随着时间推移逐步添加内容时,才应该将值设置为true。

We saw in Subsection 6.1.1 that a WebGL graphics context is created by the function canvas.getContext, where canvas is a reference to the <canvas> element where the graphics context will draw. This function takes an optional second parameter that can be used to set the value of certain options in the graphics context. The second parameter is only needed if you want to give a non-default value to at least one of the options. The parameter is a JavaScript object whose properties are the names of the options. Here is an example of context creation with options:

let options = {
    alpha: false,
    depth: false
};
gl = canvas.getContext( "webgl", options );  // (or "webgl2")

All of the options are boolean-valued. I will discuss the most useful ones here:

alpha — determines whether the drawing buffer has an alpha component. This is the alpha component for the image canvas as a whole. If there is an alpha component, then it is possible for pixels in the canvas to be transparent or translucent, letting the background (on the web page behind the canvas) show through. The default value is true. It is safe to set the value to false, if you want the canvas to be fully opaque. Setting it to false does not stop you from doing alpha blending of the drawing color with the image color; the RGB color components can still be computed by blending. However, setting the value to false is only necessary if your program outputs pixels with alpha component less than 1.0, and you don't want your image to blend with the background of the canvas. (Note however that a graphics context with an alpha component might be handled more efficiently, because web pages use RGBA colors for their display.)

depth — determines whether a depth buffer is allocated. The default value is true. You only need a depth buffer if you enable the depth test. The depth buffer is generally not needed for 2D graphics. If your application doesn't need it, eliminating the depth buffer can save some memory in the GPU.

antialias — is used to request that antialiasing be applied to the image. A WebGL implementation might ignore the request, for example if antialiasing is not supported by the GPU. The default value is true. Antialiasing can improve the quality of an image, but it can also significantly increase the computation time.

preserveDrawingBuffer — determines whether the contents of the drawing buffer are discarded after the image has been copied to the web page. The default value is false. The drawing buffer is internal to WebGL. Its contents only become visible on the screen when the web browser copies the image onto the web page. The default value for preserveDrawingBuffer means that once that happens, WebGL can discard its own copy of the image, which allows the GPU to free up resources for other operations. As long as your rendering functions completely redraw the image every time they called, the default is fine. You should set the value to true only if you need to keep the image around so that you can add to it incrementally over time.

6.2.2 GLSL 简介

A Bit of GLSL

下一节将更全面地介绍GLSL。但你将需要了解一些关于这门语言的知识,以理解本节中的示例。本节仅讨论GLSL ES 1.00,但请记住,这门语言可以与WebGL 1.0和WebGL 2.0一起使用。

顶点或片段着色器可以包含全局变量声明、类型定义和函数定义。其中一个函数必须是main(),这是着色器的入口点;也就是说,它是GPU处理顶点或片段时调用的函数。main()例程不接受任何参数,也不返回值,所以它的形式是

void main() {
    .
    .
    .
}

(或者,它可以被声明为void main(void)。)

控制结构是有限的。if语句与C或Java中的格式相同。但对for循环语法施加了一些限制,不允许使用whiledo...while循环。数据结构包括数组和structs,也有一些限制。我们将在下一节中详细介绍所有这些。

GLSL的优势在于其内置的数据类型和用于处理向量和矩阵的函数。在本节中,我们只需要数据类型floatvec2vec3vec4。这些类型分别表示1、2、3或4个浮点数。变量声明与C类似。一些示例是:

attribute vec3 a_coords;  // (仅在顶点着色器中)
vec3 rgb;
float width, height;
uniform vec2 u_size;
varying vec4 v_color;

Attributeuniformvarying变量在第6.1节中讨论过。它们用于在JavaScript与着色器程序之间以及顶点着色器与片段着色器之间进行通信。在上面的示例中,我在变量名中使用了前缀"a_"、"u_"和"v_",但这不是必须的。

通常,我们会从单独的数字或较短的向量构造向量的值。GLSL具有灵活的符号表示法来实现这一点。使用上述声明的变量,我们可以编写

rgb = vec3(1.0, 0.7, 0.0);  // 从常数构造vec3
v_color = vec4(rgb, 1.0);  // 从vec3和常数构造vec4
gl_Position = vec4(a_coords, 0.0, 1.0);  // 从vec2和2个常数构造vec4

在最后一个赋值语句中,gl_Position是特殊内置变量,用于在顶点着色器中给出顶点的坐标。gl_Position是vec4类型,需要四个数字,因为坐标是以齐次坐标3.5.3小节指定的。片段着色器中的特殊变量gl_FragCoord也是vec4类型,给出像素的坐标作为齐次坐标。而gl_FragColor是vec4类型,给出像素的四个RGBA颜色分量。

顶点着色器至少需要一个属性来给出顶点的坐标。对于2D绘图,很自然地该属性是vec2类型。如果我们假设属性的值已经用裁剪坐标表示,那么顶点着色器的完整源代码可能非常简单:

attribute vec2 coords;
void main() {
    gl_Position = vec4(coords, 0.0, 1.0);
}

对于相应的最小片段着色器,我们可能只是简单地将所有内容绘制为黄色。

precision mediump float;
void main() {
    gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}

这段片段着色器中的第一行看起来有些奇怪,还没有解释,但需要类似的语句。它将在下一节中解释。

The next section will cover GLSL more thoroughly. But you will need to know something about the language to understand the examples in this section. This section discusses GLSL ES 1.00 only, but remember that that language can be used with both WebGL 1.0 and WebGL 2.0.

A vertex or fragment shader can contain global variable declarations, type definitions, and function definitions. One of the functions must be main(), which is the entry point for the shader; that is, it is the function that is called by the GPU to process the vertex or fragment. The main() routine takes no parameters and does not return a value, so it takes the form

void main() {
    .
    .
    .
}

(Alternatively, it can be declared as void main(void).)

Control structures are limited. If statements take the same form as in C or Java. But some limitations are placed on the for loop syntax, and while and do...while loops are not allowed. Data structures include arrays and structs, again with some limitations. We will cover all this in some detail in the next section.

GLSL's strength lies in its built-in data types and functions for working with vectors and matrices. In this section, we will only need the data types float, vec2, vec3, and vec4. These types represent, respectively, 1, 2, 3, or 4 floating point numbers. Variable declarations are similar to C. Some examples are:

attribute vec3 a_coords;  // (only in vertex shader)
vec3 rgb;
float width, height;
uniform vec2 u_size;
varying vec4 v_color;

Attribute, uniform, and varying variables were discussed in Section 6.1. They are used for communication between JavaScript and the shader program and between the vertex shader and the fragment shader. In the above examples, I used the prefixes "a_", "u_", and "v_" in the names of the variables, but that is not required.

It is common to construct a value for a vector from individual numbers or from shorter vectors. GLSL has a flexible notation for doing this. Using the variables declared in the above examples, we can write

rgb = vec3( 1.0, 0.7, 0.0 );  // construct a vec3 from constants
v_color = vec4( rgb, 1.0 );  // construct a vec4 from a vec3 and a constant
gl_Position = vec4( a_coords, 0.0, 1.0 );  // vec4 from a vec2 and 2 constants

In the last assignment statement, gl_Position is the special built-in variable that is used in the vertex shader to give the coordinates of the vertex. gl_Position is of type vec4, requiring four numbers, because the coordinates are specified as homogeneous coordinates (Subsection 3.5.3). The special variable gl_FragCoord in the fragment shader is also a vec4, giving the coordinates of the pixel as homogeneous coordinates. And gl_FragColor is a vec4, giving the four RGBA color components for the pixel.

A vertex shader needs, at a minimum, an attribute to give the coordinates of the vertex. For 2D drawing, it's natural for that attribute to be of type vec2. If we assume that the values for the attribute are already expressed in clip coordinates, then the complete source code for the vertex shader could be as simple as:

attribute vec2 coords;
void main() {
    gl_Position = vec4( coords, 0.0, 1.0 );
}

For a corresponding minimal fragment shader, we might simply draw everything in yellow.

precision mediump float;
void main() {
    gl_FragColor = vec4( 1.0, 1.0, 0.0, 1.0 );  
}

The strange first line in this fragment shader has not been explained, but something like it is required. It will be explained in the next section.

6.2.3 WebGL 中的 RGB 三角形

The RGB Triangle in WebGL

我们已经准备好查看我们的第一个完整的WebGL示例,它将绘制一个常见的RGB颜色三角形,如下所示:

123

源代码可以在 webgl/webgl-rgb-triangle.html 中找到。该代码包括了在6.1.1小节6.1.2小节中讨论的通常的init()createProgram()函数,只是我已关闭了WebGL上下文中的“alpha”和“depth”选项。我将不再进一步讨论这两个函数。

该示例使用类型为vec2的属性来指定三角形顶点的坐标。在默认的WebGL坐标系中,坐标范围从-1到1。对于三角形,我使用的顶点坐标在该范围内,因此不需要坐标变换。由于三角形的每个顶点处的颜色都不同,顶点颜色也是一个属性。由于此程序不需要alpha分量,因此我使用类型为vec3的属性来表示顶点颜色。

三角形内部像素的颜色是通过插值顶点处的颜色来确定的。插值意味着我们需要一个变化变量来表示颜色。变化变量在顶点着色器中被赋值,并在片段着色器中使用其值。

看起来我们需要两个颜色变量:一个属性和一个变化变量。我们不能将同一个变量用于两个目的。属性将顶点颜色从JavaScript传入顶点着色器;变化变量将颜色从顶点着色器传入片段着色器。在这种情况下,从顶点着色器传出的颜色值与传入的值相同,因此着色器只需要将颜色属性的值复制到变化变量中。这种模式实际上相当常见。以下是顶点着色器:

attribute vec2 a_coords;
attribute vec3 a_color;
varying vec3 v_color;

void main() {
    gl_Position = vec4(a_coords, 0.0, 1.0);
    v_color = a_color;
}

片段着色器只需要将来自变化变量的传入颜色值复制到gl_FragColor中,后者指定了片段的颜色:

precision mediump float;
varying vec3 v_color;

void main() {
    gl_FragColor = vec4(v_color, 1.0);
}

为了编译着色器程序,着色器的源代码必须在JavaScript字符串中。在这种情况下,我通过连接代表代码各行的常量字符串来构造字符串。例如,片段着色器源代码作为全局变量包含在JavaScript脚本中:

const fragmentShaderSource =
            "precision mediump float;\n" +
            "varying vec3 v_color;\n" +
            "void main() {\n" +
            "   gl_FragColor = vec4(v_color, 1.0);\n" +
            "}\n";

每行末尾的换行符"\n"不是必需的,但它允许GLSL编译器在其生成的任何错误消息中包含一个有意义的行号。

在JavaScript方面,我们还需要一个全局变量来表示WebGL上下文。我们还需要为属性变量提供值。在6.1.5小节中讨论了一个相当复杂的过程。我们需要全局变量来表示着色器程序中每个属性的位置,以及表示将保存属性值的VBO。我使用以下变量:

let gl;  // WebGL图形上下文。

let attributeCoords;  // 属性 "a_coords" 的位置。
let bufferCoords;     // 一个顶点缓冲对象,用于保存 a_coords 的值。

let attributeColor;   // 属性 "a_color" 的位置。
let bufferColor;      // 一个顶点缓冲对象,用于保存 a_color 的值。

图形上下文是在init()函数中创建的。其他变量在从init()调用的initGL()函数中初始化。该函数还使用6.1.2小节中的createProgram()函数创建着色器程序:

function initGL() {
    let prog = createProgram(gl, vertexShaderSource, fragmentShaderSource);
    gl.useProgram(prog);

    attributeCoords = gl.getAttribLocation(prog, "a_coords");
    bufferCoords = gl.createBuffer();

    attributeColor = gl.getAttribLocation(prog, "a_color");
    bufferColor = gl.createBuffer();
}

要设置属性的值,我们需要六个不同的JavaScript命令(如果还要计算将属性值放入类型化数组,需要更多的命令)。getAttribLocationcreateBuffer命令很可能只需为每个属性调用一次,所以我将它们放在我的初始化程序中。其他四个命令在draw()中,这个函数用于绘制图像。在这个程序中,draw()只被调用一次,所以将代码分成两个函数并不是真的必要,但通常,绘制函数旨在被多次调用。(每次调用draw()时都创建一个新的VBO将是一个特别糟糕的主意!)

在绘制三角形之前,draw()函数用黑色背景填充画布。这是使用WebGL函数gl.clearColorgl.clear完成的,它们与OpenGL 1.1函数glClearColorglClear具有完全相同的功能。以下是代码:

function draw() { 

    gl.clearColor(0,0,0,1);  // 指定用于清除的颜色
    gl.clear(gl.COLOR_BUFFER_BIT);  // 清除画布(变为黑色)

    /* 设置 "a_coords" 属性的值 */

    let coords = new Float32Array( [-0.9,-0.8, 0.9,-0.8, 0,0.9] );

    gl.bindBuffer(gl.ARRAY_BUFFER, bufferCoords);
    gl.bufferData(gl.ARRAY_BUFFER, coords, gl.STREAM_DRAW);
    gl.vertexAttribPointer(attributeCoords, 2, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(attributeCoords); 

    /* 设置 "a_color" 属性的值 */

    let color = new Float32Array( [0,0,1, 0,1,0, 1,0,0] );

    gl.bindBuffer(gl.ARRAY_BUFFER, bufferColor);
    gl.bufferData(gl.ARRAY_BUFFER, color, gl.STREAM_DRAW);
    gl.vertexAttribPointer(attributeColor, 3, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(attributeColor); 

    /* 绘制三角形 */

    gl.drawArrays(gl.TRIANGLES, 0, 3);

}

在这个函数中,变量coords包含顶点着色器中名为"a_coords"的属性的值。该属性表示顶点的x和y坐标。由于属性是vec2类型,每个顶点需要两个数字。coords的值是在这里使用带普通JavaScript数组作为参数的Float32Array构造函数创建的;JavaScript数组中的值被复制到新创建的类型化数组中。类似地,变量color包含顶点着色器中"a_color"属性的值,每个顶点有三个数字。

现在我们已经解释了RGB三角形程序的所有部分。阅读完整的源代码以了解它是如何组合在一起的。

We are ready to look at our first full WebGL example, which will draw the usual RGB color triangle, as shown here:

123

The source code can be found in webgl/webgl-rgb-triangle.html. The code includes the usual init() and createProgram() functions as discussed in Subsection 6.1.1 and Subsection 6.1.2, except that I have turned off the "alpha" and "depth" options in the WebGL context. I won't discuss those two functions further.

The example uses an attribute of type vec2 to specify the coordinates of the vertices of the triangle. Coordinates range from −1 to 1 in the default WebGL coordinate system. For the triangle, the vertex coordinates that I use are in that range, so no coordinate transformation is needed. Since the color is different at each vertex of the triangle, the vertex color is also an attribute. I use an attribute of type vec3 for the vertex colors, since no alpha component is needed in this program.

The color of interior pixels in the triangle is interpolated from the colors at the vertices. The interpolation means that we need a varying variable to represent the color. A varying variable is assigned a value in the vertex shader, and its value is used in the fragment shader.

It looks like we need two color variables: an attribute and a varying variable. We can't use the same variable for both purposes. The attribute carries the vertex color from JavaScript into the vertex shader; the varying variable carries the color from the vertex shader to the fragment shader. In this case, the color value going out of the vertex shader is the same as the value coming in, so the shader just has to copy the value from the color attribute to the varying variable. This pattern is actually fairly common. Here is the vertex shader:

attribute vec2 a_coords;
attribute vec3 a_color;
varying vec3 v_color;

void main() {
gl_Position = vec4(a_coords, 0.0, 1.0);
v_color = a_color;
}

The fragment shader only has to copy the incoming color value from the varying variable into gl_FragColor, which specifies the outgoing color for the fragment:

precision mediump float;
varying vec3 v_color;

void main() {
gl_FragColor = vec4(v_color, 1.0);
}

In order to compile the shader program, the source code for the shaders has to be in JavaScript strings. In this case, I construct the strings by concatenating constant strings representing the individual lines of code. For example, the fragment shader source code is included in the JavaScript script as the global variable

const fragmentShaderSource =
            "precision mediump float;\n" +
            "varying vec3 v_color;\n" +
            "void main() {\n" +
            "   gl_FragColor = vec4(v_color, 1.0);\n" +
            "}\n";

The line feed character, "\n", at the end of each line is not required, but it allows the GLSL compiler to include a meaningful line number in any error message that it generates.

Also on the JavaScript side, we need a global variable for the WebGL context. And we need to provide values for the attribute variables. The rather complicated process was discussed in Subsection 6.1.5. We need global variables to represent the location of each attribute in the shader program, and to represent the VBOs that will hold the attribute values. I use the variables

let gl;  // The WebGL graphics context.

let attributeCoords;  // Location of the attribute named "a_coords".
let bufferCoords;     // A vertex buffer object to hold the values for a_coords.

let attributeColor;   // Location of the attribute named "a_color".
let bufferColor;      // A vertex buffer object to hold the values for a_color.

The graphics context is created in the init() function. The other variables are initialized in a function initGL() that is called from init(). That function also creates the shader program, using the createProgram() function from Subsection 6.1.2:

function initGL() {
    let prog = createProgram( gl, vertexShaderSource, fragmentShaderSource );
    gl.useProgram(prog);

    attributeCoords = gl.getAttribLocation(prog, "a_coords");
    bufferCoords = gl.createBuffer();

    attributeColor = gl.getAttribLocation(prog, "a_color");
    bufferColor = gl.createBuffer();
}

To set up the values for an attribute, we need six different JavaScript commands (and more if you count placing the attribute values into a typed array). The commands getAttribLocation and createBuffer will most likely be called just once for each attribute, so I put them in my initialization routine. The other four commands are in draw(), the function that draws the image. In this program, draw() is called just once, so the division of the code into two functions is not really necessary, but in general, a draw function is meant to be called many times. (It would be a particularly bad idea to create a new VBO every time draw() is called!)

Before drawing the triangle, the draw() function fills the canvas with a black background. This is done using the WebGL functions gl.clearColor and gl.clear, which have exactly the same functionality as the OpenGL 1.1 functions glClearColor and glClear. Here is the code:

function draw() { 

    gl.clearColor(0,0,0,1);  // specify the color to be used for clearing
    gl.clear(gl.COLOR_BUFFER_BIT);  // clear the canvas (to black)

    /* Set up values for the "a_coords" attribute */

    let coords = new Float32Array( [ -0.9,-0.8, 0.9,-0.8, 0,0.9 ] );

    gl.bindBuffer(gl.ARRAY_BUFFER, bufferCoords);
    gl.bufferData(gl.ARRAY_BUFFER, coords, gl.STREAM_DRAW);
    gl.vertexAttribPointer(attributeCoords, 2, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(attributeCoords); 

    /* Set up values for the "a_color" attribute */

    let color = new Float32Array( [ 0,0,1, 0,1,0, 1,0,0 ] );

    gl.bindBuffer(gl.ARRAY_BUFFER, bufferColor);
    gl.bufferData(gl.ARRAY_BUFFER, color, gl.STREAM_DRAW);
    gl.vertexAttribPointer(attributeColor, 3, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(attributeColor); 

    /* Draw the triangle. */

    gl.drawArrays(gl.TRIANGLES, 0, 3);

}

In this function, the variable coords contains values for the attribute named "a_coords" in the vertex shader. That attribute represents the x and y coordinates of the vertex. Since the attribute is of type vec2, two numbers are required for each vertex. The value for coords is created here with a Float32Array constructor that takes an ordinary JavaScript array as its parameter; the values from the JavaScript array are copied into the newly created typed array. Similarly, the variable color contains values for the "a_color" attribute in the vertex shader, with three numbers per vertex.

We have now accounted for all the pieces of the RGB triangle program. Read the complete source code to see how it fits together.

6.2.4 形状压模

Shape Stamper

我们的下一个示例将介绍一些新特性。这个示例是一个简单的交互式程序,用户可以通过点击画布放置形状。形状的属性来自一组弹出菜单。属性包括形状的颜色和透明度,以及绘制的几种可能形状中的哪一种。形状以用户点击的点为中心。

示例程序是 webgl/shape-stamper.html。这是一个程序的演示版本,你可以看到它的工作原理。

在RGB三角形示例中,color是一个属性,因为每个三角形原语的顶点都被分配了不同的颜色。在shape-stamper程序中,所有顶点,实际上所有像素,在原语中都有相同的颜色。这意味着颜色可以是一个统一变量。示例还允许透明度,所以颜色需要alpha分量以及RGB分量。在程序中将alpha和RGB分量作为单独的数量处理是方便的,所以我在着色器程序中将它们表示为两个单独的统一变量。颜色和alpha统一变量在片段着色器中用于分配片段的颜色。实际上,片段着色器只做这件事,所以完整的源代码如下:

precision mediump float;
uniform vec3 u_color;
uniform float u_alpha;
void main() {
    gl_FragColor = vec4(u_color, u_alpha);
}

要在JavaScript方面使用统一变量,我们需要知道它在着色器程序中的位置。程序使用命令在initGL()函数中获取两个统一变量的位置:

uniformColor = gl.getUniformLocation(prog, "u_color");
uniformAlpha = gl.getUniformLocation(prog, "u_alpha");

程序有两个弹出菜单,让用户选择用于绘制原语的颜色和alpha。绘制形状时,菜单中的值决定了统一变量的值:

let colorNumber = Number(document.getElementById("colorChoice").value);
let alpha = Number(document.getElementById("opacityChoice").value);

gl.uniform3fv(uniformColor, colorList[colorNumber]);
gl.uniform1f(uniformAlpha, alpha);

使用gl.uniform*系列函数设置统一变量的值。在这种情况下,colorList[colorNumber]是一个包含颜色的RGB颜色分量的三个数字数组,所以使用函数gl.uniform3fv来设置值:"3f"意味着提供了3个浮点值,"v"意味着这三个值在数组中。请注意,需要三个浮点值来匹配着色器中统一变量的类型,vec3alpha的值是一个单独的浮点数,所以使用gl.uniform1f设置相应的统一变量。

为了让颜色的alpha分量产生任何效果,必须启用alpha混合。这是通过初始化完成的,使用以下两个命令:

gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

第一行启用了alpha分量的使用。第二行告诉如何使用alpha分量。这里使用的"blendFunc"适用于2D中的透明度。在OpenGL 1.1的3.1.2小节中也使用了相同的命令。


当程序启动时,用户看到一个空白的白色画布。当用户点击画布时,会添加一个形状。当用户再次点击时,会添加第二个形状——第一个形状最好还在那里!然而,这不是WebGL的默认行为!

当用户点击画布时,会调用mousedown事件的事件处理程序函数。该函数中绘制了形状。当函数返回时,WebGL告诉网络浏览器图像已被修改,网络浏览器将新图像复制到屏幕上。一旦发生这种情况,正如本节前面讨论的,WebGL的默认行为是丢弃图像。但这意味着第二次鼠标点击是在空白画布上绘制,因为第一次鼠标点击的形状已被擦除。

为了解决这个问题,必须将WebGL图形上下文中的preserveDrawingBuffer选项设置为trueshape-stamper程序使用以下方式创建上下文:

let options = {  // 不需要alpha通道或深度缓冲区,但我们需要保留绘图缓冲区中的图像。
    alpha: false,
    depth: false,
    preserveDrawingBuffer: true
};
gl = canvas.getContext("webgl", options);

请注意,这个程序没有一个draw()函数来重新绘制整个图像。所有的绘制都在鼠标处理函数doMouseDown中完成。事情可以做得不同。程序可以使用数据结构来存储有关已绘制形状的信息。点击画布将向列表中添加一个项目,然后重新绘制整个图像,包括新形状。然而在实际程序中,图像中的内容的唯一记录就是图像本身。(用第1.1节的术语来说,它是一个绘画程序,而不是绘图程序。)


WebGL使用一个默认的坐标系统,其中每个坐标的范围是-1到1。当然,我们希望使用一个更方便的坐标系统,这意味着我们需要应用坐标变换,将我们使用的坐标转换为默认坐标系统。在shape-stamper程序中,自然坐标系统是画布上的像素坐标。在像素坐标系统中,x坐标从左侧的0到右侧的canvas.width,y坐标从顶部的0到底部的canvas.height。将像素坐标(x1,y1)转换为默认坐标(x2,y2)的方程是:

x2 = -1 + 2*( x1 / canvas.width );
y2 = 1 - 2*( y1 / canvas.height );

在WebGL中,坐标变换通常在顶点着色器中应用。在这种情况下,为了实现变换,顶点着色器只需要知道画布的宽度和高度。程序将宽度和高度作为统一变量提供给顶点着色器。顶点的原始像素坐标作为属性输入到顶点着色器。着色器应用坐标变换来计算gl_Position的值,该值必须以默认坐标系统表示。以下是顶点着色器源代码:

attribute vec2 a_coords;   // 像素坐标
uniform float u_width;     // 画布宽度
uniform float u_height;    // 画布高度
void main() {
    float x = -1.0 + 2.0*(a_coords.x / u_width);
    float y = 1.0 - 2.0*(a_coords.y / u_height);
    gl_Position = vec4(x, y, 0.0, 1.0);
}

变换可能比这更复杂,特别是在3D中,但一般模式保持不变:变换由统一变量表示,并在顶点着色器中应用。通常,变换以矩阵形式实现。我们将在后面看到,统一变量可以是矩阵,着色器语言GLSL对矩阵操作有良好的支持。

为了绘制一个形状,我们需要在Float32Array中存储该形状的像素坐标;然后,我们必须将该数组中的值加载到与“a_coords”属性相关联的缓冲区中;最后,我们必须调用gl.drawArrays进行实际绘制。形状的坐标可以根据正在绘制的形状类型以及用户点击的点来计算。例如,以下是创建圆形坐标数组的代码,其中x和y是被点击的点的像素坐标:

coords = new Float32Array(64);
k = 0;
for (let i = 0; i < 32; i++) {
    let angle = i/32 * 2*Math.PI;
    coords[k++] = x + 50*Math.cos(angle);  // 顶点i的x坐标
    coords[k++] = y + 50*Math.sin(angle);  // 顶点i的y坐标
}

圆被近似为一个32边的规则多边形,半径为50像素。每个顶点需要两个坐标,所以数组的长度是64。其他形状的代码类似。一旦数组被创建,使用以下代码绘制形状:

gl.bindBuffer(gl.ARRAY_BUFFER, bufferCoords);
gl.bufferData(gl.ARRAY_BUFFER, coords, gl.STREAM_DRAW);
gl.vertexAttribPointer(attributeCoords, 2, gl.FLOAT, false, 0, 0);

gl.drawArrays(gl.TRIANGLE_FAN, 0, coords.length/2);

在最后一行中,coords.length/2是形状中的顶点数,因为数组每个顶点保存两个数字。还要注意,gl.bufferData的最后一个参数是gl.STREAM_DRAW,当VBO中的数据只使用一次或几次后就被丢弃时,这是合适的。


尽管示例程序的演示版本具有相同的功能,但我在两个版本中以不同的方式实现了形状绘制。注意,程序中的所有圆都是相同的;它们只是位于不同的地点。应该可以在自己的对象坐标中绘制圆,然后应用建模变换将圆移动到场景中所需位置。这是我在程序的演示版本中采用的方法。

有四种形状:圆形、正方形、三角形和星形。在演示版本中,我为每种形状创建了一个单独的VBO。一个形状的VBO包含该形状在对象坐标中的顶点坐标,形状以(0,0)为中心。由于对象坐标永远不会改变,VBO可以一次性创建,并作为程序初始化的一部分。例如,使用以下代码创建圆的VBO:

coords = new Float32Array(64); 
let k = 0;  // 索引到coords数组
for (let i = 0; i < 32; i++) {
    let angle = i/32 * 2*Math.PI;
    coords[k++] = 50*Math.cos(angle);  // 顶点的x坐标
    coords[k++] = 50*Math.sin(angle);  // 顶点的y坐标
}

bufferCoordsCircle = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, bufferCoordsCircle );
gl.bufferData(gl.ARRAY_BUFFER, coords, gl.STATIC_DRAW);

注意最后一行中使用了gl.STATIC_DRAW。由于数据可以重用来绘制许多不同的圆,这是合适的。

要绘制一个中心位于(x,y)的形状,必须对VBO中的坐标应用平移。我在顶点着色器中添加了平移,并使用一个新的统一变量来表示平移量:

attribute vec2 a_coords;
uniform float u_width;
uniform float u_height;
uniform vec2 u_translation;
void main() {
    float x = -1.0 + 2.0*((a_coords.x + u_translation.x) / u_width);
    float y = 1.0 - 2.0*((a_coords.y + u_translation.y) / u_height);
    gl_Position = vec4(x, y, 0.0, 1.0);
}

你可能会觉得阅读演示示例程序的完整源代码是值得的。

Our next example will introduce a few new features. The example is a simple interactive program where the user can place shapes in a canvas by clicking the canvas with the mouse. Properties of the shape are taken from a set of popup menus. The properties include the color and degree of transparency of the shape, as well as which of several possible shapes is drawn. The shape is centered at the point where the user clicks.

The sample program is webgl/shape-stamper.html. Here is a demo version of the program so you can see how it works.

In the RGB triangle example, color is an attribute, since a different color is assigned to each vertex of the triangle primitive. In the shape-stamper program, all vertices, and in fact all pixels, in a primitive have the same color. That means that color can be a uniform variable. The example also allows transparency, so colors need an alpha component as well as the RGB components. It was convenient in the program to treat the alpha and RGB components as separate quantities, so I represent them as two separate uniform variables in the shader program. The color and alpha uniforms are used in the fragment shader to assign the fragment's color. In fact, that's the only thing the fragment shader does, so the complete source code is as follows:

precision mediump float;
uniform vec3 u_color;
uniform float u_alpha;
void main() {
gl_FragColor = vec4(u_color, u_alpha);
}

To work with a uniform variable on the JavaScript side, we need to know its location in the shader program. The program gets the locations of the two uniform variables in the intiGL() function using the commands

uniformColor = gl.getUniformLocation(prog, "u_color");
uniformAlpha = gl.getUniformLocation(prog, "u_alpha");

The program has two popup menus that let the user select the color and alpha that are to be used for drawing a primitive. When a shape is drawn, the values from the menus determine the values of the uniforms:

let colorNumber = Number(document.getElementById("colorChoice").value);
let alpha = Number(document.getElementById("opacityChoice").value);

gl.uniform3fv( uniformColor, colorList[colorNumber] );
gl.uniform1f( uniformAlpha, alpha );

Values for uniform variables are set using the gl.uniform* family of functions. In this case, colorList[colorNumber] is an array of three numbers holding the RGB color components for the color, so the function gl.uniform3fv is used to set the value: The "3f" means that 3 floating point values are provided, and the "v" means that the three values are in an array. Note that three floating point values are required to match the type, vec3, of the uniform variable in the shader. The value of alpha is a single floating point number, so the corresponding uniform variable is set using gl.uniform1f.

In order for the alpha component of the color to have any effect, alpha blending must be enabled. This is done as part of initialization with the two commands

gl.enable( gl.BLEND );
gl.blendFunc( gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA );

The first line enables use of the alpha component. The second tells how the alpha component is to be used. The "blendFunc" used here is appropriate for transparency in 2D. The same commands were used in Subsection 3.1.2 in OpenGL 1.1.


When the program starts, the user sees a blank white canvas. When the user clicks the canvas, a shape is added. When the user clicks again, a second shape is added—and the first shape better still be there! However, this is not the default behavior for WebGL!

When the user clicks the canvas, an event-handler function for the mousedown event is called. The shape is drawn in that function. When the function returns, WebGL tells the web browser that the image has been modified, and the web browser copies the new image to the screen. Once that happens, as discussed earlier in this section, the default behavior for WebGL is to discard the image. But this means that the second mouse click is drawing on a blank canvas, since the shape from the first mouse click has been erased.

To fix this problem, the preserveDrawingBuffer option in the WebGL graphics context must be set to true. The shape-stamper program creates the context with

let options = {  // No need for alpha channel or depth buffer, but we
                // need to preserve the image in the drawing buffer.
            alpha: false,
            depth: false,
            preserveDrawingBuffer: true
    };
gl = canvas.getContext("webgl", options);

Note that this program does not have a draw() function that redraws the entire image. All the drawing is done in the mouse-handling function, doMouseDown. Things could have been done differently. The program could have used a data structure to store information about the shapes that have been drawn. Clicking the canvas would add an item to the list, and the entire image would then be redrawn, including the new shape. In the actual program, however, the only record of what's in the image is the image itself. (In the terminology of Section 1.1, it is a painting program rather than a drawing program.)


WebGL uses a default coordinate system in which each of the coordinates ranges from −1 to 1. Of course, we would like to use a more convenient coordinate system, which means that we need to apply a coordinate transformation to transform the coordinates that we use into the default coordinate system. In the shape-stamper program, the natural coordinate system is pixel coordinates on the canvas. In the pixel coordinate system, the x-coordinate ranges from 0 at the left to canvas.width at the right, and y ranges from 0 at the top to canvas.height at the bottom. The equations for transforming pixel coordinates (x1,y1) to default coordinates (x2,y2) are

x2 = -1 + 2*( x1 / canvas.width );
y2 = 1 - 2*( y1 / canvas.height );

In WebGL, the coordinate transformation is usually applied in the vertex shader. In this case, to implement the transformation, the vertex shader just needs to know the width and height of the canvas. The program provides the width and height to the vertex shader as uniform variables. The original pixel coordinates of the vertex are input to the vertex shader as an attribute. The shader applies the coordinate transformation to compute the value of gl_Position, which must be expressed in the default coordinate system. Here is the vertex shader source code:

attribute vec2 a_coords;   // pixel coordinates
uniform float u_width;     // width of canvas
uniform float u_height;    // height of canvas
void main() {
float x = -1.0 + 2.0*(a_coords.x / u_width);
float y = 1.0 - 2.0*(a_coords.y / u_height);
gl_Position = vec4(x, y, 0.0, 1.0);
}

Transformations can be much more complicated than this, especially in 3D, but the general pattern holds: Transformations are represented by uniform variables and are applied in the vertex shader. In general, transformations are implemented as matrices. We will see later that uniform variables can be matrices and that the shader language GLSL has good support for matrix operations.

In order to draw a shape, we need to store the pixel coordinates for that shape in a Float32Array; then, we have to load the values from that array into the buffer associated with the "a_coords" attribute; and finally, we must call gl.drawArrays to do the actual drawing. The coordinates for the shape can be computed based on what type of shape is being drawn and on the point where the user clicked. For example, the coordinate array for a circle is created by the following code, where x and y are the pixel coordinates for the point that was clicked:

coords = new Float32Array(64);
k = 0;
for (let i = 0; i < 32; i++) {
    let angle = i/32 * 2*Math.PI;
    coords[k++] = x + 50*Math.cos(angle);  // x-coord of vertex i
    coords[k++] = y + 50*Math.sin(angle);  // y-coord of vertex i
}

The circle is approximated as a 32-sided regular polygon, with a radius of 50 pixels. Two coordinates are required for each vertex, so the length of the array is 64. The code for the other shapes is similar. Once the array has been created, the shape is drawn using

gl.bindBuffer(gl.ARRAY_BUFFER, bufferCoords);
gl.bufferData(gl.ARRAY_BUFFER, coords, gl.STREAM_DRAW);
gl.vertexAttribPointer(attributeCoords, 2, gl.FLOAT, false, 0, 0);

gl.drawArrays(gl.TRIANGLE_FAN, 0, coords.length/2);

In the last line, coords.length/2 is the number of vertices in the shape, since the array holds two numbers per vertex. Note also that the last parameter to gl.bufferData is gl.STREAM_DRAW, which is appropriate when the data in the VBO will only be used once or a few times before being discarded.


Although the demo version of the sample program has the same functionality, I implemented shape drawing differently in the two versions. Notice that all circles in the program are the same; they are just in different locations. It should be possible to draw the circle in its own object coordinates, and then apply a modeling transformation to move the circle to its desired position in the scene. This is the approach that I take in the demo version of the program.

There are four kinds of shape: circles, squares, triangles, and stars. In the demo version, I create a separate VBO for each kind of shape. The VBO for a shape contains vertex coordinates for that shape in object coordinates, with the shape centered at (0,0). Since the object coordinates will never change, the VBO can be created once and for all as part of program initialization. For example, the VBO for the circle is created with

coords = new Float32Array(64); 
let k = 0;  // index into the coords array
for (let i = 0; i < 32; i++) {
    let angle = i/32 * 2*Math.PI;
    coords[k++] = 50*Math.cos(angle);  // x-coord of vertex
    coords[k++] = 50*Math.sin(angle);  // y-coord of vertex
}

bufferCoordsCircle = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, bufferCoordsCircle );
gl.bufferData(gl.ARRAY_BUFFER, coords, gl.STATIC_DRAW);

Note the use of gl.STATIC_DRAW in the last line. It is appropriate since the data can be reused to draw many different circles.

To draw a shape with its center at (x,y), a translation must be applied to the coordinates in the VBO. I added the translation to the vertex shader, with a new uniform variable to represent the translation amount:

attribute vec2 a_coords;
uniform float u_width;
uniform float u_height;
uniform vec2 u_translation;
void main() {
float x = -1.0 + 2.0*((a_coords.x + u_translation.x) / u_width);
float y = 1.0 - 2.0*((a_coords.y + u_translation.y) / u_height);
gl_Position = vec4(x, y, 0.0, 1.0);
}

You would probably find it worthwhile to read the full source code for the demo as well as the sample program.

6.2.5 POINTS原语

The POINTS Primitive

这一节的最后一个示例演示了gl.POINTS原语。POINTS原语基本上是一组不相连的顶点。默认情况下,每个顶点被渲染为一个像素。然而,程序可以指定更大的大小。在OpenGL 1.1中,这是通过函数gl_PointSize()完成的。在WebGL中,那个函数不存在。相反,大小由顶点着色器控制。

在处理POINTS原语的顶点时,顶点着色器应该给特殊的内置变量gl_PointSize赋值。该变量是float类型。它给出了顶点的大小,以像素为单位。顶点被渲染为一个正方形,中心位于顶点位置,宽度和高度由gl_PointSize给出。这实际上意味着,片段着色器将为正方形中的每个像素调用一次。注意,有一个依赖于实现的限制,大小可能相当小。唯一保证存在的大小是一个像素,但大多数实现似乎支持至少到64像素的点大小,可能更大。

当片段着色器被调用处理POINTS原语时,它正在处理围绕顶点的像素正方形中的一个像素。特殊的片段着色器变量gl_PointCoord告诉着色器像素在正方形内的位置。gl_PointCoord的值是着色器的输入。gl_PointCoord的类型是vec2,所以它有两个浮点分量。每个分量的值在0到1的范围内。第一个分量,gl_PointCoord.x,在正方形的左边缘为0,在右边缘为1。第二个分量,gl_PointCoord.y,在正方形的顶部为0,在底部为1。例如,该值在左上角为(0,0),在右上角为(1,0),在正方形中心为(0.5,0.5)。(至少,规格是这样的,但我遇到过实现错误地将(0,0)放在左下角。希望现代网络浏览器已经修复了这个问题。)

如果片段着色器在其计算中使用gl_PointCoord,正方形的颜色可以逐像素变化。作为一个简单的例子,设置

gl_FragColor = vec4( gl_PointCoord.x, 0.0, 0.0, 1.0 );

将把原语中的每个顶点渲染为一个正方形颜色渐变,颜色从正方形左边缘的黑色水平变化到右边缘的红色。在示例程序中,我使用gl_PointCoord将顶点渲染为圆盘而不是正方形。该技术使用了一个新的GLSL语句,discard,仅在片段着色器中可用。当片段着色器执行语句

discard;

片段着色器终止,阻止对像素的所有进一步处理。特别是,图像中像素的颜色不会改变。我使用discard,如果从gl_PointCoord到中心(0.5,0.5)的距离大于0.5。这将丢弃不位于半径为0.5的圆盘内的像素。GLSL有一个函数用于计算两个向量之间的距离,所以在片段着色器中的测试编写为

float distanceFromCenter = distance( gl_PointCoord, vec2(0.5,0.5) );
if ( distanceFromCenter >= 0.5 ) {
    discard;  // 不绘制此像素!
}

示例程序是webgl/moving-points.html。它展示了一个动画,彩色圆盘在画布中移动并从边缘反弹。所有圆盘都是一步中作为gl.POINTS类型的单个原语绘制的。圆盘的大小实现为统一变量,以便所有圆盘具有相同的大小,但统一大小在动画的不同帧中可以不同。在程序中,用户可以通过弹出菜单控制大小。这是程序的演示版本,功能相同:

在程序中,用户可以选择圆盘是随机颜色还是全部为红色。由于每个圆盘是一个单个POINTS原语的顶点,圆盘可以有不同的颜色意味着颜色必须由属性变量给出。要实现随机颜色,一个Float32Array用随机数字填充,每个顶点三个。值被加载到VBO中,颜色属性的值来自VBO。但是当所有圆盘都是红色时会发生什么?我们必须用多个“1, 0, 0”的副本填充数组,并使用该数据作为属性吗?事实上,我们不必。如果我们禁用颜色属性的VertexAttribArray,那么该属性将对每个顶点具有相同的值。该值由gl.vertexAttrib*系列函数指定。所以,在示例程序中,提供颜色属性值的代码是

if ( randomColors ) { 
    // 使用在初始化期间填充的颜色VBO中的属性值。
    gl.enableVertexAttribArray( attributeColor ); 
}
else { 
    // 关闭VertexAttribArray,
    //     并设置一个恒定的属性颜色。
    gl.disableVertexAttribArray( attributeColor );
    gl.vertexAttrib3f( attributeColor, 1, 0, 0 );
}

查看源代码了解示例的完整细节。

The final example in this section demonstrates the gl.POINTS primitive. A POINTS primitive is basically a set of disconnected vertices. By default, each vertex is rendered as a single pixel. However, a program can specify a larger size. In OpenGL 1.1, this was done with the function gl_PointSize(). In WebGL, that function does not exist. Instead, the size is under the control of the vertex shader.

When working on one of the vertices of a POINTS primitive, the vertex shader should assign a value to the special built-in variable gl_PointSize. The variable is of type float. It gives the size of the vertex, in pixels. The vertex is rendered as a square, centered at the vertex position, whose width and height are given by gl_PointSize. What this really means is that the fragment shader will be called once for each pixel in that square. Note that there is an implementation-dependent limit on the size of points, which can be fairly small. The only size that is guaranteed to exist is one pixel, but most implementations seem to support point sizes at least up to 64 pixels, and possibly much larger.

When the fragment shader is called for a POINTS primitive, it is processing one pixel in the square of pixels surrounding the vertex. The special fragment shader variable gl_PointCoord tells the shader the location of the pixel within that square. The value of gl_PointCoord is an input to the shader. The type of gl_PointCoord is vec2, so it has two floating point components. The value of each component is in the range 0 to 1. The first component, gl_PointCoord.x, is 0 at the left edge of the square and 1 at the right. The second component, gl_PointCoord.y, is 0 at the top of the square and 1 at the bottom. So, for example, the value is (0,0) at the top-left corner, (1,0) at the top-right corner, and (0.5,0.5) at the center of the square. (That, at least, is what the specification says, but I have encountered implementations that incorrectly put (0,0) at the bottom left corner. Hopefully that is fixed in modern web browsers.)

If the fragment shader uses gl_PointCoord in its computation, the color of the square can vary from pixel to pixel. As a simple example, setting

gl_FragColor = vec4( gl_PointCoord.x, 0.0, 0.0, 1.0 );

would render each vertex in the primitive as a square color gradient whose color varies horizontally from black on the left edge of the square to red on the right edge. In the sample program, I use gl_PointCoord to render the vertex as a disk instead of a square. The technique uses a new GLSL statement, discard, which is available only in the fragment shader. When the fragment shader executes the statement

discard;

the fragment shader terminates, and all further processing of the pixel is prevented. In particular, the color of the pixel in the image does not change. I use discard if the distance from gl_PointCoord to the center, (0.5,0.5), is greater than 0.5. This discards pixels that do not lie in the disk of radius 0.5. GLSL has a function for computing the distance between two vectors, so the test in the fragment shader is written

float distanceFromCenter = distance( gl_PointCoord, vec2(0.5,0.5) );
if ( distanceFromCenter >= 0.5 ) {
discard;  // don't draw this pixel!
}

The sample program is webgl/moving-points.html. It shows an animation of colored disks moving in the canvas and bouncing off the edges. All of the disks are drawn in one step as a single primitive of type gl.POINTS. The size of the disks is implemented as a uniform variable, so that all the disks have the same size, but the uniform size can be different in different frames of the animation. In the program, the user controls the size with a popup menu. Here is a demo version of the program, with the same functionality:

In the program, the user can select whether the disks have random colors or are all colored red. Since each disk is a vertex of a single POINTS primitive, the fact that the disks can have different colors means that the color has to be given by an attribute variable. To implement random colors, a Float32Array is filled with random numbers, three for each vertex. The values are loaded into a VBO, and the values for the color attribute are taken from the VBO. But what happens when all the disks are red? Do we have to fill an array with multiple copies of "1, 0, 0" and use that data for the attribute? In fact, we don't. If we disable the VertexAttribArray for the color attribute, then that attribute will have the same value for every vertex. The value is specified by the gl.vertexAttrib* family of functions. So, in the sample program, the code for providing values for the color attribute is

if ( randomColors ) { 
        // Use the attribute values from the color VBO, 
        //     which was filled during initialization.
    gl.enableVertexAttribArray( attributeColor ); 
}
else { 
        // Turn off VertexAttribArray,
        //     and set a constant attribute color.
    gl.disableVertexAttribArray( attributeColor );
    gl.vertexAttrib3f( attributeColor, 1, 0, 0 );
}

See the source code for full details of the example.

6.2.6 WebGL 错误处理

WebGL Error Handling

OpenGL程序员经常发现自己面对一个空白屏幕,没有清楚地指示出了什么问题。在许多情况下,这是由于编程逻辑错误,例如意外地绘制了一个不包含任何几何体的3D空间区域。然而,有时这是由于API使用错误。在WebGL中,以及更普遍的OpenGL中,像非法参数值这样的错误通常不会使程序崩溃或产生任何自动的错误通知。相反,当WebGL检测到这样的错误时,它会忽略非法的函数调用,并设置一个错误代码的值,该值提供了一些关于错误性质的指示。

程序可以通过调用gl.getError()来检查当前的错误代码值。这个函数返回一个整数错误代码。如果没有发生错误,返回值是gl.NO_ERROR。任何其他返回值意味着发生了错误。一旦设置了错误代码,它就会一直保持设置状态,直到调用gl.getError(),即使在此期间执行了其他正确的WebGL操作。调用gl.getError()检索错误代码的值,并将值重置为gl.NO_ERROR。(所以,如果你连续两次调用gl.getError(),第二次调用总是返回gl.NO_ERROR。)这意味着当gl.getError()返回错误时,错误实际上可能是由之前执行的某个指令生成的。

作为一个例子,考虑调用gl.drawArrays(primitive,first,count)。如果primitive不是七个合法的WebGL原语之一,那么WebGL将把错误代码设置为gl.INVALID_ENUM。如果first或count是负数,错误代码设置为gl.INVALID_VALUE。如果没有使用gl.useProgram安装着色器程序,错误是gl.INVALID_OPERATION。如果没有为启用的顶点属性指定数据,就会发生gl.INVALID_STATE类型的错误。这四个错误代码实际上是最常见的。

在每个WebGL函数调用后调用gl.getError既不切实际也不高效。然而,当出现问题时,它可以作为调试工具。当我怀疑有错误时,我可能会在代码的几个点插入如下代码:

console.log("Error code is " + gl.getError());

gl.NO_ERROR的数值是零。任何非零值意味着在调用gl.getError()之前的某个点发生了错误。通过在代码中移动输出语句,我可以缩小到实际产生错误的语句。

请注意,一些浏览器会自动将有关WebGL不正确使用的某些信息输出到它们的JavaScript控制台,这是许多浏览器内置的开发工具的一部分。那个控制台也是使用console.log()写入消息的目的地。在运行正在开发的WebGL程序时,始终检查控制台是一个好主意!

It is a sad fact that OpenGL programmers often find themselves looking at a blank screen, with no clear indication of what went wrong. In many cases, this is due to a programming logic error, such as accidentally drawing a region of 3D space that contains no geometry. However, sometimes it's due to an error in the use of the API. In WebGL, and in OpenGL more generally, an error such as an illegal parameter value will not in general crash the program or produce any automatic notification of the error. Instead, when WebGL detects such an error, it ignores the illegal function call, and it sets the value of an error code that gives some indication of the nature of the error.

A program can check the current value of the error code by calling gl.getError(). This function returns an integer error code. The return value is gl.NO_ERROR if no error has occurred. Any other return value means that an error has occurred. Once an error code has been set, it stays set until gl.getError() is called, even if other, correct WebGL operations have been executed in the meantime. Calling gl.getError() retrieves the value of the error code and resets its value to gl.NO_ERROR. (So, if you call gl.getError() twice in a row, the second call will always return gl.NO_ERROR.) This means that when gl.getError() returns an error, the error might actually have been generated by an instruction that was executed some time ago.

As an example, consider a call to gl.drawArrays(primitive,first,count). If primitive is not one of the seven legal WebGL primitives, then WebGL will set the error code to gl.INVALID_ENUM. If first or count is negative, the error code is set to gl.INVALID_VALUE. If no shader program has been installed with gl.useProgram, the error is gl.INVALID_OPERATION. If no data has been specified for an enabled vertex attribute, an error of type gl.INVALID_STATE occurs. These four error codes are, in fact, the most common.

It is both impractical and inefficient to call gl.getError after each WebGL function call. However, when something goes wrong, it can be used as a debugging aid. When I suspect an error, I might insert code such as

console.log("Error code is " + gl.getError());

at several points in my code. The numeric value of gl.NO_ERROR is zero. Any non-zero value means that an error occurred at some point before the call to gl.getError. By moving the output statements around in the code, I can narrow in on the statement that actually produced the error.

Note that some browsers automatically output certain information about incorrect use of WebGL to their JavaScript console, which is part of the development tools built into many browsers. That console is also the destination for messages written using console.log(). It's always a good idea to check the console when running a WebGL program that is under development!