跳转至

6.1 可编程流水线

The Programmable Pipeline

OpenGL 1.1 使用 固定功能管线 进行图形处理。数据由程序提供,并通过一系列处理阶段,最终产生最终图像中看到的像素颜色。程序可以启用和禁用过程中的一些步骤,例如深度测试和光照计算。但是,它无法改变每个阶段发生的事情。功能是固定的。

OpenGL 2.0 引入了 可编程管线。程序员可以替换管线中的某些阶段为自己的程序。这为程序员提供了对发生该阶段的完全控制。在 OpenGL 2.0 中,可编程性是可选的;对于不需要可编程灵活性的程序,仍然可以使用完整的固定功能管线。WebGL 使用可编程管线,并且是 强制性的。没有办法在不编写程序来实现图形处理管线的一部分的情况下使用 WebGL

作为管线一部分编写的程序称为 着色器。对于 WebGL,你需要编写一个 顶点着色器,它将为原语中的每个顶点调用一次,以及一个 片段着色器,它将为原语中的每个像素调用一次。除了这两个可编程阶段外,WebGL 管线还包含原始固定功能管线的几个阶段。例如,深度测试仍然是固定功能的一部分,并且可以在 WebGL 中以与 OpenGL 1.1 中相同的方式启用或禁用。

在本节中,我们将介绍 WebGL 程序的基本结构以及数据如何从程序的 JavaScript 部分流入图形管线,并通过顶点和片段着色器。

我应该注意,OpenGL 的后续版本引入了新的可编程阶段,除了顶点和片段着色器之外,但它们不是 WebGL 1.0 或 2.0 的一部分,本书中也没有涵盖。

OpenGL 1.1 uses a fixed-function pipeline for graphics processing. Data is provided by a program and passes through a series of processing stages that ultimately produce the pixel colors seen in the final image. The program can enable and disable some of the steps in the process, such as the depth test and lighting calculations. But there is no way for it to change what happens at each stage. The functionality is fixed.

OpenGL 2.0 introduced a programmable pipeline. It became possible for the programmer to replace certain stages in the pipeline with their own programs. This gives the programmer complete control over what happens at that stage. In OpenGL 2.0, the programmability was optional; the complete fixed-function pipeline was still available for programs that didn't need the flexibility of programmability. WebGL uses a programmable pipeline, and it is mandatory. There is no way to use WebGL without writing programs to implement part of the graphics processing pipeline.

The programs that are written as part of the pipeline are called shaders. For WebGL, you need to write a vertex shader, which is called once for each vertex in a primitive, and a fragment shader, which is called once for each pixel in the primitive. Aside from these two programmable stages, the WebGL pipeline also contains several stages from the original fixed-function pipeline. For example, the depth test is still part of the fixed functionality, and it can be enabled or disabled in WebGL in the same way as in OpenGL 1.1.

In this section, we will cover the basic structure of a WebGL program and how data flows from the JavaScript side of the program into the graphics pipeline and through the vertex and fragment shaders.

I should note that later versions of OpenGL have introduced new programmable stages, in addition to the vertex and fragment shaders, but they are not part of WebGL 1.0 or 2.0, and they are not covered in this book.

6.1.1 WebGL 图形上下文

The WebGL Graphics Context

要使用WebGL,你需要一个WebGL图形上下文。图形上下文是一个JavaScript对象,其方法实现了WebGL API的JavaScript部分。WebGL在HTML画布中绘制图像,这是与第2.6节中介绍的2D API相同的<canvas>元素。图形上下文与特定画布相关联。WebGL 1.0的图形上下文可以通过调用函数canvas.getContext("webgl")获得,其中canvas是表示画布的DOM对象。对于WebGL 2.0,你只需简单地使用canvas.getContext("webgl2")。如果无法创建上下文,getContext()的返回值将为null。因此,获取WebGL图形上下文通常看起来像这样:

canvas = document.getElementById("webglcanvas");
gl = canvas.getContext("webgl");  // 或许是 canvas.getContext("webgl2")
if ( ! gl ) {
    throw new Error("WebGL not supported; can't create graphics context.");
}

在这里,第一行获取HTML画布的引用,WebGL将在整个程序中用于绘图。变量名gl由你决定,但在我的讨论中,我将始终使用gl来表示WebGL图形上下文。此代码假定网页的HTML源包含一个id="webglcanvas"的画布元素,例如:

<canvas width="800" height="600" id="webglcanvas"></canvas>

在上面的代码第二行中,如果网络浏览器不支持getContext的"webgl"参数,canvas.getContext("webgl")将返回null。由于在JavaScript中,null在布尔上下文中被视为false,第三行测试返回值是否为null。在这种情况下,代码抛出一个错误,这可以在其他地方处理,可能是通过向用户显示错误消息。此外,如果浏览器根本没有对<canvas>的支持,代码将抛出异常。我的程序通常使用以下形式的初始化函数:

function init() {
    try {
        canvas = document.getElementById("webglcanvas");
        gl = canvas.getContext("webgl");  // 或 "webgl2"
        if ( ! gl ) {
            throw new Error("WebGL not supported.");
        }
    }
    catch (e) {
        .
        .  // 报告错误
        .
        return;
    }
    initGL();  // 一个初始化WebGL图形上下文的函数
    .
    .  // 其他JavaScript初始化
    .
}

在这个函数中,canvasgl是全局变量。initGL()是在脚本中其他地方定义的函数,用于初始化图形上下文,包括创建和安装着色器程序。init()函数应在页面加载时调用。例如,你可以通过在脚本中分配"window.onload = init;"来安排。

一旦创建了图形上下文gl,就可以使用它来调用WebGL API中的函数。例如,启用深度测试的命令,在OpenGL中写为glEnable(GL_DEPTH_TEST),变成了:

gl.enable( gl.DEPTH_TEST );

请注意,API中的函数和常量都是通过图形上下文引用的。图形上下文的名称"gl"是约定俗成的,但请记住,它只是一个普通的JavaScript变量,其名称由程序员决定。

(一些非常旧的浏览器需要canvas.getContext("experimental-webgl")来创建WebGL 1.0上下文。这包括Internet Explorer 11,但到现在为止,没有人应该使用Internet Explorer。)

To use WebGL, you need a WebGL graphics context. The graphics context is a JavaScript object whose methods implement the JavaScript side of the WebGL API. WebGL draws its images in an HTML canvas, the same kind of <canvas> element that is used for the 2D API that was covered in Section 2.6. A graphics context is associated with a particular canvas. A graphics context for WebGL 1.0 can be obtained by calling the function canvas.getContext("webgl"), where canvas is a DOM object representing the canvas. For WebGL 2.0, you would simply use canvas.getContext("webgl2") instead. The return value of getContext() will be null if the context cannot be created. So, getting a WebGL graphics context often looks something like this:

canvas = document.getElementById("webglcanvas");
gl = canvas.getContext("webgl");  // or maybe canvas.getContext("webgl2")
if ( ! gl ) {
    throw new Error("WebGL not supported; can't create graphics context.");
}

Here, the first line gets a reference to the HTML canvas that WebGL will used throughout the program for drawing. The name gl for the variable is up to you, but I will always use gl for the WebGL graphics context in my discussion. This code assumes that the HTML source for the web page includes a canvas element with id="webglcanvas", such as

<canvas width="800" height="600" id="webglcanvas"></canvas>

In the second line of the above code, canvas.getContext("webgl") will return null if the web browser does not support "webgl" as a parameter to getContext. Since null is considered to be false in JavaScript when used in a boolean context, the third line tests whether the return value is null. In that case, the code throws an error, which can be handled elsewhere, probably by showing an error message to the user. Furthermore, the code will throw an exception if the browser has no support at all for <canvas>. My programs often use an initialization function of the form

function init() {
    try {
        canvas = document.getElementById("webglcanvas");
        gl = canvas.getContext("webgl");  // or "webgl2"
        if ( ! gl ) {
            throw new Error("WebGL not supported.");
        }
    }
    catch (e) {
        .
        .  // report the error
        .
        return;
    }
    initGL();  // a function that initializes the WebGL graphics context
    .
    .  // other JavaScript initialization
    .
}

In this function, canvas and gl are global variables. And initGL() is a function defined elsewhere in the script that initializes the graphics context, including creating and installing the shader programs. The init() function should be called when the page is loaded. This can be arranged, for example, by assigning "window.onload = init;" in the script.

Once the graphics context, gl, has been created, it can be used to call functions in the WebGL API. For example, the command for enabling the depth test, which was written as glEnable(GL_DEPTH_TEST) in OpenGL, becomes

gl.enable( gl.DEPTH_TEST );

Note that both functions and constants in the API are referenced through the graphics context. The name "gl" for the graphics context is conventional, but remember that it is just an ordinary JavaScript variable whose name is up to the programmer.

(Some very old browsers required canvas.getContext("experimental-webgl") to create a WebGL 1.0 context. This includes Internet Explorer 11, but at this point, no one should be using Internet Explorer.)

6.1.2 着色器程序

The Shader Program

使用WebGL绘图需要一个着色器程序,它由顶点着色器和片段着色器组成。着色器是用GLSL编程语言的某个版本编写的。WebGL 1.0使用GLSL ES 1.00,而WebGL 2.0可以使用GLSL ES 1.00或GLSL ES 3.00。这里的讨论是关于GLSL ES 1.00的;我稍后会解释3.00版本中的一些变化。

GLSL基于C编程语言。顶点着色器和片段着色器是分开的程序,每个程序都有自己的main()函数。这两个着色器分别编译,然后“链接”以产生完整的着色器程序。WebGL的JavaScript API包括用于编译着色器然后链接它们的函数。要使用这些函数,着色器的源代码必须是JavaScript字符串。让我们看看它是如何工作的。创建顶点着色器需要三个步骤。

let vertexShader = gl.createShader( gl.VERTEX_SHADER );
gl.shaderSource( vertexShader, vertexShaderSource );
gl.compileShader( vertexShader );

这里使用的函数是WebGL图形上下文gl的一部分,参数vertexShaderSource是包含着色器源代码的字符串。源代码中的错误会导致编译失败,但不会产生任何提示。你需要通过调用函数

gl.getShaderParameter( vertexShader, gl.COMPILE_STATUS )

来检查编译是否成功,该函数返回一个布尔值,表示编译是否成功。如果发生错误,你可以使用

gl.getShaderInfoLog( vertexShader )

来检索错误消息,该函数返回一个字符串,包含编译结果。(字符串的确切格式没有被WebGL标准指定。该字符串旨在易于人类阅读。)

可以以类似的方式创建片段着色器。有了这两个着色器,你可以创建并链接程序。在链接之前,需要将着色器“附加”到程序对象上。代码形式如下:

let prog = gl.createProgram();
gl.attachShader( prog, vertexShader );
gl.attachShader( prog, fragmentShader );
gl.linkProgram( prog );

即使着色器已成功编译,当它们链接到一个完整的程序时也可能发生错误。例如,顶点和片段着色器可以共享某些类型的变量。如果两个程序声明了同名但类型不同的变量,链接时就会发生错误。检查链接错误与检查着色器编译错误类似。

创建着色器程序的代码总是非常相似的,因此将其打包到一个可重用的函数中非常方便。以下是我在此章节示例中使用的函数:

/**
 * Creates a program for use in the WebGL context gl, and returns the
 * identifier for that program.  If an error occurs while compiling or
 * linking the program, an exception of type Error is thrown.  The error
 * string contains the compilation or linking error. 
 */
function createProgram(gl, vertexShaderSource, fragmentShaderSource) {
let vsh = gl.createShader( gl.VERTEX_SHADER );
gl.shaderSource( vsh, vertexShaderSource );
gl.compileShader( vsh );
if ( ! gl.getShaderParameter(vsh, gl.COMPILE_STATUS) ) {
    throw new Error("Error in vertex shader:  " + gl.getShaderInfoLog(vsh));
}
let fsh = gl.createShader( gl.FRAGMENT_SHADER );
gl.shaderSource( fsh, fragmentShaderSource );
gl.compileShader( fsh );
if ( ! gl.getShaderParameter(fsh, gl.COMPILE_STATUS) ) {
    throw new Error("Error in fragment shader:  " + gl.getShaderInfoLog(fsh));
}
let prog = gl.createProgram();
gl.attachShader( prog, vsh );
gl.attachShader( prog, fsh );
gl.linkProgram( prog );
if ( ! gl.getProgramParameter( prog, gl.LINK_STATUS) ) {
    throw new Error("Link error in program:  " + gl.getProgramInfoLog(prog));
}
return prog;
}

还有一个步骤:你必须告诉WebGL上下文使用程序。如果prog是由上述函数返回的程序标识符,这是通过调用

gl.useProgram( prog );

完成的。

可以创建多个着色器程序。然后,你可以随时通过调用gl.useProgram在不同程序之间切换,甚至在渲染图像的过程中。(例如,Three.js为每种类型的Material使用不同的程序。)

建议在初始化过程中创建所需的任何着色器程序。虽然gl.useProgram是一个快速操作,但编译和链接相当慢,所以最好避免在绘制图像的过程中创建新程序。

不再需要的着色器和程序可以被删除以释放它们消耗的资源。使用函数gl.deleteShader(shader)gl.deleteProgram(program)

Drawing with WebGL requires a shader program, which consists of a vertex shader and a fragment shader. Shaders are written in some version of the GLSL programming language. WebGL 1.0 used GLSL ES 1.00, while WebGL 2.0 can use either GLSL ES 1.00 or GLSL ES 3.00. The discussion here is about GLSL ES 1.00; I will explain some of the changes in the 3.00 version later.

GLSL is based on the C programming language. The vertex shader and fragment shader are separate programs, each with its own main() function. The two shaders are compiled separately and then "linked" to produce a complete shader program. The JavaScript API for WebGL includes functions for compiling the shaders and then linking them. To use the functions, the source code for the shaders must be JavaScript strings. Let's see how it works. It takes three steps to create the vertex shader.

let vertexShader = gl.createShader( gl.VERTEX_SHADER );
gl.shaderSource( vertexShader, vertexShaderSource );
gl.compileShader( vertexShader );

The functions that are used here are part of the WebGL graphics context, gl, and the parameter vertexShaderSource is the string that contains the source code for the shader. Errors in the source code will cause the compilation to fail silently. You need to check for compilation errors by calling the function

gl.getShaderParameter( vertexShader, gl.COMPILE_STATUS )

which returns a boolean value to indicate whether the compilation succeeded. In the event that an error occurred, you can retrieve an error message with

gl.getShaderInfoLog( vertexShader )

which returns a string containing the result of the compilation. (The exact format of the string is not specified by the WebGL standard. The string is meant to be human-readable.)

The fragment shader can be created in a similar way. With both shaders in hand, you can create and link the program. The shaders need to be "attached" to the program object before linking. The code takes the form:

let prog = gl.createProgram();
gl.attachShader( prog, vertexShader );
gl.attachShader( prog, fragmentShader );
gl.linkProgram( prog );

Even if the shaders have been successfully compiled, errors can occur when they are linked into a complete program. For example, the vertex and fragment shader can share certain kinds of variable. If the two programs declare such variables with the same name but with different types, an error will occur at link time. Checking for link errors is similar to checking for compilation errors in the shaders.

The code for creating a shader program is always pretty much the same, so it is convenient to pack it into a reusable function. Here is the function that I use for the examples in this chapter:

/**
 * Creates a program for use in the WebGL context gl, and returns the
 * identifier for that program.  If an error occurs while compiling or
 * linking the program, an exception of type Error is thrown.  The error
 * string contains the compilation or linking error. 
 */
function createProgram(gl, vertexShaderSource, fragmentShaderSource) {
let vsh = gl.createShader( gl.VERTEX_SHADER );
gl.shaderSource( vsh, vertexShaderSource );
gl.compileShader( vsh );
if ( ! gl.getShaderParameter(vsh, gl.COMPILE_STATUS) ) {
    throw new Error("Error in vertex shader:  " + gl.getShaderInfoLog(vsh));
}
let fsh = gl.createShader( gl.FRAGMENT_SHADER );
gl.shaderSource( fsh, fragmentShaderSource );
gl.compileShader( fsh );
if ( ! gl.getShaderParameter(fsh, gl.COMPILE_STATUS) ) {
    throw new Error("Error in fragment shader:  " + gl.getShaderInfoLog(fsh));
}
let prog = gl.createProgram();
gl.attachShader( prog, vsh );
gl.attachShader( prog, fsh );
gl.linkProgram( prog );
if ( ! gl.getProgramParameter( prog, gl.LINK_STATUS) ) {
    throw new Error("Link error in program:  " + gl.getProgramInfoLog(prog));
}
return prog;
}

There is one more step: You have to tell the WebGL context to use the program. If prog is a program identifier returned by the above function, this is done by calling

gl.useProgram( prog );

It is possible to create several shader programs. You can then switch from one program to another at any time by calling gl.useProgram, even in the middle of rendering an image. (Three.js, for example, uses a different program for each type of Material.)

It is advisable to create any shader programs that you need as part of initialization. Although gl.useProgram is a fast operation, compiling and linking are rather slow, so it's better to avoid creating new programs while in the process of drawing an image.

Shaders and programs that are no longer needed can be deleted to free up the resources they consume. Use the functions gl.deleteShader(shader) and gl.deleteProgram(program).

6.1.3 管道中的数据流

Data Flow in the Pipeline

WebGL图形管线渲染图像。定义图像的数据来自JavaScript。当它通过管线时,它会被当前的顶点着色器和片段着色器以及管线的固定功能阶段处理。你需要了解如何将数据通过JavaScript放置到管线中,以及数据在通过管线时是如何被处理的。

WebGL中的基本操作是绘制一个几何原语。WebGL只使用在3.1.1小节中介绍的OpenGL原语中的七个。用于绘制四边形和多边形的原语已被移除。剩余的原语绘制点、线段和三角形。在WebGL中,七种类型的原语由常量gl.POINTSgl.LINESgl.LINE_STRIPgl.LINE_LOOPgl.TRIANGLESgl.TRIANGLE_STRIPgl.TRIANGLE_FAN标识,其中gl是WebGL图形上下文。

当WebGL用于绘制一个原语时,可以为原语提供两类数据。这两种数据分别称为属性变量(或简称“属性”)和统一变量(或简称“统一”)。一个原语由其类型和顶点列表定义。属性和统一的区别在于,统一变量有一个单一的值,对整个原语都是相同的,而属性变量的值可以对不同的顶点不同。

应该始终指定的一个属性是顶点的坐标。顶点坐标必须是属性,因为原语中的每个顶点都有自己的坐标集。另一个可能的属性是颜色。我们已经看到,OpenGL允许你为原语的每个顶点指定不同的颜色。在WebGL中,你也可以这样做,这种情况下颜色将是一个属性。另一方面,如果你希望整个原语具有相同的“统一”颜色,在这种情况下,颜色可以是统一变量。其他可能根据需要是属性或统一的数量包括法向量和材质属性。如果使用,纹理坐标几乎肯定是属性,因为让原语中的所有顶点具有相同的纹理坐标没有多大意义。如果对原语应用几何变换,它很自然地被表示为统一变量。

然而,重要的是要理解,WebGL没有任何预定义的属性,甚至没有顶点坐标的属性。在可编程管线中,使用哪些属性和统一完全取决于程序员。就WebGL而言,属性只是传递到顶点着色器的值。统一可以传递到顶点着色器、片段着色器或两者。WebGL不赋予值任何意义。意义完全由着色器对值的处理决定。在绘制原语时使用的属性和统一的集合由绘制原语时使用的着色器的源代码确定。

为了理解这一点,我们需要更详细地看看管线中发生了什么。在绘制原语时,JavaScript程序会为着色器程序中的任何属性和统一指定值。对于每个属性,它将指定一个值数组,每个顶点一个。对于每个统一,它将指定一个单一的值。它必须在绘制原语之前将这些值发送到GPU。然后,可以通过调用单个JavaScript函数来绘制原语。在这一点上,GPU接管并执行着色器程序。在绘制原语时,GPU会为每个顶点调用一次顶点着色器。要处理的顶点的属性值被作为输入传递到顶点着色器。统一变量的值也会传递到顶点着色器。这是通过将属性和统一表示为顶点着色器程序中的全局变量来实现的。在调用给定顶点的着色器之前,GPU会适当地设置这些变量的值。

作为其输出之一,顶点着色器必须指定顶点在裁剪坐标系中的坐标(见3.3.1小节)。它通过给一个名为gl_Position的特殊变量赋值来实现。位置通常通过对表示对象坐标系中坐标的属性应用变换来计算,但位置的计算方式完全取决于程序员。

在计算了原语中所有顶点的位置之后,管线中的一个固定功能阶段会裁剪掉坐标超出有效裁剪坐标范围(每个坐标轴上的-1到1)的原语部分。然后,原语被光栅化;也就是说,确定哪些像素位于原语内部。然后,GPU会为原语中的每个像素调用一次片段着色器。片段着色器可以访问统一变量(但不能访问属性)。它还可以使用一个名为gl_FragCoord的特殊变量,其中包含像素的裁剪坐标。像素坐标是通过插值顶点着色器指定的gl_Position值来计算的。插值是由位于顶点着色器和片段着色器之间的另一个固定功能阶段完成的。

除了坐标之外,其他数量也可以以类似的方式工作。也就是说,顶点着色器为原语的每个顶点计算数量的值。插值器采用在顶点着色器中为原语的每个顶点生成的值,并计算原语中每个像素的值。当片段着色器被调用以处理给定像素时,变化变量的值就是该像素的插值值。片段着色器可以在自己的计算中使用该值。

变化变量存在于顶点着色器和片段着色器中。顶点着色器负责给变化变量赋值。原语的每个顶点可以给变量分配不同的值。插值器采用执行顶点着色器为原语的每个顶点产生的所有值,并插值这些值以产生每个像素的值。当片段着色器被执行以处理给定像素时,变化变量的值就是该像素的插值值。片段着色器可以在自己的计算中使用该值。

变化变量的存在是为了从顶点着色器向片段着色器通信数据。它们在着色器源代码中定义。它们不在使用API的JavaScript方面使用或引用。请注意,决定定义哪些变化变量以及如何处理它们完全取决于程序员。

我们几乎已经到了管线的末尾。在所有这些之后,片段着色器的工作仅仅是为像素指定一种颜色。它通过给一个名为gl_FragColor的特殊变量赋值来实现。然后,该值将被用在管线的剩余固定功能阶段中。

总结:程序的JavaScript方面将属性和统一变量的值发送到GPU,然后发出绘制原语的命令。GPU为每个顶点执行一次顶点着色器。顶点着色器可以使用属性和统一的值。它给gl_Position和着色器中的任何变化变量赋值。在裁剪、光栅化和插值之后,GPU为原语中的每个像素执行一次片段着色器。片段着色器可以使用变化变量、统一和gl_FragCoord的值。它计算gl_FragColor的值。这张图总结了数据的流向:

123

这张图不完整。还有一些特殊的变量我还没有提到。以及如何使用纹理的重要问题。但是如果你理解了这张图,你就对理解WebGL有了一个良好的开端。


对于GLSL ES 3.00,相同的图适用,只是没有特殊的变量gl_FragColor。相反,片段着色器必须定义自己的输出变量来表示颜色。在GLSL ES 1.00中,声明实际着色器程序源代码中的变量时使用“attribute”和“varying”。在3.00版本的源代码中,属性变量变为“in”变量,因为它们是顶点着色器的输入,而变化变量在顶点着色器中变为“out”变量,在片段着色器中变为“in”变量。变量gl_FragColor被片段着色器中的“out”变量替换。实际上,“in”和“out”的使用更适合具有附加管线阶段的系统,其中一个阶段的“out”变量可以成为下一个阶段的“in”变量。无论如何,在讨论WebGL时,即使使用GLSL ES 3.00,人们仍然使用属性和变化这些术语。

The WebGL graphics pipeline renders an image. The data that defines the image comes from JavaScript. As it passes through the pipeline, it is processed by the current vertex shader and fragment shader as well as by the fixed-function stages of the pipeline. You need to understand how data is placed by JavaScript into the pipeline and how the data is processed as it passes through the pipeline.

The basic operation in WebGL is to draw a geometric primitive. WebGL uses just seven of the OpenGL primitives that were introduced in Subsection 3.1.1. The primitives for drawing quads and polygons have been removed. The remaining primitives draw points, line segments, and triangles. In WegGL, the seven types of primitive are identified by the constants gl.POINTS, gl.LINES, gl.LINE_STRIP, gl.LINE_LOOP, gl.TRIANGLES, gl.TRIANGLE_STRIP, and gl.TRIANGLE_FAN, where gl is a WebGL graphics context.

When WebGL is used to draw a primitive, there are two general categories of data that can be provided for the primitive. The two kinds of data are referred to as attribute variables (or just "attributes") and uniform variables (or just "uniforms"). A primitive is defined by its type and by a list of vertices. The difference between attributes and uniforms is that a uniform variable has a single value that is the same for the entire primitive, while the value of an attribute variable can be different for different vertices.

One attribute that should always be specified is the coordinates of the vertex. The vertex coordinates must be an attribute since each vertex in a primitive will have its own set of coordinates. Another possible attribute is color. We have seen that OpenGL allows you to specify a different color for each vertex of a primitive. You can do the same thing in WebGL, and in that case the color will be an attribute. On the other hand, maybe you want the entire primitive to have the same, "uniform" color; in that case, color can be a uniform variable. Other quantities that could be either attributes or uniforms, depending on your needs, include normal vectors and material properties. Texture coordinates, if they are used, are almost certain to be an attribute, since it doesn't really make sense for all the vertices in a primitive to have the same texture coordinates. If a geometric transform is to be applied to the primitive, it would naturally be represented as a uniform variable.

It is important to understand, however, that WebGL does not come with any predefined attributes, not even one for vertex coordinates. In the programmable pipeline, the attributes and uniforms that are used are entirely up to the programmer. As far as WebGL is concerned, attributes are just values that are passed into the vertex shader. Uniforms can be passed into the vertex shader, the fragment shader, or both. WebGL does not assign a meaning to the values. The meaning is entirely determined by what the shaders do with the values. The set of attributes and uniforms that are used in drawing a primitive is determined by the source code of the shaders that are in use when the primitive is drawn.

To understand this, we need to look at what happens in the pipeline in a more detail. When drawing a primitive, the JavaScript program specifies values for any attributes and uniforms in the shader program. For each attribute, it will specify an array of values, one for each vertex. For each uniform, it will specify a single value. It must send these values to the GPU before drawing the primitive. The primitive can then be drawn by calling a single JavaScript function. At that point, the GPU takes over, and executes the shader programs. When drawing the primitive, the GPU calls the vertex shader once for each vertex. The attribute values for the vertex that is to be processed are passed as input into the vertex shader. Values of uniform variables are also passed to the vertex shader. The way this works is that both attributes and uniforms are represented as global variables in the vertex shader program. Before calling the shader for a given vertex, the GPU sets the values of those variables appropriately for that specific vertex.

As one of its outputs, the vertex shader must specify the coordinates of the vertex in the clip coordinate system (see Subsection 3.3.1). It does that by assigning a value to a special variable named gl_Position. The position is often computed by applying a transformation to the attribute that represents the coordinates in the object coordinate system, but exactly how the position is computed is up to the programmer.

After the positions of all the vertices in the primitive have been computed, a fixed-function stage in the pipeline clips away the parts of the primitive whose coordinates are outside the range of valid clip coordinates (−1 to 1 along each coordinate axis). The primitive is then rasterized; that is, it is determined which pixels lie inside the primitive. The GPU then calls the fragment shader once for each pixel that lies in the primitive. The fragment shader has access to uniform variables (but not attributes). It can also use a special variable named gl_FragCoord that contains the clip coordinates of the pixel. Pixel coordinates are computed by interpolating the values of gl_Position that were specified by the vertex shader. The interpolation is done by another fixed-function stage that comes between the vertex shader and the fragment shader.

Other quantities besides coordinates can work in much that same way. That is, the vertex shader computes a value for the quantity at each vertex of a primitive. An interpolator takes the values at the vertices and computes a value for each pixel in the primitive. The value for a given pixel is then input into the fragment shader when the shader is called to process that pixel. For example, color in OpenGL follows this pattern: The color of an interior pixel of a primitive is computed by interpolating the color at the vertices. In GLSL, this pattern is implemented using varying variables.

A varying variable is declared both in the vertex shader and in the fragment shader. The vertex shader is responsible for assigning a value to the varying variable. Each vertex of a primitive can assign a different value to the variable. The interpolator takes all the values produced by executing the vertex shader for each vertex of the primitive, and it interpolates those values to produce a value for each pixel. When the fragment shader is executed for a given pixel, the value of the varying variable is the interpolated value for that pixel. The fragment shader can use the value in its own computations.

Varying variables exist to communicate data from the vertex shader to the fragment shader. They are defined in the shader source code. They are not used or referred to in the JavaScript side of the API. Note that it is entirely up to the programmer to decide what varying variables to define and what to do with them.

We have almost gotten to the end of the pipeline. After all that, the job of the fragment shader is simply to specify a color for the pixel. It does that by assigning a value to a special variable named gl_FragColor. That value will then be used in the remaining fixed-function stages of the pipeline.

To summarize: The JavaScript side of the program sends values for attributes and uniform variables to the GPU and then issues a command to draw a primitive. The GPU executes the vertex shader once for each vertex. The vertex shader can use the values of attributes and uniforms. It assigns values to gl_Position and to any varying variables that exist in the shader. After clipping, rasterization, and interpolation, the GPU executes the fragment shader once for each pixel in the primitive. The fragment shader can use the values of varying variables, uniform variables, and gl_FragCoord. It computes a value for gl_FragColor. This diagram summarizes the flow of data:

123

The diagram is not complete. There are a few more special variables that I haven't mentioned. And there is the important question of how textures are used. But if you understand the diagram, you have a good start on understanding WebGL.


For GLSL ES 3.00, the same diagram applies, except that there is no special variable gl_FragColor. Instead, the fragment shader must define its own output variable to represent the color. In GLSL ES 1.00, the words "attribute" and "varying" are used when declaring variables in the actual shader program source code. In source code for the 3.00 version, attribute variables become "in" variables, since they are inputs to the vertex shader, and varying variables become "out" variables in the vertex shader and "in" variables in the fragment shader. And the variable gl_FragColor is replaced by an "out" variable in the fragment shader. The use of the terms "in" and "out" are actually more appropriate to systems with additional pipeline stages, where "out" variables from one stage can become "in" variables to the next stage. In any case, people still use the terms attribute and varying when discussing WebGL, even if it is using GLSL ES 3.00.

6.1.4 统一变量的值

Values for Uniform Variables

是时候开始看一些实际的WebGL代码了。我们将首先关注JavaScript方面,但你还需要了解一些GLSL的基本知识。GLSL有一些熟悉的基本数据类型:floatintbool。但它也有一些新的预定义数据类型来表示向量和矩阵。例如,数据类型vec3表示3D中的向量。vec3变量的值是三个浮点数的列表。类似地,还有数据类型vec2vec4来表示2D和4D向量。

在顶点着色器中的全局变量声明可以标记为attributevaryinguniform(或者在GLSL ES 3.00中作为inoutuniform,但再说一次,我们将暂时坚持使用1.00版本)。没有任何这些修饰符的变量声明定义了一个局部于顶点着色器的变量。片段着色器中的全局变量可以是uniformvarying,也可以不加修饰符声明。应在两个着色器中声明变化变量,使用相同的名称和类型。这允许GLSL编译器确定在着色器程序中使用了哪些属性、统一和变化变量。

程序的JavaScript方面需要一种方法来引用特定的属性和统一变量。函数gl.getUniformLocation可以用来获取着色器程序中统一变量的引用,其中gl指的是WebGL图形上下文。它接受两个参数:由gl.createProgram返回的编译程序的标识符,以及着色器源代码中统一变量的名称。例如,如果prog标识了一个具有名为color的统一变量的着色器程序,那么可以使用以下JavaScript语句获取color变量的位置:

colorUniformLoc = gl.getUniformLocation( prog, "color" );

然后可以使用位置colorUniformLoc来设置统一变量的值。例如:

gl.uniform3f( colorUniformLoc, 1, 0, 0 );

函数gl.uniform3f是一系列可以作为gl.uniform组的函数之一。这类似于OpenGL 1.1中的glVertex系列。代表一个后缀,表示为变量提供的值的数量和类型。在这种情况下,gl.uniform3f接受三个浮点值,适用于设置类型为vec3的统一变量的值。值的数量可以是1、2、3或4。类型可以是“f”表示浮点数或“i”表示整数。(对于布尔统一变量,应使用gl.uniform1i并将0传递以表示false或1以表示true*。)如果在后缀中添加“v”,则值以数组的形式传递。例如,

gl.uniform3fv( colorUniformLoc, [ 1, 0, 0 ] );

还有另一组函数用于设置统一矩阵变量的值。我们稍后会讲到。

统一变量的值可以在着色器程序编译后的任何时间设置,并且该值一直有效,直到通过另一次调用gl.uniform*来更改。

如果传递给gl.getUniformLocation作为第二个参数的字符串不是着色器程序中的统一变量名称,那么返回值将是null。如果统一变量在着色器源代码中声明但不在程序中“活跃”,则返回值也可能是null。一个声明但没有实际使用的变量不是活跃的,它在编译程序中不会获得位置。这偶尔给我带来了问题,当我为了调试目的注释掉着色器程序的一部分时,并不小心通过这样做使一个变量变为非活跃状态。

It's time to start looking at some actual WebGL code. We will concentrate on the JavaScript side first, but you need to know a little about GLSL. GLSL has some familiar basic data types: float, int, and bool. But it also has some new predefined data types to represent vectors and matrices. For example, the data type vec3 represents a vector in 3D. The value of a vec3 variable is a list of three floating-point numbers. Similarly, there are data types vec2 and vec4 to represent 2D and 4D vectors.

Global variable declarations in a vertex shader can be marked as attribute, varying, or uniform (or as in, out, or uniform in GLSL ES 3.00, but again, we will stick to the 1.00 version for the time being). A variable declaration with none of these modifiers defines a variable that is local to the vertex shader. Global variables in a fragment can optionally be uniform or varying, or they can be declared without a modifier. A varying variable should be declared in both shaders, with the same name and type. This allows the GLSL compiler to determine what attribute, uniform, and varying variables are used in a shader program.

The JavaScript side of the program needs a way to refer to particular attributes and uniform variables. The function gl.getUniformLocation can be used to get a reference to a uniform variable in a shader program, where gl refers to the WebGL graphics context. It takes as parameters the identifier for the compiled program, which was returned by gl.createProgram, and the name of the uniform variable in the shader source code. For example, if prog identifies a shader program that has a uniform variable named color, then the location of the color variable can be obtained with the JavaScript statement

colorUniformLoc = gl.getUniformLocation( prog, "color" );

The location colorUniformLoc can then be used to set the value of the uniform variable. For example:

gl.uniform3f( colorUniformLoc, 1, 0, 0 );

The function gl.uniform3f is one of a family of functions that can be referred to as a group as gl.uniform*. This is similar to the family glVertex* in OpenGL 1.1. The * represents a suffix that tells the number and type of values that are provided for the variable. In this case, gl.uniform3f takes three floating point values, and it is appropriate for setting the value of a uniform variable of type vec3. The number of values can be 1, 2, 3, or 4. The type can be "f" for floating point or "i" for integer. (For a boolean uniform, you should use gl.uniform1i and pass 0 to represent false or 1 to represent true.) If a "v" is added to the suffix, then the values are passed in an array. For example,

gl.uniform3fv( colorUniformLoc, [ 1, 0, 0 ] );

There is another family of functions for setting the value of uniform matrix variables. We will get to that later.

The value of a uniform variable can be set any time after the shader program has been compiled, and the value remains in effect until it is changed by another call to gl.uniform*.

If the string that is passed as the second parameter gl.getUniformLocation is not the name of a uniform variable in the shader programs, then the return value is null. The return value can also be null if the uniform variable is declared in the shader source code but is not "active" in the program. A variable that is declared but not actually used is not active, and it does not get a location in the compiled program. This has occasionally caused problems for me, when I commented out part of a shader program for debugging purposes, and accidentally made a variable inactive by doing so.

6.1.5 属性值

Values for Attributes

现在让我们来看看一些实际的WebGL代码。我们将首先关注JavaScript方面,但你还需要了解一些GLSL的基本知识。GLSL有一些熟悉的基本数据类型:floatintbool。但它也有一些新的预定义数据类型来表示向量和矩阵。例如,数据类型vec3表示3D中的向量。vec3变量的值是三个浮点数的列表。类似地,还有数据类型vec2vec4来表示2D和4D向量。

在顶点着色器中的全局变量声明可以标记为attributevaryinguniform(或者在GLSL ES 3.00中作为inoutuniform,但再说一次,我们将暂时坚持使用1.00版本)。没有任何这些修饰符的变量声明定义了一个局部于顶点着色器的变量。片段着色器中的全局变量可以是uniformvarying,也可以不加修饰符声明。应在两个着色器中声明变化变量,使用相同的名称和类型。这允许GLSL编译器确定在着色器程序中使用了哪些属性、统一和变化变量。

程序的JavaScript方面需要一种方法来引用特定的属性和统一变量。函数gl.getUniformLocation可以用来获取着色器程序中统一变量的引用,其中gl指的是WebGL图形上下文。它接受两个参数:由gl.createProgram返回的编译程序的标识符,以及着色器源代码中统一变量的名称。例如,如果prog标识了一个具有名为color的统一变量的着色器程序,那么可以使用以下JavaScript语句获取color变量的位置:

colorUniformLoc = gl.getUniformLocation( prog, "color" );

然后可以使用位置colorUniformLoc来设置统一变量的值。例如:

gl.uniform3f( colorUniformLoc, 1, 0, 0 );

函数gl.uniform3f是一系列可以作为gl.uniform组的函数之一。这类似于OpenGL 1.1中的glVertex系列。代表一个后缀,表示为变量提供的值的数量和类型。在这种情况下,gl.uniform3f接受三个浮点值,适用于设置类型为vec3的统一变量的值。值的数量可以是1、2、3或4。类型可以是“f”表示浮点数或“i”表示整数。(对于布尔统一变量,应使用gl.uniform1i并将0传递以表示false或1以表示true*。)如果在后缀中添加“v”,则值以数组的形式传递。例如,

gl.uniform3fv( colorUniformLoc, [ 1, 0, 0 ] );

还有另一组函数用于设置统一矩阵变量的值。我们稍后会讲到。

统一变量的值可以在着色器程序编译后的任何时间设置,并且该值一直有效,直到通过另一次调用gl.uniform*来更改。

如果传递给gl.getUniformLocation作为第二个参数的字符串不是着色器程序中的统一变量名称,那么返回值将是null。如果统一变量在着色器源代码中声明但不在程序中“活跃”,则返回值也可能是null。一个声明但没有实际使用的变量不是活跃的,它在编译程序中不会获得位置。这偶尔给我带来了问题,当我为了调试目的注释掉着色器程序的一部分时,并不小心通过这样做使一个变量变为非活跃状态。

现在让我们来看看属性,情况就更加复杂了,因为属性在原语中的每个顶点可以取不同的值。基本思想是在单个操作中将属性的完整数据集从JavaScript数组复制到GPU可以访问的内存中。不幸的是,设置使该操作成为可能的过程并不简单。

首先,普通的JavaScript数组不适合这个目的。为了效率,我们需要数据存储在连续内存位置的数值块中,而常规的JavaScript数组没有这种形式。为了解决这个问题,JavaScript引入了一种新型数组,称为类型化数组。我们在第5章中使用three.js时遇到了类型化数组。在5.1.4小节中有类型化数组的简短介绍。类型化数组有固定长度,在创建时分配,并且只能保存指定类型的数字。不同类型的类型化数组用于不同类型的数值数据。现在我们将使用Float32Array,它保存32位浮点数。一旦你有了类型化数组,你可以像使用常规数组一样使用它,但是当你给Float32Array的元素分配任何值时,该值会被转换为32位浮点数。如果值不能被解释为数字,它将被转换为NaN,即“不是一个数字”的值。

在数据可以从JavaScript传输到属性变量之前,它必须被放置到类型化数组中。如果可能的话,为了效率,你应该直接使用类型化数组,而不是使用常规的JavaScript数组,然后复制数据到类型化数组中。


在WebGL中使用属性数据,必须将其传输到VBO(顶点缓冲对象)。VBO在OpenGL 1.5中引入,并在3.4.4小节中简要讨论过。VBO是GPU可以访问的内存块。要使用VBO,你必须首先调用函数gl.createBuffer()来创建它。例如,

colorBuffer = gl.createBuffer();

在将数据传输到VBO之前,你必须“绑定”VBO

gl.bindBuffer( gl.ARRAY_BUFFER, colorBuffer );

gl.bindBuffer的第一个参数称为“目标”。它指定了VBO的用途。目标gl.ARRAY_BUFFER用于存储属性的值。一次只能将一个VBO绑定到给定目标。

传输数据到VBO的函数没有提到VBO——而是使用当前绑定的VBO。要将数据复制到该缓冲区,使用gl.bufferData()。例如:

gl.bufferData(gl.ARRAY_BUFFER, colorArray, gl.STATIC_DRAW);

第一个参数再次是目标。数据被传输到绑定到该目标的VBO。第二个参数是在JavaScript方面持有数据的类型化数组。数组中的所有元素都被复制到缓冲区,数组的大小决定了缓冲区的大小。注意,这是原始数据字节的直接传输;WebGL不记得数据是否表示浮点数或整数或其他类型的数据。

gl.bufferData的第三个参数是常量gl.STATIC_DRAWgl.STREAM_DRAWgl.DYNAMIC_DRAW之一。它是对WebGL的提示,说明数据将如何使用,它帮助WebGL以最有效的方式管理数据。值gl.STATIC_DRAW意味着您打算多次使用数据而不更改它。例如,如果您将在整个程序中使用相同的数据,您可以在初始化期间使用gl.STATIC_DRAW将其加载到缓冲区一次。WebGL可能会将数据存储在图形卡本身上,以便图形硬件可以最快速地访问。第二个值gl.STEAM_DRAW用于仅使用一次或最多几次的数据。(当需要时,它可以“流式传输”到卡上。)值gl.DYNAMIC_DRAW介于其他两个值之间;它适用于将多次使用但会修改的数据。


将属性数据放入VBO只是故事的一部分。您还必须告诉WebGL将VBO用作属性的值源。首先,您需要知道属性在着色器程序中的位置。您可以使用gl.getAttribLocation来确定。例如,

colorAttribLoc = gl.getAttribLocation(prog, "a_color");

这假设prog是着色器程序,“a_color”是顶点着色器中的属性变量的名称。这与gl.getUniformLocation完全类似(除了返回值是整数,如果请求的属性不存在或不活跃,则为-1)。

尽管属性通常在不同顶点处取不同的值,但有可能在每个顶点处使用相同的值。事实上,这是默认行为。可以通过gl.vertexAttrib系列函数为所有顶点设置单一属性值,它们的工作方式类似于gl.uniform**。在更常见的情况下,您想要从VBO中获取属性的值,您必须启用该属性的VBO使用。这可以通过调用

gl.enableVertexAttribArray( colorAttribLoc );

来完成,参数是属性在着色器程序中的位置,由gl.getAttribLocation()调用返回。此命令与任何特定的VBO无关。它只是打开了指定属性的缓冲区使用。通常,在初始化期间只调用这个方法一次是合理的。可以通过调用

gl.disableVertexAttribArray( colorAttribLoc );

来关闭VBO数据的使用。最后,在您绘制使用VBO属性数据的原语之前,您必须告诉WebGL哪个缓冲区包含数据以及如何解释该缓冲区中的位。这可以通过gl.vertexAttribPointer()来完成。调用此函数时,VBO必须绑定到ARRAY_BUFFER目标。例如,

gl.bindBuffer( gl.ARRAY_BUFFER, colorBuffer );
gl.vertexAttribPointer( colorAttribLoc, 3, gl.FLOAT, false, 0, 0 );

假设colorBuffer指代VBO,而colorAttribLoc是属性的位置,这告诉WebGL从该缓冲区获取属性的值。通常,你会在调用gl.vertexAttribPointer()之前调用gl.bindBuffer(),但如果所需的缓冲区已经被绑定,那就不必了。

gl.vertexAttribPointer的第一个参数是属性位置。第二个是每个顶点的值的数量。例如,如果你为vec2提供值,第二个参数将是2,你将为每个顶点提供两个数字;对于vec3,第二个参数将是3;对于float,它将是1。第三个参数指定了每个值的类型。在这里,gl.FLOAT表示每个值是一个32位浮点数。其他值包括gl.BYTEgl.UNSIGNED_BYTEgl.UNSIGNED_SHORTgl.SHORT,用于整数值。请注意,在WebGL 1.0中,所有属性都是浮点值;如果你为属性提供整数值,它们将被转换为浮点数。参数值应与缓冲区中的数据类型相匹配。例如,如果数据来自Float32Array,那么参数应该是gl.FLOAT。在调用gl.vertexAttribPointer时,我总是会使用false、0和0作为最后三个参数。这些参数增加了我不需要的灵活性;如果你感兴趣,可以在文档中查找它们。(false参数与整数值如何转换为浮点值有关。)

在WebGL 2.0中,属性变量可以是整型。当使用gl.vertexAttribPointer()配置属性时,为属性提供的值总是被转换为浮点数,所以它不适合整型属性。对于整型属性的使用,WebGL 2.0引入了一个新函数gl.vertexAttribIPointer(),它正确地处理整型数据。

这里有很多东西需要理解。使用VBO为属性提供值需要六个单独的命令,这是在生成数据并将其放入类型化数组之外的。这里是全部的命令集:

colorAttribLoc = gl.getAttribLocation( prog, "a_color" );
colorBuffer = gl.createBuffer();
gl.enableVertexAttribArray( colorAttribLoc );

gl.bindBuffer( gl.ARRAY_BUFFER, colorBuffer );
gl.vertexAttribPointer( colorAttribLoc, 3, gl.FLOAT, false, 0, 0 );
gl.bufferData( gl.ARRAY_BUFFER, colorArray, gl.STATIC_DRAW );

然而,这六个命令并不总是在JavaScript代码的同一点出现。前三个命令通常作为初始化的一部分完成。gl.bufferData将在需要更改属性数据时调用;它可能只在初始化期间使用一次,或者在需要修改数据时使用。gl.bindBuffer必须在调用gl.vertexAttribPointergl.bufferData之前调用,因为它建立了这两个命令使用的VBO。记住,所有这些都必须为着色器程序中使用的每个属性完成。

Turning now to attributes, the situation is more complicated, because an attribute can take a different value for each vertex in a primitive. The basic idea is that the complete set of data for the attribute is copied in a single operation from a JavaScript array into memory that is accessible to the GPU. Unfortunately, setting things up to make that operation possible is nontrivial.

First of all, a regular JavaScript array is not suitable for this purpose. For efficiency, we need the data to be in a block of memory holding numerical values in successive memory locations, and regular JavaScript arrays don't have that form. To fix this problem, a new kind of array, called typed arrays, was introduced into JavaScript. We encountered typed arrays when working with three.js in the Chapter 5. There is a short introduction to typed arrays in Subsection 5.1.4. A typed array has a fixed length, which is assigned when it is created, and it can only hold numbers of a specified type. There are different kinds of typed array for different kinds of numerical data. For now we will use Float32Array, which holds 32-bit floating point numbers. Once you have a typed array, you can use it much like a regular array, but when you assign any value to an element of a Float32Array, the value is converted into a 32-bit floating point number. If the value cannot be interpreted as a number, it will be converted to NaN, the "not-a-number" value.

Before data can be transferred from JavaScript into an attribute variable, it must be placed into a typed array. When possible, for efficiency, you should work with typed arrays directly, rather than working with regular JavaScript arrays and then copying the data into typed arrays.


For use in WebGL, the attribute data must be transferred into a VBO (vertex buffer object). VBOs were introduced in OpenGL 1.5 and were discussed briefly in Subsection 3.4.4. A VBO is a block of memory that is accessible to the GPU. To use a VBO, you must first call the function gl.createBuffer() to create it. For example,

colorBuffer = gl.createBuffer();

Before transferring data into the VBO, you must "bind" the VBO:

gl.bindBuffer( gl.ARRAY_BUFFER, colorBuffer );

The first parameter to gl.bindBuffer is called the "target." It specifies how the VBO will be used. The target gl.ARRAY_BUFFER is used when the buffer is being used to store values for an attribute. Only one VBO at a time can be bound to a given target.

The function that transfers data into a VBO doesn't mention the VBO—instead, it uses the VBO that is currently bound. To copy data into that buffer, use gl.bufferData(). For example:

gl.bufferData(gl.ARRAY_BUFFER, colorArray, gl.STATIC_DRAW);

The first parameter is, again, the target. The data is transferred into the VBO that is bound to that target. The second parameter is the typed array that holds the data on the JavaScript side. All the elements of the array are copied into the buffer, and the size of the array determines the size of the buffer. Note that this is a straightforward transfer of raw data bytes; WebGL does not remember whether the data represents floats or ints or some other kind of data.

The third parameter to gl.bufferData is one of the constants gl.STATIC_DRAW, gl.STREAM_DRAW, or gl.DYNAMIC_DRAW. It is a hint to WebGL about how the data will be used, and it helps WebGL to manage the data in the most efficient way. The value gl.STATIC_DRAW means that you intend to use the data many times without changing it. For example, if you will use the same data throughout the program, you can load it into a buffer once, during initialization, using gl.STATIC_DRAW. WebGL will probably store the data on the graphics card itself where it can be accessed most quickly by the graphics hardware. The second value, gl.STEAM_DRAW, is for data that will be used only once, or at most a few times. (It can be "streamed" to the card when it is needed.) The value gl.DYNAMIC_DRAW is somewhere between the other two values; it is meant for data that will be used multiple times, but with modifications.


Getting attribute data into VBOs is only part of the story. You also have to tell WebGL to use the VBO as the source of values for the attribute. To do so, first of all, you need to know the location of the attribute in the shader program. You can determine that using gl.getAttribLocation. For example,

colorAttribLoc = gl.getAttribLocation(prog, "a_color");

This assumes that prog is the shader program and "a_color" is the name of the attribute variable in the vertex shader. This is entirely analogous to gl.getUniformLocation (except that the return value is an integer, and is -1 if the requested attribute does not exist or is not active).

Although an attribute usually takes different values at different vertices, it is possible to use the same value at every vertex. In fact, that is the default behavior. The single attribute value for all vertices can be set using the family of functions gl.vertexAttrib*, which work similarly to gl.uniform*. In the more usual case, where you want to take the values of an attribute from a VBO, you must enable the use of a VBO for that attribute. This is done by calling

gl.enableVertexAttribArray( colorAttribLoc );

where the parameter is the location of the attribute in the shader program, as returned by a call to gl.getAttribLocation(). This command has nothing to do with any particular VBO. It just turns on the use of buffers for the specified attribute. Often, it is reasonable to call this method just once, during initialization. Use of data from the VBO can be turned off by calling

gl.disableVertexAttribArray( colorAttribLoc );

Finally, before you draw a primitive that uses the attribute data from a VBO, you have to tell WebGL which buffer contains the data and how the bits in that buffer are to be interpreted. This is done with gl.vertexAttribPointer(). The VBO must be bound to the ARRAY_BUFFER target when this function is called. For example,

gl.bindBuffer( gl.ARRAY_BUFFER, colorBuffer );
gl.vertexAttribPointer( colorAttribLoc, 3, gl.FLOAT, false, 0, 0 );

Assuming that colorBuffer refers to the VBO and colorAttribLoc is the location of the attribute, this tells WebGL to take values for the attribute from that buffer. Often, you will call gl.bindBuffer() just before calling gl.vertexAttribPointer(), but that is not necessary if the desired buffer is already bound.

The first parameter to gl.vertexAttribPointer is the attribute location. The second is the number of values per vertex. For example, if you are providing values for a vec2, the second parameter will be 2 and you will provide two numbers per vertex; for a vec3, the second parameter would be 3; for a float, it would be 1. The third parameter specifies the type of each value. Here, gl.FLOAT indicates that each value is a 32-bit floating point number. Other values include gl.BYTE, gl.UNSIGNED_BYTE, gl.UNSIGNED_SHORT, and gl.SHORT for integer values. Note that in WebGL 1.0, all attributes are floating point values; if you provide integer values for an attribute, they will be converted to floating point. The parameter value should match the data type in the buffer. For example, if the data came from a Float32Array, then the parameter should be gl.FLOAT. For the last three parameters in a call to gl.vertexAttribPointer, I will always use false, 0, and 0. These parameters add flexibility that I won't need; you can look them up in the documentation if you are interested. (The false parameter has to do with how integer values are converted into floating point values.)

In WebGL 2.0, attribute variables can have integer type. When gl.vertexAttribPointer() is used to configure an attribute, the values provided for the attribute will always be converted to floating point, so it is inappropriate for integer-valued attributes. For use with integer-valued attributes, WebGL 2.0 introduces a new function, gl.vertexAttribIPointer() that works correctly with integer data.

There is a lot to take in here. Using a VBO to provide values for an attribute requires six separate commands, and that is in addition to generating the data and placing it in a typed array. Here is the full set of commands:

colorAttribLoc = gl.getAttribLocation( prog, "a_color" );
colorBuffer = gl.createBuffer();
gl.enableVertexAttribArray( colorAttribLoc );

gl.bindBuffer( gl.ARRAY_BUFFER, colorBuffer );
gl.vertexAttribPointer( colorAttribLoc, 3, gl.FLOAT, false, 0, 0 );
gl.bufferData( gl.ARRAY_BUFFER, colorArray, gl.STATIC_DRAW );

However, the six commands will not always occur at the same point in the JavaScript code. The first three commands are often done as part of initialization. gl.bufferData would be called whenever the data for the attribute needs to be changed; it might be used just once during initialization, or it might be used whenever the data needs to be modified. gl.bindBuffer must be called before gl.vertexAttribPointer or gl.bufferData, since it establishes the VBO that is used by those two commands. Remember that all of this must be done for every attribute that is used in the shader program.

6.1.6 绘制基元

Drawing a Primitive

在着色器程序被创建并且为统一变量和属性设置了值之后,绘制一个原语只需要再一个命令。一种方法是使用函数gl.drawArrays

gl.drawArrays(primitiveType, startVertex, vertexCount);

第一个参数是七个常量之一,用于标识WebGL原语类型,例如gl.TRIANGLESgl.LINE_LOOPgl_POINTS。第二个和第三个参数是整数,用于确定用于原语的顶点的子集。在调用gl.drawArrays之前,你会将一些顶点的属性值放入一个或多个VBO中。当原语被渲染时,启用的属性的属性值会从VBO中获取。startVertex是在VBO中数据的起始顶点号,vertexCount是原语中的顶点数。通常,startVertex是零,vertexCount是可用数据的顶点总数。例如,绘制一个单独三角形的命令可能是:

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

gl.drawArraysgl.ARRAY_BUFFER中“array”这个词可能有点令人困惑,因为数据存储在顶点缓冲对象中,而不是JavaScript数组中。当glDrawArrays最初在OpenGL 1.1中引入时,它使用的是普通数组,而不是VBO。从OpenGL 1.5开始,glDrawArrays可以与普通数组或VBO一起使用。在WebGL中,放弃了对普通数组的支持,gl.drawArrays只能与VBO一起工作,即使名称仍然提到数组。

我们在3.4.2小节中遇到了glDrawArrays的原始版本。该部分还介绍了用于绘制原语的另一种函数glDrawElements,它可以用于绘制索引面集。在WebGL中也可用gl.drawElements函数。使用gl.drawElements时,属性数据不是按照在VBO中的顺序使用的。相反,有一个单独的索引列表,确定访问数据的顺序。

要使用gl.drawElements,需要一个额外的VBO来保存索引列表。当用于此目的时,VBO必须绑定到目标gl.ELEMENT_ARRAY_BUFFER而不是gl.ARRAY_BUFFER。VBO将保存整数值,可以是gl.UNSIGNED_BYTEgl.UNSIGNED_SHORT类型(或者对于WebGL 2.0,是gl.UNSIGNED_INT)。值可以从JavaScript类型化数组加载,对于gl.UNSIGNED_BYTE使用Uint8Array,对于gl.UNSIGNED_SHORT使用Uint16Array。创建VBO并用数据填充它是一个多步骤过程。例如:

elementBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementBuffer);
let data = new Uint8Array([2, 0, 3, 2, 1, 3, 1, 4, 3]);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STREAM_DRAW);

假设属性数据也已经加载到VBO中,然后可以使用gl.drawElements来绘制原语。调用gl.drawElements的形式是

gl.drawElements(primitiveType, count, dataType, startByte);

当调用这个函数时,包含顶点索引的VBO必须绑定到ELEMENT_ARRAY_BUFFER目标。gl.drawElements的第一个参数是原语类型,如gl.TRIANGLE_FAN。计数是原语中的顶点数。dataType指定了加载到VBO中的数据类型;它将是gl.UNSIGNED_SHORTgl.UNSIGNED_BYTEstartByte是VBO中原语数据的起始点;它通常是零。(注意,起始点以字节为单位给出,而不是顶点号。)一个典型的例子是

gl.drawElements(gl.TRIANGLES, 9, gl.UNSIGNED_BYTE, 0);

我们将在后面的场合使用这个函数。如果你发现它令人困惑,你应该回顾3.4.2小节。WebGL中的情况与OpenGL 1.1中的情况非常相似。

After the shader program has been created and values have been set up for the uniform variables and attributes, it takes just one more command to draw a primitive. One way to do that is with the function gl.drawArrays:

gl.drawArrays( primitiveType, startVertex, vertexCount );

The first parameter is one of the seven constants that identify WebGL primitive types, such as gl.TRIANGLES, gl.LINE_LOOP, and gl_POINTS. The second and third parameters are integers that determine which subset of available vertices is used for the primitive. Before calling gl.drawArrays, you will have placed attribute values for some number of vertices into one or more VBOs. When the primitive is rendered, the attribute values for enabled attributes are pulled from the VBOs. The startVertex is the starting vertex number of the data within the VBOs, and vertexCount is the number of vertices in the primitive. Often, startVertex is zero, and vertexCount is the total number of vertices for which data is available. For example, the command for drawing a single triangle might be

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

The use of the word "array" in gl.drawArrays and gl.ARRAY_BUFFER might be a little confusing, since the data is stored in vertex buffer objects rather than in JavaScript arrays. When glDrawArrays was first introduced in OpenGL 1.1, it used ordinary arrays rather than VBOs. Starting with OpenGL 1.5, glDrawArrays could be used either with ordinary arrays or VBOs. In WebGL, support for ordinary arrays was dropped, and gl.drawArrays can only work with VBOs, even though the name still refers to arrays.

We encountered the original version of glDrawArrays in Subsection 3.4.2. That section also introduced an alternative function for drawing primitives, glDrawElements, which can be used for drawing indexed face sets. A gl.drawElements function is also available in WebGL. With gl.drawElements, attribute data is not used in the order in which it occurs in the VBOs. Instead, there is a separate list of indices that determines the order in which the data is accessed.

To use gl.drawElements, an extra VBO is required to hold the list of indices. When used for this purpose, the VBO must be bound to the target gl.ELEMENT_ARRAY_BUFFER rather than gl.ARRAY_BUFFER. The VBO will hold integer values, which can be of type gl.UNSIGNED_BYTE or gl.UNSIGNED_SHORT (or, for WebGL 2.0, gl.UNSIGNED_INT). The values can be loaded from a JavaScript typed array of type Uint8Array, for gl.UNSIGNED_BYTE, or Uint16Array, for gl.UNSIGNED_SHORT. Creating the VBO and filling it with data is again a multistep process. For example,

elementBuffer = gl.createBuffer();
gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, elementBuffer );
let data = new Uint8Array( [ 2,0,3, 2,1,3, 1,4,3 ] );
gl.bufferData( gl.ELEMENT_ARRAY_BUFFER, data, gl.STREAM_DRAW );

Assuming that the attribute data has also been loaded into VBOs, gl.drawElements can then be used to draw the primitive. A call to gl.drawElements takes the form

gl.drawElements( primitiveType, count, dataType, startByte );

The VBO that contains the vertex indices must be bound to the ELEMENT_ARRAY_BUFFER target when this function is called. The first parameter to gl.drawElements is a primitive type such as gl.TRIANGLE_FAN. The count is the number of vertices in the primitive. The dataType specifies the type of data that was loaded into the VBO; it will be either gl.UNSIGNED_SHORT or gl.UNSIGNED_BYTE. The startByte is the starting point in the VBO of the data for the primitive; it is usually zero. (Note that the starting point is given in terms of bytes, not vertex numbers.) A typical example would be

gl.drawElements( gl.TRIANGLES, 9, gl.UNSIGNED_BYTE, 0 );

We will have occasion to use this function later. If you find it confusing, you should review Subsection 3.4.2. The situation is much the same in WebGL as it was in OpenGL 1.1.

6.1.7 WebGL 2.0:顶点数组对象

WebGL 2.0: Vertex Array Objects

与属性一起工作的大量函数可能看起来有些过分。在绘制几个不同对象的程序中,情况更糟。每个对象可能需要自己的缓冲区和自己的属性指针设置。在绘制每个对象之前,都需要为每个属性调用gl.bindBuffer()gl.vertexAttribPointer()。一个典型的3D图形程序会使用属性来表示顶点坐标、法向量、材质属性和纹理坐标。所以,每个对象都会有大量的函数调用。

为了解决这个问题,WebGL 2.0引入了顶点数组对象(VAOs)。VAO是一块内存区域,通常存储在显卡上。它保存了由渲染函数(如gl.drawArrays())使用的设置。这包括每个属性的启用状态、用于属性数据的缓冲区引用,以及通过调用gl.vertexAttribPointer()设置的所有属性的值。它还包括设置和对由gl.drawElements()使用的缓冲区的引用,以及下一节中讨论的属性除数。

WebGL 2.0有一个默认的VAO,当没有选择其他VAO时,它将使用这个默认VAO。要使用替代VAO,首先需要通过调用gl.createVertexArray()来创建它:

vao = gl.createVertexArray();

返回值vao是已创建VAO的标识符。在新的VAO中,所有属性都具有默认值。特别是,所有顶点属性都被禁用,并且没有关联的缓冲区。要实际使用VAO,需要绑定它:

gl.bindVertexArray(vao);

影响或使用属性的函数适用于当前绑定的VAO。例如,在调用gl.vertexAttribPointer()时的设置存储在当前VAO中。调用gl.drawArrays()时,它需要绘制原语的所有数据都来自当前VAO。程序可以随时通过调用gl.bindVertexArray从一个VAO切换到另一个VAO。要回到使用默认VAO,程序可以调用gl.bindVertexArray(0)

这个想法是,绘制几个对象的程序可以使用每个对象的不同VAO。对象的VAO必须在配置对象的设置时绑定。但在绘制对象之前,程序只需要绑定该对象的VAO。这个单一的函数调用可以替代可能需要恢复每个属性的适当设置的大量函数调用。优势不仅仅是更整洁的程序组织——也更有效率,因为只需要发送一个命令到GPU来配置所有属性。

示例WebGL 2.0程序webgl/VAO-test-webgl2.html为六个不同的对象使用不同的VAO。该程序使用了许多我们尚未涵盖的技术,但你可以查看drawModel()函数,看看它如何使用VAOs和VBOs。

The large number of functions needed to work with attributes can seem excessive. The situation is worse in a program that draws several different objects. Each object can require its own buffers and its own settings for attribute pointers. Before drawing each object, it would be necessary to call gl.bindBuffer() and gl.vertexAttribPointer() for each attribute. A typical 3D graphics program would use attributes for vertex coordinates, normal vectors, material properties, and texture coordinates. So, there would be a lot of function calls for each object.

To help with this situation, WebGL 2.0 introduced Vertex Array Objects (VAOs). A VAO is a section of memory, typically stored on the graphics card. It holds settings that are used by rendering functions such as gl.drawArrays(). This includes the enabled state of each attribute, references to the buffers used for the attribute data, and the values of all properties that are set by calling gl.vertexAttribPointer(). It also includes the settings and a reference to the buffer used by gl.drawElements(), as well as the attribute divisors that are discussed in the next subsection.

WebGL 2.0 has a default VAO, which it uses when no other VAO has been selected. To use an alternative VAO, you first have to create it, by calling gl.createVertexArray():

vao = gl.createVertexArray();

The return value, vao, is an identifier for the VAO that has been created. In the new VAO, all properties have their default values. In particular, all vertex attributes are disabled and have no associated buffers. To actually use a VAO, you need to bind it:

gl.bindVertexArray(vao);

Functions that affect or use attributes apply to the VAO that is currently bound. For example, the settings in a call to gl.vertexAttribPointer() are stored in the current VAO. And a call to gl.drawArrays() gets all the data that it needs to draw a primitive from the current VAO. A program can switch from one VAO to another at any time simply by calling gl.bindVertexArray. To go back to using the default VAO, a program can call gl.bindVertexArray(0).

The idea is that a program that draws several objects can use a different VAO for each object. The VAO for an object must be bound when the settings for the object are configured. But before drawing the object, the program simply needs to bind the VAO for that object. That single function call replaces a potentially a large number of function calls that would be needed to restore the appropriate settings for each attribute individually. The advantage is more than just a more nicely organized program—it is also much more efficient, since only one command needs to be sent to the GPU to configure all of the attributes.

The sample WebGL 2.0 program webgl/VAO-test-webgl2.html uses a different VAO for each of six different objects. That program uses many techniques that we have not yet covered, but you can look at the drawModel() function to see how it uses VAOs and VBOs.

6.1.8 WebGL 2.0:实例化绘图

WebGL 2.0: Instanced Drawing

在一个场景中,通常包含使用相同顶点坐标的同一原语的多个副本,但每个副本具有不同的变换、颜色或其他属性。WebGL 2.0使得只需一个函数调用即可绘制所有这些副本。这称为实例化绘制或实例化,原语的个别副本称为实例。使用实例化绘制的函数是gl.drawArraysInstanced()gl.drawElementsInstanced()

实例化属性——从一个实例到另一个实例变化的属性——当分别绘制每个实例时,很可能是统一变量。也就是说,每个实例都有一个适用于实例中所有顶点的属性值。尽管如此,属性在着色器程序中由属性变量表示,而不是统一变量,并且它们被配置为属性。

要指定一个属性是实例化属性,只需要为该属性指定一个“除数”。这是通过调用gl.vertexAttribDivisor完成的:

gl.vertexAttribDivisor(attribID, divisor);

这里,attribID是由gl.getAttribLocation()返回的属性标识符。除数是一个非负整数。将除数传递为零将关闭该属性的实例化。如果divisor是正数,那么属性的每个值将适用于那么多实例。例如,如果divisor是3,那么属性值数组中的第一个条目适用于第一、第二和第三个实例;数组中的第二个值适用于第四、第五和第六个实例;以此类推。实际上,除数的值通常是1,这意味着每个实例在属性值数组中都有自己的条目。

对于实例化属性,除了设置除数之外,还需要启用属性,将数据加载到VBO中,并使用gl.vertexAttribPointer进行配置。当然,还需要使用gl.drawArraysInstanced()gl.drawElementsInstanced()绘制原语,而不是使用gl.drawArrays()gl.drawElements()

示例WebGL 2.0程序webgl/instancing-test-webgl2.html是实例化绘制的一个例子。(再说一次,程序中有很多内容在我们覆盖更多WebGL之前你是不会理解的)。该程序绘制了30个彩色圆盘,其中圆盘由gl.TRIANGLE_FAN类型的原语近似。使用了三个属性:一个属性保存顶点的坐标,一个实例化属性保存圆盘的颜色,另一个实例化属性保存每个圆盘的不同平移。

程序中另一个有趣的点是它对顶点缓冲对象的使用。圆盘可以被动画化。圆盘在移动,但它们的颜色不会改变。由于颜色不会改变,圆盘的颜色值在程序初始化期间一次性加载到VBO中。gl.bufferData中的使用参数设置为gl.STATIC_DRAW,因为数据不会被修改。然而,由于圆盘在移动,圆盘的平移值必须在每一帧中更改。所以,每一帧都会将新数据加载到相应的VBO中,使用gl.STREAM_DRAW,因为正在加载的数据只会使用一次。

最后,我应该注意,VAO和实例化需要WebGL 2.0,但许多WebGL 1.0的实现中也提供了相同的功能作为可选扩展。WebGL扩展将在第7.5节中讨论。

It's common for a scene to contain multiple copies of the same primitive (that is, using the same vertex coordinates), but with different transformations, colors, or other properties for each copy. WebGL 2.0 makes it possible to draw all those copies with a single function call. This is called instanced drawing or instancing, and the individual copies of the primitive are called instances. The functions that use instanced drawing are gl.drawArraysInstanced() and gl.drawElementsInstanced().

Instanced properties—the properties that vary from one instance to another—are things that would likely be uniform variables when drawing each instance separately. That is, each instance gets just one value of the property that applies to all the vertices of the instance. Nevertheless, the properties are represented by attribute variables in the shader program, not uniform variables, and they are configured as attributes.

To specify that an attribute is an instanced property, you just need to specify a "divisor" for that attribute. This is done by calling gl.vertexAttribDivisor:

g.vertexAttribDivisor( attribID, divisor );

Here, attribID is the identifier for the attribute, as returned by gl.getAttribLocation(). The divisor is a non-negative integer. Passing zero as the divisor will turn off instancing for the attribute. If divisor is positive, then each value of the attribute will apply to that many instances. For example, if divisor is 3, then the first entry in the attribute value array applies to the first, second, and third instances; the second value in the array applies to the fourth, fifth, and sixth instances; and so on. In practice, the value of divisor is usually one, meaning that each instance has its own entry in the attribute value array.

For an instanced property, in addition to setting the divisor, it is still necessary to enable the attribute, load data for it into a VBO, and configure it with gl.vertexAttribPointer. And, of course, it is necessary to draw the primitive using gl.drawArraysInstanced() or gl.drawElementsInstanced(), and not with gl.drawArrays() or gl.drawElements().

The sample WebGL 2.0 program webgl/instancing-test-webgl2.html is an example of instanced drawing. (Again, there is a lot in the program that you won't understand until we have covered more of WebGL). The program draws 30 colored disks, where a disk is approximated by a primitive of type gl.TRIANGLE_FAN. Three attributes are used: an attribute that holds the coordinates of the vertices, an instanced attribute that holds the colors for the disks, and an instanced attribute that holds a different translation for each disk.

Another point of interest in the program is its used of vertex buffer objects. The disks can be animated. The disks move, but their colors don't change. Since the colors don't change, the color values for the disks are loaded into a VBO once, during program initialization. The usage parameter in gl.bufferData is set to gl.STATIC_DRAW because the data will not be modified. However, because the disks are moving, the values for the translations of the disks have to change in each frame. So, new data is loaded into the corresponding VBO for each frame, with usage gl.STREAM_DRAW because the data that is being loaded will only be used once.

Finally, I should note that VAOs and instancing require WebGL 2.0, but the same functionality is available in many implementations of WebGL 1.0 as optional extensions. Webgl extensions will be discussed in Section 7.5.