WebGL model view projection - Web API 接口参考

WebGL model view projection
===========================

本文探讨如何在 WebGL 项目中获取数据，并将其投影到适当的空间以在屏幕上显示。它假定了你具备用于平移，缩放和旋转的基本矩阵数学知识。它解释了组成 3D 场景时通常使用的三个核心矩阵：模型，视图和投影矩阵。

**备注：** 本文还可作为 MDN 内容套件 提供。它还使用 `MDN`全局对象下可用的 实用函数 集合。

模型、视图、投影矩阵
----------

WebGL 空间中的点和多边形的个体转化由基本的转换矩阵（例如平移，缩放和旋转）处理。可以将这些矩阵组合在一起并以特殊方式分组，以使其用于渲染复杂的 3D 场景。这些组合成的矩阵最终将原始数据类型移动到一个称为裁剪空间的特殊坐标空间中。这是一个中心点位于 (0, 0, 0)，角落范围在 (-1, -1, -1) 到 (1, 1, 1) 之间，2 个单位宽的立方体。该剪裁空间被压缩到一个二维空间并栅格化为图像。

下面讨论的第一个矩阵是**模型矩阵**，它定义了如何获取原始模型数据并将其在 3D 世界空间中移动。**投影矩阵**用于将世界空间坐标转换为剪裁空间坐标。常用的投影矩阵（**透视矩阵**）用于模拟充当 3D 虚拟世界中观看者的替身的典型相机的效果。**视图矩阵**负责移动场景中的对象以模拟相机位置的变化，改变观察者当前能够看到的内容。

以下的几个部分提供了对模型，视图和投影矩阵背后的思想及实现的深入理解。这些矩阵是在屏幕上移动数据的核心，是胜过各个框架和引擎的概念。

裁剪空间
----

在 WebGL 程序中，数据通常上传到具有自己的坐标系统的 GPU 上，然后顶点着色器将这些点转换到一个称为**裁剪空间**的特殊坐标系上。延展到裁剪空间之外的任何数据都会被剪裁并且不会被渲染。如果一个三角形超出了该空间的边界，则将其裁切成新的三角形，并且仅保留新三角形在裁剪空间中的部分。

![A 3d graph showing clip space in WebGL.](/zh-CN/docs/Web/API/WebGL_API/WebGL_model_view_projection/clip_space_graph.svg)

上面的图像裁剪空间的可视化，所有点都必须被包含在其中。它是一个角在 (-1, -1, -1)，对角在 (1, 1, 1)，中心点在 (0, 0, 0) 的每边 2 个单位的立方体。裁剪空间使用的这个两个立方米坐标系称为归一化设备坐标（NDC）。在研究和使用 WebGL 代码时，你可能时不时的会使用这个术语。

在本节中，我们将直接将数据放入裁剪空间坐标系中。通常使用位于任意坐标系中的模型数据，然后使用矩阵进行转换，将模型坐标转换为裁剪空间系下的坐标。这个例子，通过简单地使用从 (-1，-1，-1) 到 (1,1,1) 的模型坐标值来说明剪辑空间的工作方式是最简单的。下面的代码将创建 2 个三角形，这些三角形将在屏幕上绘制一个正方形。正方形中的 Z 深度确定当前正方形共享同一个空间时在顶部绘制的内容，较小的 Z 值将呈现在较大的 Z 值之上。

### WebGLBox 例子

本示例将创建一个自定义 WebGL 对象，该对象将在屏幕上绘制一个 2D 框。

**备注：** 每一个 WebGL 示例代码在此 github repo 中可找到，并按章节组织。此外，每个章节底部都有一个 JSFiddle 链接。

#### WebGLBox Constructor

构造函数看起来像这样：

```js
function WebGLBox() {
  // 设置 canvas 和 WebGL 上下文
  this.canvas = document.getElementById("canvas");
  this.canvas.width = window.innerWidth;
  this.canvas.height = window.innerHeight;
  this.gl = MDN.createContext(canvas);

  var gl = this.gl;

  // 设置一个 WebGL 程序，任何 MDN 对象相关的部分在本文之外定义
  this.webglProgram = MDN.createWebGLProgramFromIds(
    gl,
    "vertex-shader",
    "fragment-shader",
  );
  gl.useProgram(this.webglProgram);

  // 保存 attribute 和 uniform 位置
  this.positionLocation = gl.getAttribLocation(this.webglProgram, "position");
  this.colorLocation = gl.getUniformLocation(this.webglProgram, "color");

  // 告诉 WebGL 在绘制时测试深度，所以如果一个正方形后面有另一个正方形
  // 另一个正方形不会被绘制
  gl.enable(gl.DEPTH\_TEST);
}
```

#### WebGLBox 绘制

现在，我们将创建一个在屏幕上绘制框的方法。

```js
WebGLBox.prototype.draw = function (settings) {
  // 创建一下 attribute 数据; 这些是最终绘制到屏幕上的三角形
  // 有两个形成一个正方形

  var data = new Float32Array([
    //Triangle 1
    settings.left,
    settings.bottom,
    settings.depth,
    settings.right,
    settings.bottom,
    settings.depth,
    settings.left,
    settings.top,
    settings.depth,

    //Triangle 2
    settings.left,
    settings.top,
    settings.depth,
    settings.right,
    settings.bottom,
    settings.depth,
    settings.right,
    settings.top,
    settings.depth,
  ]);

  // 使用 WebGL 将其绘制到屏幕上

  // 性能要点：为每个绘制创建新的缓冲器很慢
  // 这个方法仅用于说明

  var gl = this.gl;

  // 创建一个缓冲区并绑定数据
  var buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY\_BUFFER, buffer);
  gl.bufferData(gl.ARRAY\_BUFFER, data, gl.STATIC\_DRAW);

  // 设置指向 attribute 数据的指针（三角形）
  gl.enableVertexAttribArray(this.positionLocation);
  gl.vertexAttribPointer(this.positionLocation, 3, gl.FLOAT, false, 0, 0);

  // 设置将在所有三角形之间共享的 color uniform
  gl.uniform4fv(this.colorLocation, settings.color);

  // 在屏幕上绘制该三角形
  gl.drawArrays(gl.TRIANGLES, 0, 6);
};
```

着色器是用 GLSL 编写的代码片段，它接收我们的点数据并最终将它们渲染到屏幕上。为了方便起见，这些着色器存储在 `<script>` 元素之中，该元素通过自定义函数 `MDN.createWebGLProgramFromIds()` 引入程序中。这个方法是为这些教程编写的 实用函数 集合的一部分，此处不再赘述。此函数用于处理获取一些 GLSL 源代码并将其编译为 WebGL 程序的基础操作。该函数具有三个参数 - 用于渲染程序的上下文，包含顶点着色器的 `<script>` 元素的 ID 和包含片段着色器的 `<script>` 元素的 ID。顶点着色器放置顶点，片段着色器为每个像素着色。

首先看一下将在屏幕上移动顶点的顶点着色器：

```glsl
// 一个顶点位置
attribute vec3 position;

void main() {

  // gl\_Position 是顶点着色器对其修改后在裁剪空间的最终位置
  gl_Position = vec4(position, 1.0);
}
```

接下来，要实际将数据栅格化为像素，片段着色器将在每个像素的基础上计算评估一切，设置一个单一颜色。GPU 为需要渲染的每个像素调用着色器方法。着色器的工作是返回要用于该像素的颜色。

```glsl
precision mediump float;
uniform vec4 color;

void main() {
  gl_FragColor = color;
}
```

有了这些设置，是时候使用裁剪空间坐标直接绘制到屏幕了。

```js
var box = new WebGLBox();
```

首先在中间画一个红色框。

```js
box.draw({
  top: 0.5, // x
  bottom: -0.5, // x
  left: -0.5, // y
  right: 0.5, // y

  depth: 0, // z
  color: [1, 0.4, 0.4, 1], // red
});
```

接下来，在上面的红色框的后面绘制一个绿色框。

```js
box.draw({
  top: 0.9, // x
  bottom: 0, // x
  left: -0.9, // y
  right: 0.9, // y

  depth: 0.5, // z
  color: [0.4, 1, 0.4, 1], // green
});
```

最后，为了演示裁剪实际上发生了，这个框没有被绘制，因为它完全在裁剪空间之外，深度超出 -1.0 到 1.0 的范围。

```js
box.draw({
  top: 1, // x
  bottom: -1, // x
  left: -1, // y
  right: 1, // y

  depth: -1.5, // z
  color: [0.4, 0.4, 1, 1], // blue
});
```

#### 结果

在 JSFiddle 中查看

![The results of drawing to clip space using WebGL.](/zh-CN/docs/Web/API/WebGL_API/WebGL_model_view_projection/part1.png)

#### 练习

在这一点上一个有用的练习是通过更改代码来使框在裁剪空间中移动，感受点是如何在裁剪空间中被剪切和移动的。尝试画一张有背景的方形笑脸。

齐次坐标
----

之前的裁剪空间顶点着色器主要包含以下代码：

```js
gl_Position = vec4(position, 1.0);
```

位置变量是在 `draw()` 方法中定义的，并作为 attribute 传递给着色器。这是一个三维点，但最终通过管线传递的 `gl_Position` 变量实际上是四维的 - 是 `(x,y,z,w)` 而不是 `(x,y,z)` 。 `z` 后面没有字母了，因此习惯上将第四维标记为 `w`。在上面的示例中， `w` 坐标设置为 1.0。

显而易见的问题是：“为什么要增加维度？”。事实证明，这种增加允许使用许多不错的技术来处理 3D 数据。这个增加的维度将透视的概念引入坐标系中。将其放置在适当的位置后，我们可以将 3D 坐标映射到 2D 空间中，从而允许两条平行线当它们延伸到远方时相交。 `w` 的值被用作该坐标的其他分量放除数，因此 `x`, `y` 和 `z` 的真实值被计算为 `x/w` , `y/w` 和 `z/w`（然后 `w` 也 `w/w` , 变成 1）。

三维点定义在典型的笛卡尔坐标系中。增加的第四维将这一点变为 齐次坐标 。它仍然代表 3D 空间中的一个点，并且可以通过一对简单的函数轻松地演示如何构造这种类型的坐标。

```js
function cartesianToHomogeneous(point) {
  var x = point[0];
  var y = point[1];
  var z = point[2];

  return [x, y, z, 1];
}

function homogeneousToCartesian(point) {
  var x = point[0];
  var y = point[1];
  var z = point[2];
  var w = point[3];

  return [x / w, y / w, z / w];
}
```

正如前面提到的和上面展示的函数，w 分量将和 x, y 和 z 相除。当 w 分量为非零实数时，齐次坐标很容易转换回笛卡尔空间中。现在，如果 w 分量为零会发生什么？在 JavaScript 中，返回值如下：

```js
homogeneousToCartesian([10, 4, 5, 0]);
```

计算结果为： `[Infinity, Infinity, Infinity]`.

该齐次坐标表示无穷大的某个点。这是一种方便的方式表示从原点向特定方向发射的射线。除了射线，还可以将其视为方向矢量的表示。如果将此齐次坐标和带有平移的矩阵相乘，则该平移将被有效地消去了。

当计算机上的数字非常大（或非常小）时，它们的精确度将越来越低，因为仅用这么多的“1”和“0”来表示它们。对较大的数字执行的操作越多，结果中就会积累越来越多的错误。当除以 w 时，这可以通过两个可能更小，更不易出错的数字进行运算来有效地提高非常大的数字的精度。

使用齐次坐标的最终好处是，它们非常适合与 4x4 矩阵相乘。一个顶点必须至少与矩阵的一个维数（行/列）匹配，才能与其相乘。4x4 矩阵可用于编码各种转换。实际上，典型的透视矩阵使用 w 分量除法来实现其变换。

实际上，在将齐次坐标转换回笛卡尔坐标之后（通过除以 w），会发生从裁剪空间中裁剪点和多边形的情况。该最终空间称为**归一化设备坐标**或 NDC。

为了开始使用这个思想，可以修改前面的示例，以允许使用 `w` 分量。

```js
// 重新定义三角形以使用 W 分量
var data = new Float32Array([
  //Triangle 1
  settings.left,
  settings.bottom,
  settings.depth,
  settings.w,
  settings.right,
  settings.bottom,
  settings.depth,
  settings.w,
  settings.left,
  settings.top,
  settings.depth,
  settings.w,

  //Triangle 2
  settings.left,
  settings.top,
  settings.depth,
  settings.w,
  settings.right,
  settings.bottom,
  settings.depth,
  settings.w,
  settings.right,
  settings.top,
  settings.depth,
  settings.w,
]);
```

然后，顶点着色器使用传入的 4 维点。

```js
attribute vec4 position;

void main() {
  gl_Position = position;
}
```

首先，我们在中间绘制一个红色框，但将 W 设置为 0.7。但坐标除以 0.7 时，它们全部会被放大。

```js
box.draw({
  top: 0.5, // x
  bottom: -0.5, // x
  left: -0.5, // y
  right: 0.5, // y
  w: 0.7, // w - 放大这个盒子

  depth: 0, // z
  color: [1, 0.4, 0.4, 1], // red
});
```

现在，我们在上面绘制一个绿色框，但是通过将 w 分量设置为 1.1 来缩小它。

```js
box.draw({
  top: 0.9, // x
  bottom: 0, // x
  left: -0.9, // y
  right: 0.9, // y
  w: 1.1, // w - 缩小这个盒子

  depth: 0.5, // z
  color: [0.4, 1, 0.4, 1], // green
});
```

最后一个框未被绘制，因为它在裁剪空间之外。深度超出 -1.0 到 1.0 范围。

```js
box.draw({
  top: 1, // x
  bottom: -1, // x
  left: -1, // y
  right: 1, // y
  w: 1.5, // w - 把这个盒子带回范围内

  depth: -1.5, // z
  color: [0.4, 0.4, 1, 1], // blue
});
```

### 结果

在 JSFiddle 中查看

### The results of using homogeneous coordinates to move the boxes around in WebGL.

### 练习

* 尝试使用这些值，看看它们如何影响屏幕上渲染的内容。请注意如何通过设置其 w 分量将先前裁剪的蓝色框带回带范围内。
* 尝试创建一个在裁剪空间之外的新框，然后将其除以 w，将其返回裁剪空间。
模型转换
----

将点直接放入裁剪空间的用途有限。在现实世界的应用程序中，你拥有的源坐标不全部在裁剪空间中。因此大多数时候，你需要将模型数据和其他坐标转换到裁剪空间中。简单的立方体就是一个如何执行此操作的简单示例。立方体数据由顶点位置，立方体表面颜色和构成单个多边形的顶点位置的顺序组成（以 3 个顶点为一组，以构成立方体表面的三角形）。这些位置和颜色存储在 GL 缓冲区中，作为属性发到着色器，然后分别进行操作。

最后，计算并设置单个模型矩阵。该矩阵表示要在组成模型的每个点上执行的转换，以将其移到正确的空间，并在模型中的每个点上执行任何其他所需的转换。这不仅适用于每一个顶点，而且还适用于模型每个表面的每个点。

在这种情况下，对于动画的每一帧，一系列缩放，旋转和平移矩阵会将数据移动到裁剪空间中所需的位置。这个立方体是裁剪空间 (-1, -1, -1) 到 (1, 1, 1) 的大小，因此需要缩小以不填满整个裁剪空间。该矩阵事先已经在 JavaScript 中进行了乘法运算，直接发到着色器。

以下代码示例在 `CubeDemo` 对象上定义了一个创建模型矩阵的方法。它使用了自定义函数来创建和乘以 MDN WebGL 共享代码中定义的矩阵。新的函数如下：

```js
CubeDemo.prototype.computeModelMatrix = function (now) {
  // 缩小 50%
  var scale = MDN.scaleMatrix(0.5, 0.5, 0.5);

  // 轻微旋转
  var rotateX = MDN.rotateXMatrix(now \* 0.0003);

  // 根据时间旋转
  var rotateY = MDN.rotateYMatrix(now \* 0.0005);

  // 稍微向下移动
  var position = MDN.translateMatrix(0, -0.1, 0);

  // 相乘，确定以相反的顺序读取它们
  this.transforms.model = MDN.multiplyArrayOfMatrices([
    position, // step 4
    rotateY, // step 3
    rotateX, // step 2
    scale, // step 1
  ]);
};
```

为了在着色器中使用它，必须将其设置在 uniforms 的位置。uniforms 的位置保存在 `locations` 对象中，如下所示：

```js
this.locations.model = gl.getUniformLocation(webglProgram, "model");
```

最后，将 uniforms 设置在那个位置，这就把矩阵交给了 GPU。

```js
gl.uniformMatrix4fv(
  this.locations.model,
  false,
  new Float32Array(this.transforms.model),
);
```

在着色器中，每个位置顶点首先被转换为齐次坐标（vec4 对象），然后与模型矩阵相乘。

```glsl
gl_Position = model \* vec4(position, 1.0);
```

**备注：** 在 JavaScript 中，矩阵乘法需要自定义函数，而在着色器中，它使用了内置在语言中的简单的 \* 运算。

### 结果

在 JSFiddle 中查看

![Using a model matrix](/zh-CN/docs/Web/API/WebGL_API/WebGL_model_view_projection/part3.png)

此时，变换点的 w 值仍为 1.0。立方体仍然没有什么角度。下一节将进行此设置并修改 w 值以提供一些透视效果。

### 练习

* 使用缩放矩阵缩小立方体，并将其放置在裁剪空间中不同位置。
* 尝试将其移到裁剪空间之外。
* 调整窗口大小，然后观察盒子的变形情况。
* 添加一个 `rotateZ` 。
除以 W
----

一个开始了解立方体模型透视的简单方法是获取 Z 坐标并将其复制到 w 坐标。通常，将笛卡尔点转换为齐次坐标时，它变为 `(x,y,z,1)` ，但我们将其设置为 `(x,y,z,z)` 。实际上，我们希望确保视图中的点的 z 值大于 0，因此我们将其值改为 `((1.0 + z) * scaleFactor)` 对其进行轻微的修改。这将需要一个通常位于裁剪空间（-1 到 1）中的点，并将其移到更像（0 到 1）的空间中，具体取决于比例因子设置为什么。比例因子将最终 w 值更改为总体上更高或更低。

着色器代码如下：

```js
// 首先转换点
vec4 transformedPosition = model \* vec4(position, 1.0);

// 透视有多大的影响？
float scaleFactor = 0.5;

// 通过采用介于 -1 到 1 之间的 z 值来设置 w
// 然后进行缩放为 0 到某个数，在这种情况下为 0 到 1
float w = (1.0 + transformedPosition.z) \* scaleFactor;

// 使用自定义 w 分量保存新的 gl\_Position
gl_Position = vec4(transformedPosition.xyz, w);
```

### 结果

在 JSFiddle 中查看

![Filling the W component and creating some projection.](/zh-CN/docs/Web/API/WebGL_API/WebGL_model_view_projection/part4.png)

看到那个深蓝色的小三角形吗？那是添加到对象上的另一个面，因为形状的旋转导致了该角延伸到裁剪空间之外，从而导致该角被裁剪掉。有关如何使用更复杂的矩阵来帮助控制和防止裁剪的介绍，请参照下面的 Perspective matrix。

### 练习

如果这听起来有点抽象，请打开顶点着色器，然后使用比例因子，观察其如何将顶点向表面进一步收缩。完全更改 w 分量的值，以表示真实空间。

在下一节中，我们将执行把 Z 值复制到 w 插槽并将其转换为矩阵的步骤。

简单投影
----

填充 w 分量的最后一步实际上可以用一个简单的矩阵完成。从 identity 矩阵开始：

```js
var identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];

MDN.multiplyPoint(identity, [2, 3, 4, 1]);
//> [2, 3, 4, 1]
```

然后将最后一列的 1 向上移动一个空格。

```js
var copyZ = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0];

MDN.multiplyPoint(copyZ, [2, 3, 4, 1]);
//> [2, 3, 4, 4]
```

但是，在最后一个示例中，我们执行了 `(z + 1) * scaleFactor`:
```

var scaleFactor = 0.5;

var simpleProjection = [
  1, 0, 0, 0,
  0, 1, 0, 0,
  0, 0, 1, scaleFactor,
  0, 0, 0, scaleFactor,
];

MDN.multiplyPoint(simpleProjection, [2, 3, 4, 1]);
//> [2, 3, 4, 2.5]
```

进一步展开我们可以看到它是如何工作的：

```js
var x = 2 \* 1 + 3 \* 0 + 4 \* 0 + 1 \* 0;
var y = 2 \* 0 + 3 \* 1 + 4 \* 0 + 1 \* 0;
var z = 2 \* 0 + 3 \* 0 + 4 \* 1 + 1 \* 0;
var w = 2 \* 0 + 3 \* 0 + 4 \* scaleFactor + 1 \* scaleFactor;
```

最后一行可以简化为：

```js
w = 4 \* scaleFactor + 1 \* scaleFactor;
```

然后将 scaleFactor 提取出来，我们得到：

```js
w = (4 + 1) \* scaleFactor;
```

这与我们在前面示例中使用的 `(z + 1) * scaleFactor` 完全相同。

在 box demo 中，添加了一个额外的 `.computeSimpleProjectionMatrix()` 方法。在 `.draw()` 方法中调用，并将比例因子传递给它。结果应该与上一个示例相同：

```js
CubeDemo.prototype.computeSimpleProjectionMatrix = function (scaleFactor) {
  this.transforms.projection = [
    1,
    0,
    0,
    0,
    0,
    1,
    0,
    0,
    0,
    0,
    1,
    scaleFactor,
    0,
    0,
    0,
    scaleFactor,
  ];
};
```

尽管结果相同，但重要的步骤还是在顶点着色器中。与其直接修改顶点，不如将其乘以一个附加的 **projection matrix**，该矩阵将 3D 点投影到 2D 绘图表面上：

```glsl
// 确保以相反的顺序读取转换矩阵
gl_Position = projection \* model \* vec4(position, 1.0);
```

### 结果

在 JSFiddle 中查看

![A simple projection matrix](/zh-CN/docs/Web/API/WebGL_API/WebGL_model_view_projection/part5.png)

透视矩阵
----

至此，我们逐步构建了自己的 3D 渲染设置。但是，我们当前构建的代码存在一些问题。首先，每当我们调整窗口大小时，它就会倾斜。另外是我们的简单投影无法处理场景数据的大范围值。大多数场景在裁剪空间中不起作用。定义与场景相关的距离是很有帮助的，这样在转换数字时不会损失精度。最后，对哪些点放在裁剪空间的内部和外部进行精度控制非常有帮助。在前面的例子中，立方体的角偶尔会被裁剪。

透视矩阵是一种可以满足这些要求的投影矩阵。也开始涉及数学更多的内容，这些示例中将不做充分解释。简而言之，它结合了除以 w（与前面的例子相同）和基于 相似三角形 相似三角形的一些巧妙操作。如果你想阅读有关其背后数学的完整说明，请查看以下一些链接：

* OpenGL 投影矩阵
* 透视投影
* 尝试了解 WebGL 中透视矩阵背后的数学

关于下面使用的透视矩阵，需要注意的一件重要的事是它会翻转 z 轴。在裁剪空间中，z+ 原理观察者，而使用此矩阵，它朝向观察者。

翻转 z 轴的原因是，裁剪空间坐标系是左手坐标系（z 轴指向远离观察者并指入屏幕的位置），而数学，物理学和 3D 建模中的惯例与 OpenGL 中视图/眼睛坐标系一样，是使用右手坐标系（z 轴指向屏幕，朝向观察者）。有关的 Wikipedia 文章的更多信息：直角坐标系, 右手法则。

让我们看一下 `perspectiveMatrix()` 函数，该函数计算了透视矩阵。

```js
MDN.perspectiveMatrix = function (
  fieldOfViewInRadians,
 aspectRatio,
 near,
 far,
) {
  var f = 1.0 / Math.tan(fieldOfViewInRadians / 2);
  var rangeInv = 1 / (near - far);

  return [
    f / aspectRatio,
    0,
    0,
    0,
    0,
    f,
    0,
    0,
    0,
    0,
    (near + far) \* rangeInv,
    -1,
    0,
    0,
    near \* far \* rangeInv \* 2,
    0,
  ];
};
```

此函数的四个参数是：

`fieldOfviewInRadians`

一个以弧度表示的角度，指示观看者一层可以看多少场景。数字越大，摄像机可见的越多。边缘的几何形状变得越来越失真，等同于广角镜。当视野更大时，物体通常会变小。当视野较小时，摄像机在场景中的看到的东西会越来越少。物体因透视而变形的程度要小得多，并且物体似乎更靠近相机。

`aspectRatio`

场景的宽高比，等于其宽度除以其高度。在本示例中，就是窗口的宽度除以窗口的高度。此参数的引入最终解决了当画布调整大小和形状时模型的变形问题。

`nearClippingPlaneDistance`

一个正数，表示到屏幕的距离是垂直于地板的平面的距离，该距离比将所有内容都裁剪的距离更近。它在裁剪空间中映射为 -1，并且不应设置为 0。

`farClippingPlaneDistance`

 一个正数，表示与平面之间的距离，超出该距离将裁剪几何体。它在裁剪空间中映射为 1.该值应保持合理的距离以接近几何图形的距离，以免在渲染时出现精度误差。
 在最新版本的盒子 demo 中， `computeSimpleProjectionMatrix()` 函数已替换为 `computePerspectiveMatrix()` 函数。

```js
CubeDemo.prototype.computePerspectiveMatrix = function () {
  var fieldOfViewInRadians = Math.PI \* 0.5;
  var aspectRatio = window.innerWidth / window.innerHeight;
  var nearClippingPlaneDistance = 1;
  var farClippingPlaneDistance = 50;

  this.transforms.projection = MDN.perspectiveMatrix(
    fieldOfViewInRadians,
    aspectRatio,
    nearClippingPlaneDistance,
    farClippingPlaneDistance,
  );
};
```

着色器代码与前面的示例相同：

```js
gl_Position = projection \* model \* vec4(position, 1.0);
```

此外（未显示），更改了模型的位置和缩放矩阵，以使其脱离裁剪空间并进入更大的坐标系。

### 结果

在 JSFiddle 中查看

![A true perspective matrix](/zh-CN/docs/Web/API/WebGL_API/WebGL_model_view_projection/part6.png)

### 练习

* 使用透视矩阵和模型矩阵的参数进行体验。
* 将透视矩阵替换为 正交矩阵。在 MDN WebGL 共享代码中可以找到 `MDN.orthographicMatrix()` 替换 `CubeDemo.prototype.computePerspectiveMatrix()` 中的 `MDN.perspectiveMatrix()` 函数。
视图矩阵
----

尽管某些图形库提供的虚拟相机可以在构成场景时可以定位和指向，但 OpenGL（以及扩展的 WebGL）却没有。这是视图矩阵的用处。它的作用是平移，旋转和缩放场景中的物体，以使根据观察者的位置和方向将它们放置到正确的位置。

### 模拟相机

这利用了爱因斯坦狭义相对论的基本理论之一：参考系和相对运动的原理说，从观察者的角度来看，你可以通过将相反的变化应用于场景中的物体来模拟改变观察者的位置和方向。无论哪种方式，结果似乎对于观察者是一样的。

假设一个位于桌子上的盒子和一个放在一米外的桌子上的相机，它指向盒子，盒子的正面指向相机。然后考虑将相机从盒子中移开，直到 2 米远（通过在相机的 Z 值增加 1 米），然后将其向左滑动 10 厘米。盒子与相机的距离缩小了一定量，并向右稍微滑动，从而在相机中看起来较小，左侧的一小部分也暴露在相机前。

现在，让我们重置场景，将盒子放回它的起始点，使相机距离盒子 2 米，并正对着盒子。但这一次，相机被锁定在桌子上无法移动或旋转。这就是在 WebGL 中运作的样子。那，我们如何模拟在空间中移动的相机？

我们没有向后和向左移动相机，而是对盒子应用了逆变换：我们将盒子向后移动 1 米，然后向右移动 10 厘米。从两个物体的角度来看，结果是一样的。

最后一步是创建**视图矩阵**，该矩阵将转换场景中的对象，以便对它们进行定位以模拟相机当前位置与方向。目前的代码可以在世界空间中移动立方体并投影所有内容以获得透视图，但我们仍然无法移动相机。

想象一下使用物理摄像机拍摄电影。你可以自由地将相机放到任何你想放置的位置，并对准任何你选择的方向。为了在 3D 图形中对此进行仿真，我们使用视图矩阵来模拟物理相机的位置和旋转。

与直接转换模型顶点的模型矩阵不同，视图矩阵会移动一个抽象的相机。实际上，顶点着色器仍然移动的是模型，而“相机”保持在原位。为了使此计算正确，必须使用变换矩阵的逆。逆矩阵实质上是逆转了变换，因此，如果我们向前移动相机，则逆矩阵会导致场景中的物体向后移动。

以下的 `computeViewMatrix()` 函数通过向内和向外，向左和向右移动的视图矩阵来激活视图矩阵。

```js
CubeDemo.prototype.computeViewMatrix = function (now) {
  var moveInAndOut = 20 \* Math.sin(now \* 0.002);
  var moveLeftAndRight = 15 \* Math.sin(now \* 0.0017);

  // 各个方向移动相机
  var position = MDN.translateMatrix(moveLeftAndRight, 0, 50 + moveInAndOut);

  // 相乘，确保以相反的顺序读取它们
  var matrix = MDN.multiplyArrayOfMatrices([
    // 练习：旋转相机的视角
    position,
  ]);

  // 翻转相机的运动操作，因为我们实际上是
  // 移动场景中的几何图形，而不是相机本身
  this.transforms.view = MDN.invertMatrix(matrix);
};
```

着色器现在使用三个矩阵。

```glsl
gl_Position = projection \* view \* model \* vec4(position, 1.0);
```

此步骤后，GPU 管线将裁剪超出范围的顶点，并将模型向下发送到片段着色器以进行栅格化。

### 结果

在 JSFiddle 中查看

![The view matrix](/zh-CN/docs/Web/API/WebGL_API/WebGL_model_view_projection/part7.png)

### 相关坐标系

此时，回顾并标记我们使用的各种坐标系是很有用的。首先，在**模型空间**中定义了立方体的顶点。在场景中移动模型。这些顶点需要通过应用模型矩阵转换到**世界空间**。

模型空间 → 模型矩阵 → 世界空间

相机尚未执行任何操作，需要再次移动这些点。目前它们在世界空间中，但需要将它们移动到**视图空间**（使用视图矩阵）以表示相机的位置。

世界空间 → 视图矩阵 → 视图空间

最后，需要添加**投影**（在我们的示例中是**透视矩阵**），以便将世界坐标映射到裁剪空间。

视图空间 → 投影矩阵 → 裁剪空间

### 练习

* 在场景中移动相机。
* 向视图矩阵中添加一些旋转矩阵以四处看看。
* 最后，跟踪鼠标的位置。使用 2 个旋转矩阵可以根据用户鼠标在屏幕上的位置上下移动相机。
参见
--

* WebGL
* 3D projection


WebGL types - Web API 接口参考

WebGL types
===========

以下是WebGL提供的接口中用到的变量类型。

WebGL 1
-------

以下类型的变量属于`WebGLRenderingContext`对象。

| 类型 | Web 接口类型 | 描述 |
| --- | --- | --- |
| `GLenum` | `unsigned long` | 用于枚举。另见 constants。 |
| `GLboolean` | `boolean` | `Boolean`型。 |
| `GLbitfield` | `unsigned long` | 一个位字段（bit field），用于存储逻辑位（bit）。例如，在 `WebGLRenderingContext.clear()`的使用。 |
| `GLbyte` | `byte` | 八位（一个字节），2 的补码表示的有符号整数。 |
| `GLshort` | `short` | 十六位 2 的补码表示的有符号整数。 |
| `GLint` | `long` | 三十二位 2 的补码表示的有符号整数。 |
| `GLsizei` | `long` | 用来描述尺寸（例如：绘画缓冲 drawing buffer 的宽和高）。 |
| `GLintptr` | `long long` | 用来表示指针的特殊类型。 |
| `GLsizeiptr` | `long long` | 用来表示指针的特殊类型。 |
| `GLubyte` | `octet` | 八位（一个字节），2 的补码表示的无符号整数。 |
| `GLushort` | `unsigned short` | 十六位 2 的补码表示的无符号整数。 |
| `GLuint` | `unsigned long` | 三十二位 2 的补码表示的有符号整数。 |
| `GLfloat` | `unrestricted float` | 三十二位的 IEEE 标准的浮点数。 |
| `GLclampf` | `unrestricted float` | 限值 32 位 IEEE 浮点数。 |

WebGL 2
-------

以下类型的变量属于 `WebGL2RenderingContext`. 所有 WebGL 1 中的类型也有使用。

| 类型 | Web 接口类型 | 描述 |
| --- | --- | --- |
| `GLint64` | `long long` | 六十四位有符号整数。 |

WebGL 扩展
--------

以下类型用在 WebGL extensions中。

| 类型 | Web 接口类型 | 描述 |
| --- | --- | --- |
| `GLuint64EXT` | `long long` | 六十四位无符号整数 |

规范
--

| Specification |
| --- |
| WebGL # 5.1 |
| WebGL2 # 3.1 |
| EXT\_disjoint\_timer\_query  |

另见
--

* `WebGLRenderingContext`


使用 WebGL 扩展 - Web API 接口参考

使用 WebGL 扩展
===========

WebGL，像它的姐妹 API（OpenGL 和 OpenGL ES），支持使用扩展（extension）。可在 khronos webgl extension registry 查看完整的扩展列表。

**备注：** 不像别的 GL API，在 WebGL 中，扩展只有在明确需要的情况下才会加载。

规范扩展名、供应商前缀和首选项
---------------

扩展（extension）在未被官方正式制定为标准前（但仅当它们处于草案阶段时），某些浏览器厂商可能会支持 WebGL 扩展。这样的话，扩展的名字应该加上相应的厂商前缀（`MOZ_`、`WEBKIT_` 等），否则这个扩展只能在浏览器切换了偏好设置的前提下生效。

如果你希望使用最前沿的扩展，并希望在规范被批准后可以继续使用（当然，假设扩展不会以不兼容的方式更改），你可以查询规范扩展名称以及供应商扩展名称。例如：

```js
const ext =
  gl.getExtension("OES\_vertex\_array\_object") ||
  gl.getExtension("MOZ\_OES\_vertex\_array\_object") ||
  gl.getExtension("WEBKIT\_OES\_vertex\_array\_object");
```

请注意，不鼓励使用供应商前缀，因此大多数浏览器使用功能标志（feature flag）而不是供应商前缀后面实施实验性扩展。

功能标志有：

* Firefox：`webgl.enable-draft-extensions`
* 基于 Chromium 的浏览器（Chrome、Opera）：`chrome://flags/#enable-webgl-draft-extensions`
命名约定
----

WebGL 扩展以“ANGLE”、“OES”、“EXT”、“WEBGL”为前缀。这些前缀反映了来源和意图：

* `ANGLE_`：由 ANGLE library 的作者编写的扩展。
* `OES_` 和 `KHR_`：由各自的架构审查委员会（Khronos）批准的对 OpenGL ES（OES）或 OpenGL API 扩展的功能的镜像。
* `OVR_`：针对虚拟现实进行优化的扩展。
* `EXT_`：从其他 OpenGL ES 或 OpenGL API 扩展镜像的扩展。
* `WEBGL_`：特定于 WebGL 的扩展，旨在与多种 Web 浏览器兼容。它也应该用于源自 OpenGL ES 或 OpenGL API 的扩展，但其行为已被显著改变。
查询可用的扩展程序
---------

WebGL 上下文支持查询可用的扩展。

```js
const available_extensions = gl.getSupportedExtensions();
```

`WebGLRenderingContext.getSupportedExtensions()` 方法返回一个字符串数组，每个字符串对应一个支持的扩展。

扩展列表
----

当前的扩展有：

* `ANGLE_instanced_arrays`
* `EXT_blend_minmax` (en-US)
* `EXT_color_buffer_float` (en-US)
* `EXT_color_buffer_half_float` (en-US)
* `EXT_disjoint_timer_query` (en-US)
* `EXT_float_blend`
实验性
* `EXT_frag_depth` (en-US)
* `EXT_shader_texture_lod` (en-US)
* `EXT_sRGB` (en-US)
* `EXT_texture_compression_bptc` (en-US)
* `EXT_texture_compression_rgtc` (en-US)
* `EXT_texture_filter_anisotropic` (en-US)
* `EXT_texture_norm16` (en-US)
* `KHR_parallel_shader_compile` (en-US)
* `OES_element_index_uint` (en-US)
* `OES_fbo_render_mipmap` (en-US)
* `OES_standard_derivatives` (en-US)
* `OES_texture_float` (en-US)
* `OES_texture_float_linear` (en-US)
* `OES_texture_half_float` (en-US)
* `OES_texture_half_float_linear` (en-US)
* `OES_vertex_array_object`
* `OVR_multiview2` (en-US)
* `WEBGL_color_buffer_float` (en-US)
* `WEBGL_compressed_texture_astc` (en-US)
* `WEBGL_compressed_texture_etc` (en-US)
* `WEBGL_compressed_texture_etc1` (en-US)
* `WEBGL_compressed_texture_pvrtc` (en-US)
* `WEBGL_compressed_texture_s3tc` (en-US)
* `WEBGL_compressed_texture_s3tc_srgb` (en-US)
* `WEBGL_debug_renderer_info` (en-US)
* `WEBGL_debug_shaders` (en-US)
* `WEBGL_depth_texture` (en-US)
* `WEBGL_draw_buffers` (en-US)
* `WEBGL_lose_context`
* `WEBGL_multi_draw` (en-US)
启用扩展
----

在一个扩展可用之前，必须使用 `WebGLRenderingContext.getExtension()` 来启用它。例如：

```js
const float_texture_ext = gl.getExtension("OES\_texture\_float");
```

如果不支持扩展，则返回值为 `null`，否则为扩展对象。

扩展对象
----

如果扩展定义了 WebGL 核心规范中不可用的特定符号或函数，则它们将在调用 `gl.getExtension()` 后返回的扩展对象中可用。

参见
--

* `WebGLRenderingContext.getSupportedExtensions()`
* `WebGLRenderingContext.getExtension()`
* webglreport.com


检测 WebGL - Web API 接口参考

检测 WebGL
========

* « 上一页
* 下一页 »

这个例子演示了如何通过渲染上下文来检测WebGL，并将结果报告给用户。

### WebGL 特性检测

在第一个例子中，我们将检查浏览器是否支持WebGL。为此，我们将尝试从`canvas`元素获取WebGL 渲染的上下文 。WebGL 渲染的上下文是一个接口，通过它你可以设置和查询绘图器的状态，发送数据到 WebGL，执行绘制命令。

在单个上下文接口中保存绘图器的状态并不是WebGL独有的。这在其他绘图技术里也是存在的API,比如2D 渲染上下文的 canvas。然而，你可以调整的属性和变量对于每个API来说都是不同的。

```html
<p>[ Here would go the result of WebGL feature detection ]</p>
<button>Press here to detect WebGLRenderingContext</button>
```

```css
body {
  text-align: center;
}
button {
  display: block;
  font-size: inherit;
  margin: auto;
  padding: 0.6em;
}
```

```js
// Run everything inside window load event handler, to make sure
// DOM is fully loaded and styled before trying to manipulate it.
window.addEventListener(
  "load",
  function () {
    var paragraph = document.querySelector("p"),
      button = document.querySelector("button");
    // Adding click event handler to button.
    button.addEventListener("click", detectWebGLContext, false);
    function detectWebGLContext() {
      // Create canvas element. The canvas is not added to the
      // document itself, so it is never displayed in the
      // browser window.
      var canvas = document.createElement("canvas");
      // Get WebGLRenderingContext from canvas element.
      var gl =
        canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
      // Report the result.
      if (gl && gl instanceof WebGLRenderingContext) {
        paragraph.innerHTML = "Congratulations! Your browser supports WebGL.";
      } else {
        paragraph.innerHTML =
          "Failed to get WebGL context. " +
          "Your browser or device may not support WebGL.";
      }
    }
  },
  false,
);
```

这个例子的源代码可以在GitHub上获取。

* « 上一页
* 下一页 »


Scissor animation - Web API 接口参考

Scissor animation
=================

* « 上一页
* 下一页 » (en-US)

使用剪切和清除操作实现一些动画的简单 WebGL 的例子。

### 剪切动画

本例中，我们使用`scissor()` 和 `clear()`。我们再次建立一个动画循环使用计时器。注意，这次是方块的位置 (剪切区) 更新每一帧 (我们设置帧率大约每 17 毫秒，约 60 fps -帧每秒)

相比之下，方块的颜色 (用`clearColor`) 仅创建一个新的方块。这是一个很好的演示WebGL是一个状态机。对于每一个方块，我们设置它的颜色，然后只更新它的位置每一帧。WebGL 的清晰的颜色状态维持在设定值，直到我们再次改变它创建一个新的方块。
```

<p>
  WebGL animation by clearing the drawing buffer with solid color and applying
  scissor test.
</p>
<button id="animation-onoff">
  Press here to
  <strong>[verb goes here]</strong>
  the animation
</button>
```

```
<canvas>Your browser does not seem to support HTML5 canvas.</canvas>
```

```
body {
  text-align: center;
}
canvas {
  display: block;
  width: 280px;
  height: 210px;
  margin: auto;
  padding: 0;
  border: none;
  background-color: black;
}
button {
  display: block;
  font-size: inherit;
  margin: auto;
  padding: 0.6em;
}
```

```
;(function(){
```

```js
"use strict";
window.addEventListener("load", setupAnimation, false);
// Variables to hold the WebGL context, and the color and
// position of animated squares.
var gl,
  color = getRandomColor(),
  position;

function setupAnimation(evt) {
  window.removeEventListener(evt.type, setupAnimation, false);
  if (!(gl = getRenderingContext())) return;

  gl.enable(gl.SCISSOR\_TEST);
  gl.clearColor(color[0], color[1], color[2], 1.0);
  // Unlike the browser window, vertical position in WebGL is
  // measured from bottom to top. In here we set the initial
  // position of the square to be at the top left corner of the
  // drawing buffer.
  position = [0, gl.drawingBufferHeight];

  var button = document.querySelector("button");
  var timer;
  function startAnimation(evt) {
    button.removeEventListener(evt.type, startAnimation, false);
    button.addEventListener("click", stopAnimation, false);
    document.querySelector("strong").innerHTML = "stop";
    timer = setInterval(drawAnimation, 17);
    drawAnimation();
  }
  function stopAnimation(evt) {
    button.removeEventListener(evt.type, stopAnimation, false);
    button.addEventListener("click", startAnimation, false);
    document.querySelector("strong").innerHTML = "start";
    clearInterval(timer);
  }
  stopAnimation({ type: "click" });
}

// Variables to hold the size and velocity of the square.
var size = [60, 60],
  velocity = 3.0;
function drawAnimation() {
  gl.scissor(position[0], position[1], size[0], size[1]);
  gl.clear(gl.COLOR\_BUFFER\_BIT);
  // Every frame the vertical position of the square is
  // decreased, to create the illusion of movement.
  position[1] -= velocity;
  // When the sqaure hits the bottom of the drawing buffer,
  // we override it with new square of different color and
  // velocity.
  if (position[1] < 0) {
    // Horizontal position chosen randomly, and vertical
    // position at the top of the drawing buffer.
    position = [
      Math.random() \* (gl.drawingBufferWidth - size[0]),
      gl.drawingBufferHeight,
    ];
    // Random velocity between 1.0 and 7.0
    velocity = 1.0 + 6.0 \* Math.random();
    color = getRandomColor();
    gl.clearColor(color[0], color[1], color[2], 1.0);
  }
}

function getRandomColor() {
  return [Math.random(), Math.random(), Math.random()];
}
```

```
function getRenderingContext() {
  var canvas = document.querySelector("canvas");
  canvas.width = canvas.clientWidth;
  canvas.height = canvas.clientHeight;
  var gl =
    canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
  if (!gl) {
    var paragraph = document.querySelector("p");
    paragraph.innerHTML =
      "Failed to get WebGL context." +
      "Your browser or device may not support WebGL.";
    return null;
  }
  gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  gl.clear(gl.COLOR\_BUFFER\_BIT);
  return gl;
}
```

```
})();
```

The source code of this example is also available on GitHub.

* « 上一页
* 下一页 » (en-US)


清除画布 - Web API 接口参考

清除画布
====

* « 上一页
* 下一页 »

这个例子将展示如何用一个单色清除画布

### 使用单一颜色清除 WebGl 区域

这是一个最简单的 WebGL 代码。通过rendering context设置好状态后，直接将整个区域清除为绿色。要注意 css 已经将 canvas 画布设置为黑色了，所以当画布变为绿色时，我们就知道神奇的 WebGL 魔法起作用了！

此外，你需要注意用单色绘制图像是两个步骤：首先，通过使用`clearColor()`设置清除色为绿色。这只会改变 Webgl 内部的一个状态，但并不会绘制任何东西。接下来，我们就真的开始绘制了，使用`clear()`方法，这是一个典型的用 webgl 绘制的方法，webgl 实际上只有少数的几个绘制方法 (clear() 就是其中之一)。其他方法大多都是类似设置或改变 WebGl 状态和变量的（例如设置 clearcolor）。

这里有许多属性和方法作用于 Webgl，清除方法只是你第一个掌握的，这也就是为什么 WebGL／OpenGl 经常被叫做状态机，通过调整这些属性和方法可以修改 WebGL 内部的状态，从而进行输出（例如先设置好绿色，在清除画布的时候像素点都变成了绿色）

最后，我们知道在 WebGl 中颜色格式是由 RGBA（红，绿，蓝，透明度）组成的，因此 clearColor() 方法接受四个参数

```html
<p>A very simple WebGL program that shows some color.</p>
<!-- Text within a canvas element is displayed
 only if canvas is not supported. -->
<canvas>Your browser does not seem to support HTML5 canvas.</canvas>
```

```css
body {
  text-align: center;
}
canvas {
  display: block;
  width: 280px;
  height: 210px;
  margin: auto;
  padding: 0;
  border: none;
  background-color: black;
}
```

```js
// Run everything inside window load event handler, to make sure
// DOM is fully loaded and styled before trying to manipulate it,
// and to not mess up the global scope. We are giving the event
// handler a name (setupWebGL) so that we can refer to the
// function object within the function itself.
window.addEventListener(
  "load",
  function setupWebGL(evt) {
    "use strict";

    // Cleaning after ourselves. The event handler removes
    // itself, because it only needs to run once.
    window.removeEventListener(evt.type, setupWebGL, false);

    // References to the document elements.
    var paragraph = document.querySelector("p"),
      canvas = document.querySelector("canvas");

    // Getting the WebGL rendering context.
    var gl =
      canvas.getContext("webgl") || canvas.getContext("experimental-webgl");

    // If failed, inform user of failure. Otherwise, initialize
    // the drawing buffer (the viewport) and clear the context
    // with a solid color.
    if (!gl) {
      paragraph.innerHTML =
        "Failed to get WebGL context. " +
        "Your browser or device may not support WebGL.";
      return;
    }
    paragraph.innerHTML = "Congratulations! Your browser supports WebGL. ";
    gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
    // Set the clear color to darkish green.
    gl.clearColor(0.0, 0.5, 0.0, 1.0);
    // Clear the context with the newly set color. This is
    // the function call that actually does the drawing.
    gl.clear(gl.COLOR\_BUFFER\_BIT);
  },
  false,
);
```

这个例子的代码可以在 GitHub 上下载。

* « 上一页
* 下一页 »


Hello GLSL - Web API 接口参考

Hello GLSL
==========

* « 上一页 (en-US)
* 下一页 » (en-US)

该例子将演示一个绘制固态颜色的矩形的简单着色器程序。

**备注：** 本例子能在大多数现代桌面版浏览器上运行。但或许不能在移动端或者古老的浏览器上运行。如果 canvas 显示一片空白，你可以试着用下一个例子检查一下输出是否绘制的是同样的图形。但要记住在前往下一个例子之前，要仔细阅读本页并动手写代码。

### 用 GLSL 语言写 Hello World 程序

第一个非常简单的着色器程序。
```

<p>Hello World! Hello GLSL!</p>
```

```
<canvas>Your browser does not seem to support HTML5 canvas.</canvas>
```

```
body {
  text-align: center;
}
canvas {
  width: 280px;
  height: 210px;
  margin: auto;
  padding: 0;
  border: none;
  background-color: black;
}
button {
  display: block;
  font-size: inherit;
  margin: auto;
  padding: 0.6em;
}
```

```html
<script type="x-shader/x-vertex" id="vertex-shader">
 #version 100
 void main() {
 gl\_Position = vec4(0.0, 0.0, 0.0, 1.0);
 gl\_PointSize = 64.0;
 }
</script>
```

```html
<script type="x-shader/x-fragment" id="fragment-shader">
 #version 100
 void main() {
 gl\_FragColor = vec4(0.18, 0.54, 0.34, 1.0);
 }
</script>
```

```
;(function(){
```

```js
"use strict";
window.addEventListener("load", setupWebGL, false);
var gl, program;
function setupWebGL(evt) {
  window.removeEventListener(evt.type, setupWebGL, false);
  if (!(gl = getRenderingContext())) return;

  var source = document.querySelector("#vertex-shader").innerHTML;
  var vertexShader = gl.createShader(gl.VERTEX\_SHADER);
  gl.shaderSource(vertexShader, source);
  gl.compileShader(vertexShader);
  source = document.querySelector("#fragment-shader").innerHTML;
  var fragmentShader = gl.createShader(gl.FRAGMENT\_SHADER);
  gl.shaderSource(fragmentShader, source);
  gl.compileShader(fragmentShader);
  program = gl.createProgram();
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);
  gl.detachShader(program, vertexShader);
  gl.detachShader(program, fragmentShader);
  gl.deleteShader(vertexShader);
  gl.deleteShader(fragmentShader);
  if (!gl.getProgramParameter(program, gl.LINK\_STATUS)) {
    var linkErrLog = gl.getProgramInfoLog(program);
    cleanup();
    document.querySelector("p").innerHTML =
      "Shader program did not link successfully. " + "Error log: " + linkErrLog;
    return;
  }

  initializeAttributes();

  gl.useProgram(program);
  gl.drawArrays(gl.POINTS, 0, 1);

  cleanup();
}

var buffer;
function initializeAttributes() {
  gl.enableVertexAttribArray(0);
  buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY\_BUFFER, buffer);
  gl.vertexAttribPointer(0, 1, gl.FLOAT, false, 0, 0);
}

function cleanup() {
  gl.useProgram(null);
  if (buffer) gl.deleteBuffer(buffer);
  if (program) gl.deleteProgram(program);
}
```

```
function getRenderingContext() {
  var canvas = document.querySelector("canvas");
  canvas.width = canvas.clientWidth;
  canvas.height = canvas.clientHeight;
  var gl =
    canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
  if (!gl) {
    var paragraph = document.querySelector("p");
    paragraph.innerHTML =
      "Failed to get WebGL context." +
      "Your browser or device may not support WebGL.";
    return null;
  }
  gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  gl.clear(gl.COLOR\_BUFFER\_BIT);
  return gl;
}
```

```
})();
```

该例子的源代码能在 GitHub获得。

* « 上一页 (en-US)
* 下一页 » (en-US)


Clearing by clicking - Web API 接口参考

Clearing by clicking
====================

* « 上一页
* 下一页 »

此示例演示了如何通过用户单击时用随机颜色渲染上下文来将用户交互与 WebGL 图形操作结合起来。

### 用随机颜色渲染上下文

这个例子提供了一个简单的例子，说明如何结合 WebGL 和用户交互。每次用户点击画布或按钮时，画布都会使用一种新的随机色。

注意我们如何在事件处理函数中嵌入 WebGL 函数调用。

```html
<p>
  A very simple WebGL program that still shows some color and user interaction.
</p>
<p>
  You can repeatedly click the empty canvas or the button below to change color.
</p>
<canvas id="canvas-view"
 >Your browser does not seem to support HTML5 canvas.</canvas
>
<button id="color-switcher">Press here to switch color</button>
```

```css
body {
  text-align: center;
}
canvas {
  display: block;
  width: 280px;
  height: 210px;
  margin: auto;
  padding: 0;
  border: none;
  background-color: black;
}
button {
  display: inline-block;
  font-size: inherit;
  margin: auto;
  padding: 0.6em;
}
```

```js
window.addEventListener(
  "load",
  function setupWebGL(evt) {
    "use strict";

    // Cleaning after ourselves. The event handler removes
    // itself, because it only needs to run once.
    window.removeEventListener(evt.type, setupWebGL, false);

    // 给 canvas 和 button 添加相同的时间处理器
    var canvas = document.querySelector("#canvas-view");
    var button = document.querySelector("#color-switcher");
    canvas.addEventListener("click", switchColor, false);
    button.addEventListener("click", switchColor, false);

    // 存放 WebGLRenderingContext 的变量
    var gl;

    // 点击事件处理器
    function switchColor() {
      // Referring to the externally defined gl variable.
      // If undefined, try to obtain the WebGLRenderingContext.
      // If failed, alert user of failure.
      // Otherwise, initialize the drawing buffer (the viewport).
      if (!gl) {
        gl =
          canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
        if (!gl) {
          alert(
            "Failed to get WebGL context.\n" +
              "Your browser or device may not support WebGL.",
          );
          return;
        }
        gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
      }
      // 使用辅助函数获取一种随机色
      var color = getRandomColor();
      // 用随机色设置底色
      gl.clearColor(color[0], color[1], color[2], 1.0);
      // Clear the context with the newly set color. This is
      // the function call that actually does the drawing.
      gl.clear(gl.COLOR\_BUFFER\_BIT);
    }

    // 随机颜色辅助函数
    function getRandomColor() {
      return [Math.random(), Math.random(), Math.random()];
    }
  },
  false,
);
```

这个例子的代码可以在 GitHub 上下载。

* « 上一页
* 下一页 »


Boilerplate 1 - Web API 接口参考

Boilerplate 1
=============

* « 上一页
* 下一页 »

这个例子描述了从现在开始将要隐藏重复的代码片断，以及定义一个 JavaScript 函数复用以简化 WebGL 初始化。

用于设置 WebGL 呈现上下文的复用代码
---------------------

现在你很习惯看到相同的HTML, CSS和JavaScript重复一遍又一遍。所以我们从现在起要隐藏他们。这将使我们能够专注于代码最有趣的部分相关学习WebGL。

特别是，在 HTML 的`<p>`元素包含一些描述性的文本页面也可以是错误消息；一个`<canvas>` 元素;和一个可选的`<button>`。CSS 规则包含`body`, `canvas`, 和`button`。任何额外的冗余的 CSS 和 HTML 将不会显示在页面的具体的例子。

在以下示例中，我们将使用一个 JavaScript 函数功能，`getRenderingContext()` ，来初始化WebGL rendering context。现在，你应该能够了解什么功能。基本上，它得到了 WebGL 从画布元素，渲染上下文初始化绘图缓冲区，清除它黑色，并返回初始化上下文。在错误的情况下，它会显示一个错误消息，并返回 `null`。

最后，所有 JavaScript 代码将运行在一个直接的函数，这是一种常见的 JavaScript 技术 (see Function)。函数声明和调用也将被隐藏。

### HTML

```html
<p>[ Some descriptive text about the example. ]</p>
<button>[ Optional button element. ]</button>
<canvas>Your browser does not seem to support HTML5 canvas.</canvas>
```

### CSS

```css
body {
  text-align: center;
}
canvas {
  display: block;
  width: 280px;
  height: 210px;
  margin: auto;
  padding: 0;
  border: none;
  background-color: black;
}
button {
  display: block;
  font-size: inherit;
  margin: auto;
  padding: 0.6em;
}
```

### JavaScript

```js
function getRenderingContext() {
  var canvas = document.querySelector("canvas");
  canvas.width = canvas.clientWidth;
  canvas.height = canvas.clientHeight;
  var gl =
    canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
  if (!gl) {
    var paragraph = document.querySelector("p");
    paragraph.innerHTML =
      "Failed to get WebGL context." +
      "Your browser or device may not support WebGL.";
    return null;
  }
  gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  gl.clear(gl.COLOR\_BUFFER\_BIT);
  return gl;
}
```

The source code of this example is also available on GitHub.

* « 上一页
* 下一页 »


Canvas size and WebGL - Web API 接口参考

Canvas size and WebGL
=====================

* « 上一页 (en-US)
* 下一页 »

此 WebGL 案例将探究设置（或不设置）Canvas 属性的宽高值在浏览器中显示的影响。

canvas 属性值大小对 WebGL 渲染的作用
-------------------------

使用 `scissor()` 和 `clear()` 我们可以观察到 canvas 属性大小是如何影响 WebGL 绘图展示的。

第一个 canvas 元素通过 css 样式定义了元素的大小，之后通过 javascript 获取该元素的 `clientWidth` 和`clientHeight` 值，并分别赋值给元素的 `width` 和`height`。

相反的，第二个 canvas 元素并没有这样做，canvas 内部对象的`width` 和 `height` 属性值仍然是默认值，这样导致在浏览器中实际画布大小是不同的。

使用 `scissor()` 和`clear()`在 canvas 中绘制矩形的效果是清晰可见的，在第一个 canvas 中，通过指定位置和像素大小，可以得到我们想要的效果，但是在第二个 canvas 中，这个矩形的位置、大小都是错误展示的。

```html
<p>Compare the two canvases.</p>
<canvas>Your browser does not seem to support HTML5 canvas.</canvas>
<canvas>Your browser does not seem to support HTML5 canvas.</canvas>
```

```css
body {
  text-align: center;
}
canvas {
  display: inline-block;
  width: 120px;
  height: 80px;
  margin: auto;
  padding: 0;
  border: none;
  background-color: black;
}
```

```js
window.addEventListener(
  "load",
  function () {
    "use strict";
    var firstCanvas = document.getElementsByTagName("canvas")[0],
      secondCanvas = document.getElementsByTagName("canvas")[1];
    firstCanvas.width = firstCanvas.clientWidth;
    firstCanvas.height = firstCanvas.clientHeight;
    [firstCanvas, secondCanvas].forEach(function (canvas) {
      var gl =
        canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
      if (!gl) {
        document.querySelector("p").innerHTML =
          "Failed to get WebGL context. " +
          "Your browser or device may not support WebGL.";
        return;
      }
      gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
      gl.enable(gl.SCISSOR\_TEST);
      gl.scissor(30, 10, 60, 60);
      gl.clearColor(1.0, 1.0, 0.0, 1.0);
      gl.clear(gl.COLOR\_BUFFER\_BIT);
    });
  },
  false,
);
```

The source code of this example is also available on GitHub.

* « 上一页 (en-US)
* 下一页 »


Simple color animation - Web API 接口参考

Simple color animation
======================

* « 上一页
* 下一页 » (en-US)

这是一个非常基础的WebGL色彩动画案例，通过定时器来逐秒填充不同的颜色来实现。

### 通过填充实现色彩动画

本案例使用WebGL来实现简单的色彩动画和用户交互效果，用户可以通过按按钮来开始/暂停/重新开始动画。

我们把 WebGL函数放在一个定时循环器里 (`setInterval`)。通过监听点击事件来让用户开始/暂停动画。并通过定时器来循环执行绘制指令 (通常是逐帧动画，这次我们设置为逐秒动画)

```html
<p>一个色彩动画的简单 WebGl 程序</p>
<p>点击下面的按钮来开/关动画</p>
<canvas id="canvas-view">你的浏览器不支持 Html5 canvas</canvas>
<button id="animation-onoff">
  点我来
  <strong>[verb goes here]</strong>
  动画
</button>
```

```css
body {
  text-align: center;
}
canvas {
  display: block;
  width: 280px;
  height: 210px;
  margin: auto;
  padding: 0;
  border: none;
  background-color: black;
}
button {
  display: inline-block;
  font-size: inherit;
  margin: auto;
  padding: 0.6em;
}
```

```js
window.addEventListener(
  "load",
  function setupAnimation(evt) {
    "use strict";
    window.removeEventListener(evt.type, setupAnimation, false);

    // 定义一个变量来保存定时器，以播放动画
    var timer;

    // 点击事件处理器
    var button = document.querySelector("#animation-onoff");
    var verb = document.querySelector("strong");
    function startAnimation(evt) {
      button.removeEventListener(evt.type, startAnimation, false);
      button.addEventListener("click", stopAnimation, false);
      verb.innerHTML = "暂停";
      //设置一个循环来逐秒渲染动画
      timer = setInterval(drawAnimation, 1000);
      // 在用户点击完以后立即执行一次动画
      drawAnimation();
    }
    function stopAnimation(evt) {
      button.removeEventListener(evt.type, stopAnimation, false);
      button.addEventListener("click", startAnimation, false);
      verb.innerHTML = "开始";
      // 通过清除定时器来停止动画
      clearInterval(timer);
    }
    // 调用 stopAnimation() 来初始化按钮的事件处理器
    stopAnimation({ type: "click" });

    var gl;
    function drawAnimation() {
      if (!gl) {
        var canvas = document.getElementById("canvas-view");
        gl =
          canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
        if (!gl) {
          clearInterval(timer);
          alert(
            "Failed to get WebGL context.\n" +
              "Your browser or device may not support WebGL.",
          );
          return;
        }
        gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
      }

      // 使用辅助函数 得到随机颜色
      var color = getRandomColor();
      // 将随机颜色设置到 WebGL 渲染上下文的填充颜色上去
      gl.clearColor(color[0], color[1], color[2], 1.0);
      // 使用新设置的颜色来清除上下文
      gl.clear(gl.COLOR\_BUFFER\_BIT);
    }

    //获取随机颜色的辅助函数
    function getRandomColor() {
      return [Math.random(), Math.random(), Math.random()];
    }
  },
  false,
);
```

在 GitHub.上查看源码

* « 上一页
* 下一页 » (en-US)


使用着色器将颜色应用于 WebGL - Web API 接口参考

使用着色器将颜色应用于 WebGL
=================

* « 上一页
* 下一页 »

在之前的展示中我们已经创建好了一个正方形，接下来我们要做的就是给它添加一抹色彩。添加颜色可以通过修改着色器来实现。

给顶点着色
-----

在 WebGL 中，物体是由一系列顶点组成的，每一个顶点都有位置和颜色信息。在默认情况下，所有像素的颜色（以及它所有的属性，包括位置）都由线性插值计算得来，自动形成平滑的渐变。我们以前的顶点着色器没有给顶点添加任何特定的颜色——在顶点着色器与片段着色器之间给每个像素着白色，于是整个正方形被渲染成纯白。

现在我们假设正方形的每个顶点使用不同的颜色：红，黄，绿，白，以此渲染一个渐变的色彩。第一步，要给这些顶点建立相应的颜色。首先我们要创建一个顶点颜色数组，然后将它们存在 WebGL 的缓冲区中。为实现这一功能，我们在 initBuffers() 函数中加入如下代码：

**备注：** 在 `init-buffers.js` 文件中添加如下函数：

```js
function initColorBuffer(gl) {
  const colors = [
    1.0,
    1.0,
    1.0,
    1.0, // 白
    1.0,
    0.0,
    0.0,
    1.0, // 红
    0.0,
    1.0,
    0.0,
    1.0, // 绿
    0.0,
    0.0,
    1.0,
    1.0, // 蓝
  ];

  const colorBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY\_BUFFER, colorBuffer);
  gl.bufferData(gl.ARRAY\_BUFFER, new Float32Array(colors), gl.STATIC\_DRAW);

  return colorBuffer;
}
```

这段代码首先建立了一个 JavaScript 的数组，此数组中包含四组四值向量，每一组向量代表一个顶点的颜色。然后，创建一个 WebGL 缓冲区用来存储这些颜色——将数组中的值转换成 WebGL 所规定的浮点型后，存储在该缓冲区中。

当然，我们也需要从 `initBuffers()` 中调用这个新函数，并返回它创建的新缓冲区。

**备注：** 在 `initBuffers()` 函数中用下面代码替换旧的 `return` 语句：

```js
const colorBuffer = initColorBuffer(gl);

return {
  position: positionBuffer,
  color: colorBuffer,
};
```

为了实际使用这些颜色，我们继续修改顶点着色器，使得着色器可以从颜色缓冲区中正确取出颜色：

**备注：** 在 `main()` 函数中更新 `vsSource` 的定义，如下：

```js
// Vertex shader program

const vsSource = `
 attribute vec4 aVertexPosition;
 attribute vec4 aVertexColor;

 uniform mat4 uModelViewMatrix;
 uniform mat4 uProjectionMatrix;

 varying lowp vec4 vColor;

 void main(void) {
 gl\_Position = uProjectionMatrix \* uModelViewMatrix \* aVertexPosition;
 vColor = aVertexColor;
 }
 `;
```

与之前相比，这段代码的关键不同点在于：每个顶点都与一个颜色数组中的数值相连接。

给片段着色
-----

为使每个像素都得到插值后的颜色，我们只需要在此从 `vColor` 变量中获取这个颜色的值：

**备注：** 在 `main()` 函数中更新 `fsSource` 的定义，如下：

```js
// Fragment shader program

const fsSource = `
 varying lowp vec4 vColor;

 void main(void) {
 gl\_FragColor = vColor;
 }
 `;
```

这是一个非常简单的改变，每个片段只是根据其相对于顶点的位置得到一个插值过的颜色，而不是一个指定的颜色值。

带颜色的绘制
------

接下来，我们要初始化颜色属性，以便着色器程序使用

**备注：** 在 `main()`函数中更新 `programInfo` 的定义，如下：

```js
// Collect all the info needed to use the shader program.
// Look up which attributes our shader program is using
// for aVertexPosition, aVertexColor and also
// look up uniform locations.
const programInfo = {
  program: shaderProgram,
  attribLocations: {
    vertexPosition: gl.getAttribLocation(shaderProgram, "aVertexPosition"),
    vertexColor: gl.getAttribLocation(shaderProgram, "aVertexColor"),
  },
  uniformLocations: {
    projectionMatrix: gl.getUniformLocation(shaderProgram, "uProjectionMatrix"),
    modelViewMatrix: gl.getUniformLocation(shaderProgram, "uModelViewMatrix"),
  },
};
```

然后，我们便可以修改 `drawScene()` 使之在绘制正方形时使用这些颜色：

**备注：** 在 `draw-scene.js` 文件中添加下面函数：

```js
// Tell WebGL how to pull out the colors from the color buffer
// into the vertexColor attribute.
function setColorAttribute(gl, buffers, programInfo) {
  const numComponents = 4;
  const type = gl.FLOAT;
  const normalize = false;
  const stride = 0;
  const offset = 0;
  gl.bindBuffer(gl.ARRAY\_BUFFER, buffers.color);
  gl.vertexAttribPointer(
    programInfo.attribLocations.vertexColor,
    numComponents,
    type,
    normalize,
    stride,
    offset,
  );
  gl.enableVertexAttribArray(programInfo.attribLocations.vertexColor);
}
```

**备注：** `drawScene()` 函数中在调用 `gl.useProgram()` 之前，先调用 `setColorAttribute()`：

```js
setColorAttribute(gl, buffers, programInfo);
```

查看完整代码 | 在新页面中打开示例

* « 上一页
* 下一页 »


在 WebGL 中使用纹理 - Web API 接口参考

在 WebGL 中使用纹理
=============

* « 上一页
* 下一页 »

现在我们已经创建好了一个可以旋转的 3D 的立方体，接下来是时候使用贴图来代替每个面的单一的颜色了。

加载纹理
----

首先加入加载纹理的代码。现在我们只使用一张单一的纹理贴到立方体的 6 个面上，但是同样的方法可以用来加载任意数量的纹理贴图。

**备注：** 值得注意的一点是对纹理的加载同样需要遵循跨域访问规则；也就是说你只能从允许跨域访问的网址加载你需要的纹理。见下方跨域纹理小节以了解详情。

**备注：** 在你的“webgl-demo.js”脚本中添加下面的两个函数：

```js
//
// Initialize a texture and load an image.
// When the image finished loading copy it into the texture.
//
function loadTexture(gl, url) {
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE\_2D, texture);

  // Because images have to be downloaded over the internet
  // they might take a moment until they are ready.
  // Until then put a single pixel in the texture so we can
  // use it immediately. When the image has finished downloading
  // we'll update the texture with the contents of the image.
  const level = 0;
  const internalFormat = gl.RGBA;
  const width = 1;
  const height = 1;
  const border = 0;
  const srcFormat = gl.RGBA;
  const srcType = gl.UNSIGNED\_BYTE;
  const pixel = new Uint8Array([0, 0, 255, 255]); // opaque blue
  gl.texImage2D(
    gl.TEXTURE\_2D,
    level,
    internalFormat,
    width,
    height,
    border,
    srcFormat,
    srcType,
    pixel,
  );

  const image = new Image();
  image.onload = () => {
    gl.bindTexture(gl.TEXTURE\_2D, texture);
    gl.texImage2D(
      gl.TEXTURE\_2D,
      level,
      internalFormat,
      srcFormat,
      srcType,
      image,
    );

    // WebGL1 has different requirements for power of 2 images
    // vs. non power of 2 images so check if the image is a
    // power of 2 in both dimensions.
    if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
      // Yes, it's a power of 2. Generate mips.
      gl.generateMipmap(gl.TEXTURE\_2D);
    } else {
      // No, it's not a power of 2. Turn off mips and set
      // wrapping to clamp to edge
      gl.texParameteri(gl.TEXTURE\_2D, gl.TEXTURE\_WRAP\_S, gl.CLAMP\_TO\_EDGE);
      gl.texParameteri(gl.TEXTURE\_2D, gl.TEXTURE\_WRAP\_T, gl.CLAMP\_TO\_EDGE);
      gl.texParameteri(gl.TEXTURE\_2D, gl.TEXTURE\_MIN\_FILTER, gl.LINEAR);
    }
  };
  image.src = url;

  return texture;
}

function isPowerOf2(value) {
  return (value & (value - 1)) === 0;
}
```

函数 `loadTexture()` 首先调用 WebGL 的 `createTexture()` 函数来创建一个 WebGL 纹理对象 texture。接下来使用 `texImage2D()` 以上传一个蓝色的像素点。这样我们就可以在图片下载完成之前使用这个蓝色的纹理了。

要从图片文件加载纹理，接下来创建一个 `Image` 对象，并为 `src` 设置我们想要用作纹理的图片的 URL。我们为 `image.onload` 设置的函数会在图片下载完成时被调用。那时我们再次调用 `texImage2D()`，这次我们将图片作为纹理的数据源。之后，我们根据下载的图像在两个维度上是否为 2 的幂来设置纹理的过滤（filter）和平铺（wrap）。

WebGL1 中，对于非 2 的幂纹理只能使用 `NEAREST` 和 `LINEAR` 过滤，且不会生成贴图。此外，平铺模式也必须设置为 `CLAMP_TO_EDGE`。另一方面，如果纹理在两个维度上都是 2 的幂，那么 WebGL 就可以使用更高质量的过滤，可以使用贴图，还能够将平铺模式设置为 `REPEAT` 或 `MIRRORED_REPEAT`。

使用重复纹理寻址的一个例子就是使用一张砖块的纹理来平铺满一面墙壁。

多级渐进纹理和纹理坐标重复可以通过调用 `texParameteri()` 来禁用，当然首先你已经通过调用 `bindTexture()` 绑定过纹理了。这样虽然已经可以使用非 2 的幂纹理了，但是你将无法使用多级渐进纹理，纹理坐标包装，纹理坐标重复，而且无法控制设备如何处理你的纹理。

```js
// gl.NEAREST is also allowed, instead of gl.LINEAR, as neither mipmap.
gl.texParameteri(gl.TEXTURE\_2D, gl.TEXTURE\_MIN\_FILTER, gl.LINEAR);
// Prevents s-coordinate wrapping (repeating).
gl.texParameteri(gl.TEXTURE\_2D, gl.TEXTURE\_WRAP\_S, gl.CLAMP\_TO\_EDGE);
// Prevents t-coordinate wrapping (repeating).
gl.texParameteri(gl.TEXTURE\_2D, gl.TEXTURE\_WRAP\_T, gl.CLAMP\_TO\_EDGE);
```

现在，当使用以上参数时，兼容 WebGL 的设备就会自动变得可以使用任何分辨率的纹理（当然还要考虑像素上限）。如果不使用上面这些参数的话，任何非 2 的幂纹理使用都会失败然后返回一张纯黑图片。

为了加载图像，在我们的 `main()` 函数中添加对 `loadTexture()` 函数的调用。这可以在 `initBuffers(gl)` 调用之后添加。

但需要注意的是：浏览器会从加载的图像中按从左上角开始的自上而下顺序复制像素，而 WebGL 需要按自下而上的顺序——从左下角开始的像素顺序。（参见为什么我的 WebGL 纹理是颠倒的？以了解详情。）

所以为了防止渲染时图像纹理方向错误，我们还需要调用 `pixelStorei()` 并将 `gl.UNPACK_FLIP_Y_WEBGL` 参数设置为 `true`，以调整像素顺序，使其翻转成 WebGL 需要的自下而上顺序。

**备注：** 添加下面的代码到 `main()` 函数，紧跟在 `initBuffers()` 调用之后：

```js
// Load texture
const texture = loadTexture(gl, "cubetexture.png");
// Flip image pixels into the bottom-to-top order that WebGL expects.
gl.pixelStorei(gl.UNPACK\_FLIP\_Y\_WEBGL, true);
```

**备注：** 最后，将 cubetexture.png 下载到与 JavaScript 文件相同的本地目录。

映射纹理到面
------

现在，纹理已加载并准备就绪。但在我们使用它之前，我们需要建立纹理坐标到立方体上的面的顶点的映射。这将取代 `initBuffers()` 中为设置每个立方体面颜色而存在的所有先前的代码。

**备注：** 添加这个函数到“init-buffer.js”模块：

```js
function initTextureBuffer(gl) {
  const textureCoordBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY\_BUFFER, textureCoordBuffer);

  const textureCoordinates = [
    // Front
    0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
    // Back
    0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
    // Top
    0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
    // Bottom
    0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
    // Right
    0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
    // Left
    0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0,
  ];

  gl.bufferData(
    gl.ARRAY\_BUFFER,
    new Float32Array(textureCoordinates),
    gl.STATIC\_DRAW,
  );

  return textureCoordBuffer;
}
```

首先，这段代码创建了一个 WebGL 缓冲区，我们将在其中存储每个面的纹理坐标，然后将该缓冲区绑定为将要写入的数组。

`textureCoordinates` 数组定义好了与每个面上的每个顶点一一对应的纹理坐标。请注意，纹理坐标的取值范围只能从 0.0 到 1.0，所以不论纹理贴图的实际大小是多少，为了实现纹理映射，我们要使用的纹理坐标只能规范化到 0.0 到 1.0 的范围内。

一旦我们建立了纹理映射数组，我们将数组传递到缓冲区中，这样 WebGL 就可以使用该数据了。

然后我们返回新的缓冲区。

接下来，我们需要更新 `initBuffers()` 来创建并返回纹理坐标缓冲区代替颜色缓冲区。

**备注：** 在“init-buffers.js”模块的 `initBuffers()` 函数中，将 `initColorBuffer()` 的调用替换为下面的这行：

```js
const textureCoordBuffer = initTextureBuffer(gl);
```

**备注：** 在“init-buffers.js”模块的 `initBuffers()` 函数中，将 `return` 语句替换为以下内容：

```js
return {
  position: positionBuffer,
  textureCoord: textureCoordBuffer,
  indices: indexBuffer,
};
```

更新着色器
-----

为了使用纹理来代替单一的颜色，着色器程序和着色器程序的初始化代码都需要进行修改。

### 顶点着色器

我们需要更改顶点着色器，使其不再获取颜色数据，而是获取纹理坐标数据。

**备注：** 在你的 `main()` 函数中更新 `vsSource` 定义，像这样：

```js
const vsSource = `
 attribute vec4 aVertexPosition;
 attribute vec2 aTextureCoord;

 uniform mat4 uModelViewMatrix;
 uniform mat4 uProjectionMatrix;

 varying highp vec2 vTextureCoord;

 void main(void) {
 gl\_Position = uProjectionMatrix \* uModelViewMatrix \* aVertexPosition;
 vTextureCoord = aTextureCoord;
 }
 `;
```

代码的关键更改在于不再获取顶点颜色数据转而获取和设置纹理坐标数据；这样就能把顶点与其对应的纹理联系在一起了。

### 片段着色器

那么片段着色器也要相应地进行更改，像这样更新 `main()` 函数中的 `fsSource` 声明：

```js
const fsSource = `
 varying highp vec2 vTextureCoord;

 uniform sampler2D uSampler;

 void main(void) {
 gl\_FragColor = texture2D(uSampler, vTextureCoord);
 }
 `;
```

现在的代码不会再使用一个简单的颜色值填充片段颜色，片段的颜色是通过采样器使用最好的映射方式从纹理中的每一个像素计算出来的。

### Attribute 与 Uniform 位置

因为我们修改了 attribute 并添加了 uniform，所以我们需要查找它们的位置。

**备注：** 在你的 `main()` 函数中，像这样更新 `programInfo` 的定义：

```js
const programInfo = {
  program: shaderProgram,
  attribLocations: {
    vertexPosition: gl.getAttribLocation(shaderProgram, "aVertexPosition"),
    textureCoord: gl.getAttribLocation(shaderProgram, "aTextureCoord"),
  },
  uniformLocations: {
    projectionMatrix: gl.getUniformLocation(shaderProgram, "uProjectionMatrix"),
    modelViewMatrix: gl.getUniformLocation(shaderProgram, "uModelViewMatrix"),
    uSampler: gl.getUniformLocation(shaderProgram, "uSampler"),
  },
};
```

绘制具体纹理贴图的立方体
------------

对 `drawScene()` 函数的更改很简单。

**备注：** 在“draw-scene.js”模块的 `drawScene()` 函数中添加以下函数：

```js
// 告诉 WebGL 如何从缓冲区中提取纹理坐标
function setTextureAttribute(gl, buffers, programInfo) {
  const num = 2; // 每个坐标由 2 个值组成
  const type = gl.FLOAT; // 缓冲区中的数据为 32 位浮点数
  const normalize = false; // 不做标准化处理
  const stride = 0; // 从一个坐标到下一个坐标要获取多少字节
  const offset = 0; // 从缓冲区内的第几个字节开始获取数据
  gl.bindBuffer(gl.ARRAY\_BUFFER, buffers.textureCoord);
  gl.vertexAttribPointer(
    programInfo.attribLocations.textureCoord,
    num,
    type,
    normalize,
    stride,
    offset,
  );
  gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord);
}
```

**备注：** 在你的“draw-scene.js”模块的 `drawScene()` 函数中，用下面一行替换 `setColorAttribute()` 的调用：

```js
setTextureAttribute(gl, buffers, programInfo);
```

然后添加代码来指定要映射到面的纹理。

**备注：** 在你的 `drawScene()` 函数中，就在对 `gl.uniformMatrix4fv()` 的两次调用之后，添加以下代码：

```js
// Tell WebGL we want to affect texture unit 0
gl.activeTexture(gl.TEXTURE0);

// Bind the texture to texture unit 0
gl.bindTexture(gl.TEXTURE\_2D, texture);

// Tell the shader we bound the texture to texture unit 0
gl.uniform1i(programInfo.uniformLocations.uSampler, 0);
```

WebGL 提供了至少 8 个纹理单元，`gl.TEXTURE0` 是第一个。若我们想要改变第一个单元，我们需要调用 `bindTexture()` 以将纹理绑定到纹理单元 0 的 `TEXTURE_2D` 绑定点。然后告诉着色器所用纹理单元 0 作为 `uSampler`。

最后，在 `drawScene()` 函数中添加 `texture` 作为参数，包括它被定义和被调用的地方。

**备注：** 更新你的 `drawScene()` 函数的定义以添加新的参数：

```js
function drawScene(gl, programInfo, buffers, texture, cubeRotation) {
```

**备注：** 更新你的 `main()` 函数中调用 `drawScene()` 的地方：

```js
drawScene(gl, programInfo, buffers, texture, cubeRotation);
```

好，现在我们的立方体就会像这样旋转起来了。

查看完整示例代码 | 在新窗口里打开示例

跨域纹理
----

加载 WebGL 纹理应该也可以说是跨域访问控制里的一个话题。为了在我们的显示内容里使用其他域名里的纹理图片，允许跨域访问也是要考虑的。可以通过查看HTTP 访问控制来获取到更多的相关细节。

这篇文章也对跨域加载纹理到 WebGL 做出了解释。而且文章里面还包含了一个使用的例子。

被污染过的（只写）画布是不能拿来当作 WebGL 纹理来使用的。举个例子来说，当把一张跨域的图片画到一个 2D 的 `<canvas>` 中时，这个画布就是“被污染过的”。

* « 上一页
* 下一页 »


用 WebGL 让目标动起来 - Web API 接口参考

用 WebGL 让目标动起来
==============

* « 上一页
* 下一页 »

在此示例中，我们实际上将旋转正方形平面。

使正方形旋转
------

让我们开始旋转正方形。我们需要的第一件事是创建一个变量，用于跟踪正方形的当前旋转：

```js
var squareRotation = 0.0;
```

现在我们需要更新`drawScene()`函数以在绘制正方形时将当前旋转应用于正方形。转换为正方形的初始绘图位置后，我们像这样应用旋转：

```js
mat4.rotate(
  modelViewMatrix, // destination matrix
  modelViewMatrix, // matrix to rotate
  squareRotation, // amount to rotate in radians
  [0, 0, 1],
); // axis to rotate around
```

这会将 modelViewMatrix 的当前值`squareRotation`绕 Z 轴旋转。

要进行动画制作，我们需要添加`squareRotation`随时间更改值的代码。为此，我们可以创建一个新变量来跟踪上次动画播放的时间（我们称之为`then`），然后将以下代码添加到主函数的末尾

```js
var then = 0;

// Draw the scene repeatedly
function render(now) {
  now \*= 0.001; // convert to seconds
  const deltaTime = now - then;
  then = now;

  drawScene(gl, programInfo, buffers, deltaTime);

  requestAnimationFrame(render);
}
requestAnimationFrame(render);
```

该代码用于 `requestAnimationFrame` 要求浏览器在每一帧上调用函数“`render`”。`requestAnimationFrame` 自页面加载以来经过的时间（以毫秒为单位）。我们将其转换为秒，然后从中减去，以计算 `deltaTime` 自渲染最后一帧以来的秒数。在 drawscene 的结尾，我们添加了要更新的代码 `squareRotation`。

```js
squareRotation += deltaTime;
```

该代码使用自上次我们更新值以来所经过的时间`squareRotation`来确定旋转正方形的距离。

查看完整代码 | 在新页面中打开示例

* « 上一页
* 下一页 »


初识 WebGL - Web API 接口参考

初识 WebGL
========

* 下一页 »

WebGL 使得在支持 HTML 的 `canvas` 标签的浏览器中，不需要安装任何插件，便可以使用基于 OpenGL ES 2.0 的 API 在 canvas 中进行 2D 和 3D 渲染。WebGL 程序包括用 JavaScript 写的控制代码，以及在图形处理单元（GPU, Graphics Processing Unit）中执行的着色代码（GLSL，注：GLSL 为 OpenGL 着色语言）。WebGL 元素可以和其他 HTML 元素混合使用，并且可以和网页其他部分或者网页背景结合起来。

本文将向你介绍 WebGL 的基本用法。此处假定你对三维图形方面的数学知识已经有一定的理解，本文也不会试图向你教授 3D 图像概念本身。

本文的代码也可以在这里下载 GitHub 上的 webgl-examples 文件夹。

THREE.js和BABYLON.js等很多框架封装了 WebGL，提供了各个平台之间的兼容性。使用这些框架而非原生的 WebGL 可以更容易地开发 3D 应用和游戏。

准备 3D 渲染
--------

为了使用 WebGL 进行 3D 渲染，你首先需要一个 canvas 元素。下面的 HTML 片段用来建立一个 canvas 元素并设置一个 onload 事件处理程序来初始化我们的 WebGL 上下文。

```html
<body onload="main()">
  <canvas id="glcanvas" width="640" height="480">
    你的浏览器似乎不支持或者禁用了 HTML5 <code><canvas></code> 元素。
  </canvas>
</body>
```

### 准备 WebGL 上下文

我们的 JavaScript 代码中的 `main()` 函数将会在文档加载完成之后被调用。它的任务是设置 WebGL 上下文并开始渲染内容。

```js
// 从这里开始
function main() {
  const canvas = document.querySelector("#glcanvas");
  // 初始化 WebGL 上下文
  const gl = canvas.getContext("webgl");

  // 确认 WebGL 支持性
  if (!gl) {
    alert("无法初始化 WebGL，你的浏览器、操作系统或硬件等可能不支持 WebGL。");
    return;
  }

  // 使用完全不透明的黑色清除所有图像
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  // 用上面指定的颜色清除缓冲区
  gl.clear(gl.COLOR\_BUFFER\_BIT);
}
```

我们所要做的第一件事就是是获取 canvas 的引用，把它保存在‘canvas’变量里。

当我们获取到 canvas 之后，我们会调用getContext 函数并向它传递 `"webgl"` 参数，来尝试获取WebGLRenderingContext。如果浏览器不支持 webgl, `getContext` 将会返回 `null`，我们就可以显示一条消息给用户然后退出。

如果 WebGL 上下文成功初始化，变量‘gl’会用来引用该上下文。在这个例子里，我们用黑色清除上下文内已有的元素。（用背景颜色重绘 canvas）。

查看完整的源码 | 在新标签页中查看演示

参见
--

* WebGL 介绍: 由 Luz Caballero 所著，发布在 dev.opera.com。这篇文章说明 WebGL 是什么，解释了 WebGL 是如何工作的 (介绍了渲染管线的概念)，并且介绍了一些 WebGL 库。
* WebGL 基础
* 现代 OpenGL 介绍： 由 Joe Groff 写的一系列关于 OpenGL 的不错的文章，提供了一个清晰的介绍，从 OpenGL 的历史到图形管线概念，也包括一些说明 OpenGL 如何工作的例子，如果你对 OpenGL 没有任何概念的话，这是不错的出发点。
* 下一页 »


使用 WebGL 创建 2D 内容 - Web API 接口参考

使用 WebGL 创建 2D 内容
=================

* « 上一页
* 下一页 »

一旦创建 WebGL 上下文创建成功，你就可以在这个上下文里渲染画图了。而对我们而言最简单的事，莫过于绘制一个没有纹理的 2D 图形了。那就让我们从画出一个正方形开始吧。

本项目的完整源代码可在 GitHub 上获得。

引入 glMatrix 库
-------------

该项目使用了 glMatrix 库来执行其矩阵操作，因此需要引入它。本次示例通过 CDN 形式引入使用。

**备注：** 更新“index.html”文件，让它看起来像这样：

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>WebGL Demo</title>
    <link rel="stylesheet" href="./webgl.css" type="text/css" />
    <script
 src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"
 integrity="sha512-zhHQR0/H5SEBL3Wn6yYSaTTZej12z0hVZKOv3TwCUXT1z5qeqGcXJLLrbERYRScEDDpYIJhPC1fk31gqR783iQ=="
 crossorigin="anonymous"
 defer></script>
    <script src="webgl-demo.js" type="module" defer></script>
  </head>

  <body>
    <canvas id="glcanvas" width="640" height="480"></canvas>
  </body>
</html>
```

渲染场景
----

在开始前，我们首先需要明确最重要的一点，虽然我们的例子只是画一个二维物体，但我们仍然是在把它画在一个三维空间里。所以，我们依然需要创建着色器，通过它来渲染我们的简单场景并画出我们的物体。往下，我们将展示正方形是怎样一步步被画出来的。

### 着色器

**着色器是**使用 OpenGL ES 着色语言(**GLSL**) 编写的程序，它携带着绘制形状的顶点信息以及构造绘制在屏幕上像素的所需数据，换句话说，它负责记录着像素点的位置和颜色。

绘制 WebGL 时候有两种不同的着色器函数，**顶点着色器和片段着色器。**你需要通过用 GLSL 编写这些着色器，并将代码文本传递给 WebGL，使之在 GPU 执行时编译。顺便一提，顶点着色器和片段着色器的集合我们通常称之为**着色器程序。**

下面我们通过在 WebGL 环境绘制一个 2D 图像的例子快速介绍这两种着色器。

#### 顶点着色器

每次渲染一个形状时，顶点着色器会在形状中的每个顶点运行。它的工作是将输入顶点从原始坐标系转换到 WebGL 使用的**裁剪空间**坐标系，其中每个轴的坐标范围从 -1.0 到 1.0，并且不考虑纵横比，实际尺寸或任何其他因素。

顶点着色器需要对顶点坐标进行必要的转换，在每个顶点基础上进行其他调整或计算，然后通过将其保存在由 GLSL 提供的特殊变量（我们称为 gl\_Position）中来返回变换后的顶点

顶点着色器根据需要，也可以完成其他工作。例如，决定哪个包含 texel (en-US) 面部纹理的坐标，可以应用于顶点；通过法线来确定应用到顶点的光照因子等。然后将这些信息存储在变量（varyings)或属性 (attributes)属性中，以便与片段着色器共享

以下的顶点着色器接收一个我们定义的属性（aVertexPosition）的顶点位置值。之后这个值与两个 4x4 的矩阵（uProjectionMatrix 和 uModelMatrix）相乘; 乘积赋值给 gl\_Position。有关投影和其他矩阵的更多信息，在这里你可能可以找到有帮助的文章。

**备注：** 添加下面代码到 `main()` 函数中：

```js
// Vertex shader program

const vsSource = `
 attribute vec4 aVertexPosition;

 uniform mat4 uModelViewMatrix;
 uniform mat4 uProjectionMatrix;

 void main() {
 gl\_Position = uProjectionMatrix \* uModelViewMatrix \* aVertexPosition;
 }
 `;
```

这个例子中，我们没有计算任何光照效果，因为我们还没有应用到场景，它将后面的 WebGL 光照章节介绍。同样我们也还没应用任何纹理，这将在WebGL 添加纹理章节补充。

#### 片段着色器

**片段着色器**在顶点着色器处理完图形的顶点后，会被要绘制的每个图形的每个像素点调用一次。它的职责是确定像素的颜色，通过指定应用到像素的纹理元素（也就是图形纹理中的像素），获取纹理元素的颜色，然后将适当的光照应用于颜色。之后颜色存储在特殊变量 gl\_FragColor 中，返回到 WebGL 层。该颜色将最终绘制到屏幕上图形对应像素的对应位置。

在这种情况下，我们每次都会返回白色，因为我们只是在画一个白色的正方形，没有使用光照。

**备注：** 添加下面代码到 `main()` 函数中：

```js
const fsSource = `
 void main() {
 gl\_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
 }
 `;
```

### 初始化着色器

现在我们已经定义了两个着色器，我们需要将它们传递给 WebGL，编译并将它们连接在一起。下面的代码通过调用 `loadShader()`，为着色器传递类型和来源，创建了两个着色器。然后创建一个附加着色器的程序，将它们连接在一起。如果编译或链接失败，代码将弹出 alert。

**备注：** 将下面两个函数添加到“webgl-demo.js”文件中：

```js
//
// 初始化着色器程序，让 WebGL 知道如何绘制我们的数据
function initShaderProgram(gl, vsSource, fsSource) {
  const vertexShader = loadShader(gl, gl.VERTEX\_SHADER, vsSource);
  const fragmentShader = loadShader(gl, gl.FRAGMENT\_SHADER, fsSource);

  // 创建着色器程序

  const shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);

  // 如果创建失败，alert
  if (!gl.getProgramParameter(shaderProgram, gl.LINK\_STATUS)) {
    alert(
      "Unable to initialize the shader program: " +
        gl.getProgramInfoLog(shaderProgram),
    );
    return null;
  }

  return shaderProgram;
}

//
// 创建指定类型的着色器，上传 source 源码并编译
//
function loadShader(gl, type, source) {
  const shader = gl.createShader(type);

  // Send the source to the shader object

  gl.shaderSource(shader, source);

  // Compile the shader program

  gl.compileShader(shader);

  // See if it compiled successfully

  if (!gl.getShaderParameter(shader, gl.COMPILE\_STATUS)) {
    alert(
      "An error occurred compiling the shaders: " + gl.getShaderInfoLog(shader),
    );
    gl.deleteShader(shader);
    return null;
  }

  return shader;
}
```

loadShader 函数将 WebGL 上下文，着色器类型和`源码`作为参数输入，然后按如下步骤创建和编译着色器：

1. 调用`gl.createShader()`.创建一个新的着色器。
2. 调用`gl.shaderSource()`.将源代码发送到着色器。
3. 一旦着色器获取到源代码，就使用`gl.compileShader()`.进行编译。
4. 为了检查是否成功编译了着色器，将检查着色器参数 gl.COMPILE\_STATUS 状态。通过调用`gl.getShaderParameter()`获得它的值，并指定着色器和我们想要检查的参数的名字（gl.COMPILE\_STATUS）。如果返回错误，则着色器无法编译，因此通过`gl.getShaderInfoLog()` (en-US)从编译器中获取日志信息并 alert，然后删除着色器返回 null，表明加载着色器失败。
5. 如果着色器被加载并成功编译，则返回编译的着色器。

我们可以像这样调用这段代码

**备注：** 添加下面代码到 `main()` 函数中：

```js
const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
```

在创建着色器程序之后，我们需要查找 WebGL 返回分配的输入位置。在上述情况下，我们有一个属性和两个 Uniform 。属性从缓冲区接收值。顶点着色器的每次迭代都从分配给该属性的缓冲区接收下一个值。uniform 类似于 JavaScript 全局变量。它们在着色器的所有迭代中保持相同的值。由于属性和统一的位置是特定于单个着色器程序的，因此我们将它们存储在一起以使它们易于传递

**备注：** 添加下面代码到 `main()` 函数中：

```js
const programInfo = {
  program: shaderProgram,
  attribLocations: {
    vertexPosition: gl.getAttribLocation(shaderProgram, "aVertexPosition"),
  },
  uniformLocations: {
    projectionMatrix: gl.getUniformLocation(shaderProgram, "uProjectionMatrix"),
    modelViewMatrix: gl.getUniformLocation(shaderProgram, "uModelViewMatrix"),
  },
};
```

创建对象
----

在画正方形前，我们需要创建一个缓冲器来存储它的顶点。我们会用到名字为 initBuffers() 的函数。当我们了解到更多 WebGL 的高级概念时，这段代码会将有更多参数，变得更加复杂，并且用来创建更多的三维物体。

**备注：** 创建一个新文件叫“init-buffers.js”，并填写如下内容：

```js
function initBuffers(gl) {
  const positionBuffer = initPositionBuffer(gl);

  return {
    position: positionBuffer,
  };
}

function initPositionBuffer(gl) {
  // Create a buffer for the square's positions.
  const positionBuffer = gl.createBuffer();

  // Select the positionBuffer as the one to apply buffer
  // operations to from here out.
  gl.bindBuffer(gl.ARRAY\_BUFFER, positionBuffer);

  // Now create an array of positions for the square.
  const positions = [1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0];

  // Now pass the list of positions into WebGL to build the
  // shape. We do this by creating a Float32Array from the
  // JavaScript array, then use it to fill the current buffer.
  gl.bufferData(gl.ARRAY\_BUFFER, new Float32Array(positions), gl.STATIC\_DRAW);

  return positionBuffer;
}

export { initBuffers };
```

这段代码简单给出了绘画场景的本质。首先，它调用 gl 的成员函数 `createBuffer()` 得到了缓冲对象并存储在顶点缓冲器。然后调用 `bindBuffer()` 函数绑定上下文。

当上一步完成，我们创建一个 Javascript 数组去记录每一个正方体的每一个顶点。然后将其转化为 WebGL 浮点型类型的数组，并将其传到 gl 对象的 `bufferData()` 方法来建立对象的顶点。

绘制场景
----

当着色器和物体都创建好后，我们可以开始渲染这个场景了。因为我们这个例子不会产生动画，所以 drawScene() 方法非常简单。它还使用了几个工具函数，稍后我们会介绍。

**备注：** 创建一个新文件叫“draw-scene.js”，并填写如下内容：

```js
function drawScene(gl, programInfo, buffers) {
  gl.clearColor(0.0, 0.0, 0.0, 1.0); // Clear to black, fully opaque
  gl.clearDepth(1.0); // Clear everything
  gl.enable(gl.DEPTH\_TEST); // Enable depth testing
  gl.depthFunc(gl.LEQUAL); // Near things obscure far things

  // Clear the canvas before we start drawing on it.

  gl.clear(gl.COLOR\_BUFFER\_BIT | gl.DEPTH\_BUFFER\_BIT);

  // Create a perspective matrix, a special matrix that is
  // used to simulate the distortion of perspective in a camera.
  // Our field of view is 45 degrees, with a width/height
  // ratio that matches the display size of the canvas
  // and we only want to see objects between 0.1 units
  // and 100 units away from the camera.

  const fieldOfView = (45 \* Math.PI) / 180; // in radians
  const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  const zNear = 0.1;
  const zFar = 100.0;
  const projectionMatrix = mat4.create();

  // note: glmatrix.js always has the first argument
  // as the destination to receive the result.
  mat4.perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar);

  // Set the drawing position to the "identity" point, which is
  // the center of the scene.
  const modelViewMatrix = mat4.create();

  // Now move the drawing position a bit to where we want to
  // start drawing the square.

  mat4.translate(
    modelViewMatrix, // destination matrix
    modelViewMatrix, // matrix to translate
    [-0.0, 0.0, -6.0],
  ); // amount to translate

  // Tell WebGL how to pull out the positions from the position
  // buffer into the vertexPosition attribute.
  {
    const numComponents = 2; // pull out 2 values per iteration
    const type = gl.FLOAT; // the data in the buffer is 32bit floats
    const normalize = false; // don't normalize
    const stride = 0; // how many bytes to get from one set of values to the next
    // 0 = use type and numComponents above
    const offset = 0; // how many bytes inside the buffer to start from
    gl.bindBuffer(gl.ARRAY\_BUFFER, buffers.position);
    gl.vertexAttribPointer(
      programInfo.attribLocations.vertexPosition,
      numComponents,
      type,
      normalize,
      stride,
      offset,
    );
    gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);
  }

  // Tell WebGL to use our program when drawing

  gl.useProgram(programInfo.program);

  // Set the shader uniforms

  gl.uniformMatrix4fv(
    programInfo.uniformLocations.projectionMatrix,
    false,
    projectionMatrix,
  );
  gl.uniformMatrix4fv(
    programInfo.uniformLocations.modelViewMatrix,
    false,
    modelViewMatrix,
  );

  {
    const offset = 0;
    const vertexCount = 4;
    gl.drawArrays(gl.TRIANGLE\_STRIP, offset, vertexCount);
  }
}

// Tell WebGL how to pull out the positions from the position
// buffer into the vertexPosition attribute.
function setPositionAttribute(gl, buffers, programInfo) {
  const numComponents = 2; // pull out 2 values per iteration
  const type = gl.FLOAT; // the data in the buffer is 32bit floats
  const normalize = false; // don't normalize
  const stride = 0; // how many bytes to get from one set of values to the next
  // 0 = use type and numComponents above
  const offset = 0; // how many bytes inside the buffer to start from
  gl.bindBuffer(gl.ARRAY\_BUFFER, buffers.position);
  gl.vertexAttribPointer(
    programInfo.attribLocations.vertexPosition,
    numComponents,
    type,
    normalize,
    stride,
    offset,
  );
  gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);
}

export { drawScene };
```

第一步，用背景色擦除画布，接着建立摄像机透视矩阵。设置 45 度的视图角度，并且设置一个适合实际图像的宽高比。指定在摄像机距离 0.1 到 100 单位长度的范围内的物体可见。

接着加载特定位置，并把正方形放在距离摄像机 6 个单位的的位置。然后，我们绑定正方形的顶点缓冲到上下文，并配置好，再通过调用 `drawArrays()` 方法来画出对象。

最后，让我们引入 `initBuffers()` 和 `drawScene()`。

**备注：** 在“webgl-demo.js”文件头部添加如下代码：

```js
import { initBuffers } from "./init-buffers.js";
import { drawScene } from "./draw-scene.js";
```

**备注：** 在 `main()` 函数结尾处添加如下代码：

```js
// Here's where we call the routine that builds all the
// objects we'll be drawing.
const buffers = initBuffers(gl);

// Draw the scene
drawScene(gl, programInfo, buffers);
```

如果你的浏览器支持 WebGL 的话，可以点击这里看看 DEMO。完整的源代码从这里获得

矩阵通用计算
------

矩阵计算是一个很复杂的运算。没人会想去自己写完所有代码来处理这些运算。通常人们使用一个矩阵运算库，而不会自己实现矩阵运算。在这个例子中我们使用的是glMatrix library.

参见
--

* Matrices WebGLFundamentals
* Matrices 线上数学百科全书
* 维基百科上的矩阵
* « 上一页
* 下一页 »


Lighting in WebGL - Web API 接口参考

Lighting in WebGL
=================

* « 上一页
* 下一页 »

在使用灯光之前，首先我们需要了解，与定义更广泛的 OpenGL 不同，WebGL 并没有继承 OpenGL 中灯光的支持。所以你只能由自己完全得控制灯光。幸运得是，这也并不是很难，本文接下来就会介绍完成灯光的基础。

在 3D 空间中模拟现实灯光
--------------

在 3D 空间中模拟现实世界的灯光的具体原理和细节绝非本篇文章能够描述清楚的，但是对灯光模型有一定的了解对我们的学习还是很有帮助的。虽然这里没办法深入讲解，但是维基百科中的Phong 着色法给出了一个不错的概要介绍，其中包含了最常用的几种光照模型。

光源类型可以概括成如下三种：

**环境光** 是一种可以渗透到场景的每一个角落的光。它是非方向光并且会均匀地照射物体的每一个面，无论这个面是朝向哪个方向的。

**方向光** 是一束从一个固定的方向照射过来的光。这种光的特点可以理解为好像是从一个很遥远的地方照射过来的，然后光线中的每一个光子与其他光子都是平行运动的。举个例子来说，阳光就可以认为是方向光。

**点光源光** 是指光线是从一个点发射出来的，是向着四面八方发射的。这种光在我们的现实生活中是最常被用到的。举个例子来说，电灯泡就是向各个方向发射光线的。

以我们的需要来看，我们会简化光照模型，只考虑简单的方向光和环境光，不会考虑任何镜面反射和点光源。这样的话，我们只需要在我们使用的环境光上加上照射到旋转立方体的方向光就可以了。在这里可以看到之前的旋转立方体的例子。

虽然可以抛开了点光源和镜面反射，但是关于方向光还是有两点需要注意一下：

1. 需要在每个顶点信息中加入面的**朝向法线**。这个法线是一个垂直于这个顶点所在平面的向量。
2. 需要明确方向光的传播方向，可以使用一个**方向向量**来定义。

接着，我们会更新顶点着色器，考虑到环境光，再考虑到方向光（方向光的作用会因为光线方向与面的夹角关系而不同），计算每一个顶点的颜色。实现这一目标的代码如下。

建立顶点法线
------

首先我们需要做的是建立一个数组来存放立方体所有顶点的法线。由于立方体是一个很简单的物体，所以很容易实现；显然如果是对复杂物体，则法线的计算方法需要更深入的研究。(注：译者调试后发现此处 new WebGLFloatArray(...) 可能应该使用 new Float32Array())

```js
cubeVerticesNormalBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY\_BUFFER, cubeVerticesNormalBuffer);

var vertexNormals = [
  // Front
  0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0,

  // Back
  0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0,

  // Top
  0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0,

  // Bottom
  0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0,

  // Right
  1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0,

  // Left
  -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0,
];

gl.bufferData(
  gl.ARRAY\_BUFFER,
  new WebGLFloatArray(vertexNormals),
  gl.STATIC\_DRAW,
);
```

现在我们应该对此非常熟悉了；创建新的 buffer，将它和 gl.ARRAR\_BUFFER 绑定在一起，然后通过调用 bufferData() 把我们的顶点法线数组一起传入。

然后我们在 drawScene() 中添加代码，将法线数组和着色器的 attribute 绑定起来以便着色器能够获取到法线数组的信息。

（此处变量 vertexNormalAttribute 应该在 initShader() 函数中声明，并赋值：vertexNormalAttribute = gl.getAttribLocation(shaderProgram, "aVertexNormal"）; gl.enableVertexAttribArray(vertexNormalAttribute);)

```js
gl.bindBuffer(gl.ARRAY\_BUFFER, cubeVerticesNormalBuffer);
gl.vertexAttribPointer(vertexNormalAttribute, 3, gl.FLOAT, false, 0, 0);
```

最后，我们（为了读者便于理解，此处代码应该在 setMatrixUniforms() 函数中添加）需要更新下代码，在着色器中建立和传递法线向量矩阵，用这个矩阵来处理当前立方体相对于光源位置法线向量的转换 (注：译者调试后发现此处 new WebGLFloatArray(...) 应该使用 new Float32Array())：

```js
var normalMatrix = mvMatrix.inverse();
normalMatrix = normalMatrix.transpose();
var nUniform = gl.getUniformLocation(shaderProgram, "uNormalMatrix");
gl.uniformMatrix4fv(
  nUniform,
  false,
  new WebGLFloatArray(normalMatrix.flatten()),
);
```

更新着色器
-----

现在着色器需要的所有数据已经全部可以获取到了（或者说全部准备好了），我们需要更新下着色器本身的代码。

### 顶点着色器

首先更新顶点着色器，让它给每一个基于环境光和方向光的顶点一个着色器值。让我们看下代码：

```html
<script id="shader-vs" type="x-shader/x-vertex">
 attribute highp vec3 aVertexNormal;
 attribute highp vec3 aVertexPosition;
 attribute highp vec2 aTextureCoord;

 uniform highp mat4 uNormalMatrix;
 uniform highp mat4 uMVMatrix;
 uniform highp mat4 uPMatrix;

 varying highp vec2 vTextureCoord;
 varying highp vec3 vLighting;

 void main(void) {
 gl\_Position = uPMatrix \* uMVMatrix \* vec4(aVertexPosition, 1.0);
 vTextureCoord = aTextureCoord;

 // Apply lighting effect

 highp vec3 ambientLight = vec3(0.6, 0.6, 0.6);
 highp vec3 directionalLightColor = vec3(0.5, 0.5, 0.75);
 highp vec3 directionalVector = vec3(0.85, 0.8, 0.75);

 highp vec4 transformedNormal = uNormalMatrix \* vec4(aVertexNormal, 1.0);

 highp float directional = max(dot(transformedNormal.xyz, directionalVector), 0.0);
 vLighting = ambientLight + (directionalLightColor \* directional);
 }
</script>
```

一旦顶点位置计算完毕，我们就可以获得纹理对应于顶点的坐标，从而计算出顶点的阴影。

我们先根据立方体位置和朝向，通过顶点法线乘以法线矩阵来转换法线。接着我们可以通过计算转换过后的法线与方向向量（即，光来自的方向）的点积来计算得出顶点反射方向光的量。如果计算出的这个值小于 0，则我们把值固定设为 0，因为你不会有小于 0 的光。

当方向光的量计算完，我们可以通过获取环境光并且添加方向光的颜色和要提供的定向光的量来生成光照值（lighting value）。最终结果我们会得到一个 RGB 值，用于片段着色器调整我们渲染的每一个像素的颜色。

### 片段着色器

片段着色器现在需要根据顶点着色器计算出的光照值来更新：

```js
<script id="shader-fs" type="x-shader/x-fragment">
  varying highp vec2 vTextureCoord;
  varying highp vec3 vLighting;

  uniform sampler2D uSampler;

  void main(void) {
    mediump vec4 texelColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));

    gl_FragColor = vec4(texelColor.rgb \* vLighting, texelColor.a);
  }
</script>
```

和先前我们做的例子一样，我们获取纹理的颜色（原文“color of the texel”? 此处个人觉得应该就是指纹理的颜色），不同的是在设置片段颜色之前，我们将纹理的颜色乘以光照值来调整纹理以达到我们光源的效果。

效果就是这样！

查看完整的源码 | 在新标签页中查看演示

读者练习
----

显然这是一个很简单的例子，实现了基本的每个顶点的照明。对于更高级的图形，你将可能需要实现每个像素（或者说更多像素）的照明，但这会帮助你朝着正确的方向前行。

你也可以尝试光源方向颜色等等。

* « 上一页
* 下一页 »


动画纹理 - Web API 接口参考

动画纹理
====

* « 上一页

在本演示中，我们以上一个示例为基础，将静态纹理替换为正在播放的 mp4 视频文件的帧。实际上，这很容易做到，而且观看起来很有趣，所以让我们开始吧。你可以使用类似的代码来使用任何类型的数据（例如 `<canvas>`）作为纹理的源。

获取视频
----

第一步是创建将用于检索视频帧的 `<video>` 元素：

**备注：** 请将以下声明添加到“webgl-demo.js”脚本的开头：

```js
// 当视频可以复制到纹理中时将被设置为 true
let copyVideo = false;
```

**备注：** 请将以下函数添加到“webgl-demo.js”脚本中：

```js
function setupVideo(url) {
  const video = document.createElement("video");

  let playing = false;
  let timeupdate = false;

  video.playsInline = true;
  video.muted = true;
  video.loop = true;

  // 等待以下两个事件
  // 确保 video 中已有数据

  video.addEventListener(
    "playing",
    () => {
      playing = true;
      checkReady();
    },
    true,
  );

  video.addEventListener(
    "timeupdate",
    () => {
      timeupdate = true;
      checkReady();
    },
    true,
  );

  video.src = url;
  video.play();

  function checkReady() {
    if (playing && timeupdate) {
      copyVideo = true;
    }
  }

  return video;
}
```

首先，我们创建一个视频元素。我们将其设置为自动播放、静音和循环播放视频。然后，我们设置了两个事件以确保视频正在播放并且时间轴已更新。我们需要进行这两项检查，因为如果将尚无可用数据的视频上传到 WebGL，它将产生错误。检查这两个事件可确保有可用数据，并且可以安全地开始将视频上传到 WebGL 纹理。在上面的代码中，我们确认是否同时发生了这两个事件。如果是这样，我们将全局变量设置 `copyVideo` 为 true，以表示可以安全地开始将视频复制到纹理。

最后，我们将 `src` 属性设置为 start 并调用 `play` 以开始加载和播放视频。

用视频帧作为纹理
--------

接下来的更改是初始化纹理，这很简单，因为它不再需要加载图像文件。相反，它所做的只是创建一个空的纹理对象，在其中放置一个像素，然后设置其过滤条件以供后续使用：

**备注：** 请将“webgl-demo.js”中的 `loadTexture()` 函数替换为以下代码：

```js
function initTexture(gl) {
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE\_2D, texture);

  // 因为视频必须通过互联网下载
  // 可能需要一些时间才能准备好
  // 因此在纹理中放置一个像素，以便我们
  // 可以立即使用它。
  const level = 0;
  const internalFormat = gl.RGBA;
  const width = 1;
  const height = 1;
  const border = 0;
  const srcFormat = gl.RGBA;
  const srcType = gl.UNSIGNED\_BYTE;
  const pixel = new Uint8Array([0, 0, 255, 255]); // 不透明的蓝色
  gl.texImage2D(
    gl.TEXTURE\_2D,
    level,
    internalFormat,
    width,
    height,
    border,
    srcFormat,
    srcType,
    pixel,
  );

  // 关闭 mips 并将包裹（wrapping）设置为边缘分割（clamp to edge）
  // 这样无论视频的尺寸如何，都可以正常工作。
  gl.texParameteri(gl.TEXTURE\_2D, gl.TEXTURE\_WRAP\_S, gl.CLAMP\_TO\_EDGE);
  gl.texParameteri(gl.TEXTURE\_2D, gl.TEXTURE\_WRAP\_T, gl.CLAMP\_TO\_EDGE);
  gl.texParameteri(gl.TEXTURE\_2D, gl.TEXTURE\_MIN\_FILTER, gl.LINEAR);

  return texture;
}
```

**备注：** 请将以下函数添加到“webgl-demo.js”中：

```js
function updateTexture(gl, texture, video) {
  const level = 0;
  const internalFormat = gl.RGBA;
  const srcFormat = gl.RGBA;
  const srcType = gl.UNSIGNED\_BYTE;
  gl.bindTexture(gl.TEXTURE\_2D, texture);
  gl.texImage2D(
    gl.TEXTURE\_2D,
    level,
    internalFormat,
    srcFormat,
    srcType,
    video,
  );
}
```

你之前已经看过此代码。它与上一个示例中的 image onload 函数几乎相同——除了我们调用 `texImage2D()` 时传入的是 `<video>` 元素，而不是 `Image` 对象。WebGL 知道如何获取当前帧并将其用作纹理。

然后，我们需要在 `main()` 函数中调用几个新的函数。

**备注：** 在你的 `main()` 函数中，将调用 `loadTexture()` 的代码替换为以下内容：

```js
const texture = initTexture(gl);
const video = setupVideo("Firefox.mp4");
```

**备注：** 你还需要将 Firefox.mp4 文件下载到你本地与你的 JavaScript 文件相同的目录中。

**备注：** 在你的 `main()` 函数中，将 `render()` 函数替换为以下内容：

```js
// 重复绘制场景
function render(now) {
  now \*= 0.001; // 转换为秒
  deltaTime = now - then;
  then = now;

  if (copyVideo) {
    updateTexture(gl, texture, video);
  }

  drawScene(gl, programInfo, buffers, texture, cubeRotation);
  cubeRotation += deltaTime;

  requestAnimationFrame(render);
}
```

如果 `copyVideo` 为真，我们将会在调用 `drawScene()` 之前调用 `updateTexture()`。

这就是实现动画纹理的全部内容！

查看完整的代码 | 在新页面中打开这个演示

参见
--

* 在 Firefox 中使用音视频
* « 上一页


使用 WebGL 创建 3D 对象 - Web API 接口参考

使用 WebGL 创建 3D 对象
=================

* « 上一页
* 下一页 »

现在让我们给之前的正方形添加五个面从而可以创建一个三维的立方体。最简单的方式就是通过调用方法 `gl.drawElements()` 使用顶点数组列表来替换之前的通过方法`gl.drawArrays()` 直接使用顶点数组。而顶点数组列表里保存着将会被引用到一个个独立的顶点。

其实现在会存在这样一个问题：每个面需要 4 个顶点，而每个顶点会被 3 个面共享。我们会创建一个包含 24 个顶点的数组列表，通过使用数组下标来索引顶点，然后把这些用于索引的下标传递给渲染程序而不是直接把整个顶点数据传递过去，这样来减少数据传递。那么也许你就会问：那么使用 8 个顶点就好了，为什么要使用 24 个顶点呢？这是因为每个顶点虽然被 3 个面共享但是它在每个面上需要使用不同的颜色信息。24 个顶点中的每一个都会有独立的颜色信息，这就会造成每个顶点位置都会有 3 份副本。

定义立方体顶点位置
---------

首先，更新 `initBuffers()` 函数中代码来创建立方体的顶点位置缓存区。现在的代码看起来和渲染正方形时的代码很相似，只是比之前的代码更长因为现在有了 24 个顶点（每个面使用 4 个顶点）：

**备注：** 在“init-buffers.js”文件 `initPositionBuffer()` 函数中，用下面代码替换 `positions`：

```js
const positions = [
  // Front face
  -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0,

  // Back face
  -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0,

  // Top face
  -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0,

  // Bottom face
  -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0,

  // Right face
  1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0,

  // Left face
  -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0,
];
```

由于我们给顶点添加了 Z 分量，因此我们需要将 `vertexPosition` 属性的 `numComponents` 更新为 3。

**备注：** 在“draw-scene.js”文件 `setPositionAttribute()` 函数中，将 `numComponents` 从 `2` 改为 `3`:

```js
const numComponents = 3;
```

定义顶点颜色
------

然后我们还要为每个顶点定义颜色。下面的代码首先为每个面定义颜色，然后用一个循环语句为每个顶点定义颜色信息。

**备注：** 在“init-buffers.js”文件 `initColorBuffer()` 函数中，用下面代码替换 `colors` 定义：

```js
const faceColors = [
  [1.0, 1.0, 1.0, 1.0], // Front face: white
  [1.0, 0.0, 0.0, 1.0], // Back face: red
  [0.0, 1.0, 0.0, 1.0], // Top face: green
  [0.0, 0.0, 1.0, 1.0], // Bottom face: blue
  [1.0, 1.0, 0.0, 1.0], // Right face: yellow
  [1.0, 0.0, 1.0, 1.0], // Left face: purple
];

// Convert the array of colors into a table for all the vertices.

var colors = [];

for (var j = 0; j < faceColors.length; ++j) {
  const c = faceColors[j];
  // Repeat each color four times for the four vertices of the face
  colors = colors.concat(c, c, c, c);
}
```

定义元素（三角形）数组
-----------

既然已经创建好了顶点数组，接下来就要创建元素（三角形）数组了。

**备注：** 在“init-buffer.js”文件中添加下面的函数：

```js
function initIndexBuffer(gl) {
  const indexBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ELEMENT\_ARRAY\_BUFFER, indexBuffer);

  // This array defines each face as two triangles, using the
  // indices into the vertex array to specify each triangle's
  // position.

  const indices = [
    0,
    1,
    2,
    0,
    2,
    3, // front
    4,
    5,
    6,
    4,
    6,
    7, // back
    8,
    9,
    10,
    8,
    10,
    11, // top
    12,
    13,
    14,
    12,
    14,
    15, // bottom
    16,
    17,
    18,
    16,
    18,
    19, // right
    20,
    21,
    22,
    20,
    22,
    23, // left
  ];

  // Now send the element array to GL

  gl.bufferData(
    gl.ELEMENT\_ARRAY\_BUFFER,
    new Uint16Array(indices),
    gl.STATIC\_DRAW,
  );
  return indexBuffer;
}
```

`indices` 数组声明每一个面都使用两个三角形来渲染。通过立方体顶点数组的索引指定每个三角形的顶点。那么这个立方体就是由 12 个三角形组成的了。

**备注：** 在“init-buffers.js”文件 `initBuffers()`函数中，添加下面的代码替换之前的 `return` 代码片段：

```js
const indexBuffer = initIndexBuffer(gl);

return {
  position: positionBuffer,
  color: colorBuffer,
  indices: indexBuffer,
};
```

渲染立方体
-----

接下来就需要在 `drawScene()` 函数里添加代码使用立方体顶点索引数据来渲染这个立方体了。代码里添加了对 `gl.bindBuffer()` 和 `gl.drawElements()`的调用：

**备注：** 在 `drawScene()` 函数中，`gl.useProgram` 代码前添加如下代码：

```js
// Tell WebGL which indices to use to index the vertices
gl.bindBuffer(gl.ELEMENT\_ARRAY\_BUFFER, buffers.indices);
```

**备注：** 在“draw-scene.js”文件 `drawScene()` 函数中，用下面这段代码替换之前 `gl.drawArrays()`：

```js
{
  const vertexCount = 36;
  const type = gl.UNSIGNED\_SHORT;
  const offset = 0;
  gl.drawElements(gl.TRIANGLES, vertexCount, type, offset);
}
```

立方体的每个面都由 2 个三角形组成，那就是每个面需要 6 个顶点，或者说总共 36 个顶点，尽管有许多重复的。然而，因为索引数组的每个元素都是简单的整数类型，所以每一帧动画需要传递给渲染程序的数据也不是很多。

最后，让我们把变量 `squareRotation` 替换成 `cubeRotation` 并添加 X 轴的第二个旋转。

**备注：** 在“webgl-demo.js”文件的头部，把变量 `squareRotation` 替换成 `cubeRotation`：

```js
let cubeRotation = 0.0;
```

**备注：** 在 `drawScene()` 函数声明中，将变量 `squareRotation` 替换成 `cubeRotation`：

```js
function drawScene(gl, programInfo, buffers, cubeRotation) {
```

**备注：** 在 `drawScene()` 函数中，用下面代码替换之前的 `mat4.rotate` 函数：

```js
mat4.rotate(
  modelViewMatrix, // destination matrix
  modelViewMatrix, // matrix to rotate
  cubeRotation, // amount to rotate in radians
  [0, 0, 1],
); // axis to rotate around (Z)
mat4.rotate(
  modelViewMatrix, // destination matrix
  modelViewMatrix, // matrix to rotate
  cubeRotation \* 0.7, // amount to rotate in radians
  [0, 1, 0],
); // axis to rotate around (Y)
mat4.rotate(
  modelViewMatrix, // destination matrix
  modelViewMatrix, // matrix to rotate
  cubeRotation \* 0.3, // amount to rotate in radians
  [1, 0, 0],
); // axis to rotate around (X)
```

**备注：** 在 `main()` 函数中，替换 `drawScene()` 函数调用参数中的 `squareRotation` 为 `cubeRotation`：

```js
drawScene(gl, programInfo, buffers, cubeRotation);
cubeRotation += deltaTime;
```

到现在为止，我们已经创建了一个颜色生动的并且会在场景中移动和旋转的立方体，这一定很酷吧。

查看全部源代码 | 在新页面打开示例

* « 上一页
* 下一页 »


CompressionStream() - Web API 接口参考

CompressionStream()
===================

**`CompressionStream()`** 构造函数创建一个新的 `CompressionStream` 对象，该对象用于压缩数据流。

语法
--

```js
new CompressionStream(format)
```

### 参数

`format`

以下允许的压缩格式之一：

* `"gzip"`
* `"deflate"`
* `"deflate-raw"`

异常
--

`TypeError`

如果传递给构造函数的格式是不支持的则抛出。

示例
--

在此示例中，使用 gzip 压缩流。

```js
const compressedReadableStream = inputReadableStream.pipeThrough(
  new CompressionStream("gzip"),
);
```

规范
--

| Specification |
| --- |
| Compression Streams # dom-compressionstream-compressionstream |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


CompressionStream.readable - Web API 接口参考

CompressionStream.readable
==========================

`CompressionStream` 接口的 **`readable`** 只读属性返回 `ReadableStream`。

值
-

一个 `ReadableStream`。

示例
--

以下示例从 `CompressionStream` 返回 `ReadableStream`。

```js
let stream = new CompressionStream("gzip");
console.log(stream.readable); //一个 ReadableStream
```

规范
--

| Specification |
| --- |
| Streams Standard # dom-generictransformstream-readable |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


CompressionStream.writable - Web API 接口参考

CompressionStream.writable
==========================

`CompressionStream` 接口的 **`writable`** 属性返回 `WritableStream`。

值
-

一个 `WritableStream`。

示例
--

以下示例从 `CompressionStream` 返回 `WritableStream`。

```js
let stream = new CompressionStream("gzip");
console.log(stream.writeable); //一个 WritableStream
```

规范
--

| Specification |
| --- |
| Streams Standard # dom-generictransformstream-writable |

浏览器兼容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


网页音频接口的基本概念 - Web API 接口参考

网页音频接口的基本概念
===========

这篇文章解释了 网页音频接口 (Web Audio API) 运作过程中的部分音频处理概念。本文并不会将你变为一名音频处理大师，但它可以给你足够的背景知识来理解 网页音频接口 的运行原理，并能让你在使用它时做出更好的决策。

音频节点：模块化连接
----------

网页音频接口 (Web Audio API) 主要在 **音频环境 (audio context)** 中进行音频处理，它也允许模块间的连接。基本的音频处理在 **音频节点 (audio node)** 当中进行，这些节点连接在一起形成了 **音频导向图 (audio routing graph)**。多个声源，甚至包含不同种类的声道，都可以在一个音频环境中进行处理。这种模块化的设计使得人们在创建复杂多变的声音特效时可以更加灵活。

音频节点可以通过各自的输入与输出相连，形成一个 从一个或多个声源开始，经过处理节点，终止于末节点 的链式结构（有时你不需要末节点，比如你只是想数字化处理某些音频数据的时候）。一个简单、典型的网页音频接口的操作流程可以是这样的：

1. 创建一个音频环境
2. 在音频环境中，创建声源——例如`<audio>` 标签，振动发声器 (oscillator)，音频流
3. 创建特效节点——例如混响，双二阶滤波，声相控制，音频振幅压缩
4. 选择音频的终点——例如系统的扬声器
5. 连接声源和特效，以及特效和终点。

![A simple box diagram with an outer box labeled Audio context, and three inner boxes labeled Sources, Effects and Destination. The three inner boxes have arrow between them pointing from left to right, indicating the flow of audio information.](/zh-CN/docs/Web/API/Web_Audio_API/Basic_concepts_behind_Web_Audio_API/webaudioapi_en.svg)

每个输入和输出都可以包括几个声道，声道代表了一个特定的音效通道。各种声道分离结构都可以使用，包括*单声道*，*立体声*，*四声道*，*5.1*等等。

![Show the ability of AudioNodes to connect via their inputs and outputs and the channels inside these inputs/outputs.](/zh-CN/docs/Web/API/Web_Audio_API/Basic_concepts_behind_Web_Audio_API/audionodes_en.svg)

声源可以来自不同的地方：

* 直接通过 Javascript 生成声音节点产生（例如一个振动发声器）
* 由脉冲编码调制产生的原始数据（音频环境中可以调用一些方法来解码部分支持的格式）
* 从 HTML 元素中（例如 `<video>` 或者 `<audio>` 标签）
* 直接通过一个 WebRTC (en-US) `MediaStream` 获取流媒体（例如一个摄像头或麦克风）
音频数据：什么是样本
----------

当一个音频信号被处理时，取样意味着从一个连续的信号转化为离散的信号；更具体地说，一个连续的声波（例如一个正在演奏的乐队发出的声音）会被转化成一系列的样本点（一个时间上离散的信号），计算机只可以处理这些离散的样本块。

更多的细节可以查看维基百科的采样页面。

音频片段：帧、样本和声道
------------

一个 音频片段 (`AudioBuffer`) 会包含几个组成参数：一个或几个声道（1 代表*单声道*，2 代表*立体声*等等），一个长度（代表片段中采样帧的数目）和一个采样率（是每秒钟采样帧的个数）。

每个样本点都是一个 代表着该音频流在特定时间特定声道上的数值的 单精度浮点数。一个帧，或者一个采样帧是由一组在特定时间上的所有声道的样本点组成的——即所有声道在同一时间的样本点（*立体声*有 2 个，*5.1*有 6 个，等等，每个帧包含的样本点个数和声道数相同）。

采样率就是一秒钟内获取帧的个数，单位是赫兹 (Hz)。采样率越高，音频效果越好。

现在让我们来看一下通道，一个单声道和一个立体声的音频片段，每个都是 1 秒钟，播放频率（采样率）为 44100 赫兹：

* 单声道片段会有 44100 个样本点和 44100 个帧。长度属性为 44100。
* 立体声片段会有 88200 个样本点和 44100 个帧。长度属性依旧为 44100，因为长度总和帧的个数相同。

![A diagram showing several frames in an audio buffer in a long line, each one containing two samples, as the buffer has two channels, it is stereo.](/zh-CN/docs/Web/API/Web_Audio_API/Basic_concepts_behind_Web_Audio_API/sampleframe.svg)

当一个音频片段开始播放时，你将会听到最左侧的样本帧，之后是他右侧相邻的一帧，以此类推。在立体声中，你将会同时听到两个声道。样本帧的概念在此时非常有用，因为每个样本帧代表特定的播放时间，而和声道个数无关，这种方式很有利于精确的多声道同步处理。

**备注：** 只需用帧的数目除以采样率即可得到播放时间（单位为秒）。用样本点数目除以声道个数即可得到帧的数目。

下面我们将展示几个浅显易懂的示例：

```js
var context = new AudioContext();
var buffer = context.createBuffer(2, 22050, 44100);
```

如果你使用上面的方法调用，你将会得到一个立体声（两个声道）的音频片段 (Buffer)，当它在一个频率为 44100 赫兹（这是目前大部分声卡处理声音的频率）的音频环境中播放的时候，会持续 0.5 秒：22050 帧 / 44100 赫兹 = 0.5 秒。

**备注：** 在 数字音频 中，**44,100 赫兹** （有时也写作 **44.1 kHz**）是一个常见的 取样频率。为什么选取 44.1kHz 呢？首先，因为 人耳的接收频率 大约在 20 Hz 到 20,000 Hz 之间，根据 采样定理，采样频率一定要大于最终生成数据最大频率的二倍，因此就一定要大于 40,000 Hz（即 40kHz）。不仅如此，在采样之前信号还必须通过 低通滤波器 ，否则 会发生混叠现象，一个理想低通滤波器会完全留下低于 20kHz 的信号（且没有使它衰减）并完美阻拦一切高于 20kHz 的信号，而事实上 过度频带（英文）总是存在，在这个区域内信号会被部分衰减。这个频带越宽，建立一个 抗混叠滤波器 才越容易。因此我们选取 44.1kHz 允许我们有 2.05kHz 的空间预留给过度频带。

```js
var context = new AudioContext();
var buffer = context.createBuffer(1, 22050, 22050);
```

如果你这样调用，你将会得到一个单声道的音频片段 (Buffer)，当它在一个频率为 44100 赫兹的音频环境中播放的时候，将会被自动按照 44100 赫兹*重采样*（因此也会转化为 44100 赫兹的片段），并持续 1 秒：44100 帧 / 44100 赫兹 = 1 秒。

**备注：** 音频重采样与图片的缩放非常类似：比如你有一个 16 x 16 的图像，但是你想把它填充到一个 32 x 32 大小的区域，你就要对它进行缩放（重采样）。得到的结果会是一个较低品质的（图像会模糊或者有锯齿形的边缘，这取决于缩放采用的算法），但它却是能将原图形缩放，并且缩放后的图像占用空间比相同大小的普通图像要小。重新采样的音频道理相同——你会节约一些空间，但事实上你无法产出高频率的声音（高音区）。

### 分离式与交错式音频片段

网页音频接口使用了分离式的片段储存方式：左 (L) 右 (R) 声道像这样存储：
```

LLLLLLLLLLLLLLLLRRRRRRRRRRRRRRRR（对于一个有 16 帧的音频片段）
```

这种储存方式在音频处理中非常常见：这种方式允许对每个声道单独处理。

另一种储存方式是使用交错式的片段储存方式：
```

LRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLR（对于一个有 16 帧的音频片段）
```

这种方式在存储以及播放音频等不需要音频加工的操作中非常常见，例如一个解码后的 MP3 流媒体。

在开发者接触到的网页音频接口中**只有**分离式音频片段，因为它主要用于音频加工。在处理过程中它使用分离式，但是当它传送到声卡中用于播放时，音频片段会被转化为交错式。反过来，当 MP3 被解码时，初始状态它是交错式的，但是他会被转化成分离式以用于音频加工。

声道
--

不同的音频片段可能包含不同数量的声道个数，从基本的*单声道*（只有一个声道），*立体声*（左右两个声道），到更加复杂的*四声道*，*5.1*。由于每个声道中包含的音频数据可以不同，因此声道越多听觉效果越好。在不同声道模式中，表示特定声道的缩写如下表所示：

| *单声道* | `0: M: 唯一声道` | *Mono* | `0: M: mono` |
| --- | --- | --- | --- |
| *立体声* | `0: L: 左 1: R: 右` | *Stereo* | `0: L: left 1: R: right` |
| *四声道* | `0: L: 左 1: R: 右 2: SL: 环绕左 3: SR: 环绕右` | *Quad* | `0: L: left 1: R: right 2: SL: surround left 3: SR: surround right` |
| *5.1* | `0: L: 左 1: R: 右 2: C: 中央 3: LFE: 低音炮 4: SL: 环绕左 5: SR: 环绕右` | *5.1* | `0: L: left 1: R: right 2: C: center 3: LFE: subwoofer 4: SL: surround left 5: SR: surround right` |

**备注：** 由于缩写来自英文，因此保留英文作对照。

### 向上和向下混频

当输入与输出的声道数不同时，我们就需要按照如下方法进行混频。这些封装好的方法可以通过设置声音节点的 `AudioNode.channelInterpretation` (en-US) 属性为 `"speakers"`（扬声器）或 `"discrete"`（离散声道）进行混频。

| 混频方式 (Interpretation) | 输入声道模式 | 输出声道模式 | 混频详细规则 |
| --- | --- | --- | --- |
| `speakers`
（扬声器） | `1`*（单声道）* | `2`*（立体声）* | *从单声道到立体声的向上混频。*唯一的输入声道（M）会被同时用于立体声的两个声道（L 和 R）。`output.L = input.Moutput.R = input.M` |
| `1`*（单声道）* | 4*（四声道）* | *从单声道到四声道的向上混频。*唯一的输入声道（M）会被同时用于非环绕声的两个声道（L 和 R）。环绕声道（SL
 和 SR）将为静音。`output.L = input.Moutput.R = input.Moutput.SL = 0output.SR
= 0` |
| `1`*（单声道）* | 6*（5.1）* | *从单声道到 5.1 的向上混频。*唯一的输入声道（M）会被同时用于中央声道（C）。其余所有声道（L，R，LFE，SL，SR）都将保持静音。`output.L = 0output.R = 0``output.C = input.Moutput.LFE = 0output.SL = 0output.SR
= 0` |
| `2`*（立体声）* | `1`*（单声道）* | *从立体声到单声道的向下混频。*两个输入声道（L 和
 R）将会被均等的合并到唯一的输出声道（M）当中。`output.M = 0.5 * (input.L + input.R)` |
| `2`*（立体声）* | 4*（四声道）* | *从立体声到四声道的向上混频。*输入的左右声道（L 和
 R）分别对应输出的非环绕左右声道（L 和 R）。环绕左右声道（SL 和
 SR）将保持静音。`output.L = input.Loutput.R = input.Routput.SL = 0output.SR
= 0` |
| `2`*（立体声）* | 6*（5.1）* | *从立体声到 5.1 的向上混频。*输入的左右声道（L 和
 R）分别对应输出的非环绕左右声道（L 和 R）。其余所有输出声道（C，SL，SR
 和 LFE）将保持静音。`output.L = input.Loutput.R = input.Routput.C = 0output.LFE
= 0output.SL = 0output.SR = 0` |
| 4*（四声道）* | `1`*（单声道）* | *从四声道到单声道的向下混频。*四个输入声道（L，R，SL 和
 SR）将会被均等的合并到唯一的输出声道（M）当中。`output.M = 0.25 * (input.L + input.R +` `input.SL + input.SR``)` |
| 4*（四声道）* | `2`*（立体声）* | *从四声道到立体声的向下混频。*两个输入左声道（L 和
 SL）将会被均等的合并到输出的左声道（L）当中。相似的，两个输入右声道（R
 和 SR）将会被均等的合并到输出的右声道（R）当中。`output.L = 0.5 * (input.L + input.SL``)``output.R = 0.5 * (input.R + input.SR``)` |
| 4*（四声道）* | 6*（5.1）* | *从四声道到 5.1 的向上混频。*四个输入声道（L，R，SL 和
 SR）将会分别进入它们对应的输出声道（L，R，SL 和 SR）当中。其余输出声道（C
 和 LFE）将保持静音。`output.L = input.Loutput.R = input.Routput.C = 0output.LFE
= 0output.SL = input.SLoutput.SR = input.SR` |
| 6*（5.1）* | `1`*（单声道）* | *从 5.1 到单声道的向下混频。*输入的低音炮声道（LFE）将会被抛弃。其余声道按不同权值混合到唯一的输出声道（M）：输入的中央声道（C）权值为
 1，输入的非环绕侧声道（L 和 R）有所减弱，权值为`√2/2（即 1/√2`，`约等于 0.7071）`，输入的环绕声道（SL 和
 SR）进一步衰减，权值为 0.5。`output.M = 0.7071 * (input.L + input.R) + input.C + 0.5 * (input.SL +
input.SR)` |
| 6*（5.1）* | `2`*（立体声）* | *从 5.1 到立体声的向下混频。*输入的低音炮声道（LFE）将会被抛弃。对于每侧的输出声道（L 或
 R）：由输入的中央声道（C）先与同侧环绕声道（SL 或
 SR）混合，加权（权值为`√2/2`）后与输入的同侧非环绕声道（L 或
 R）混合得到。`output.L = input.L + 0.7071 * (input.C + input.SL)output.R =
input.R + 0.7071 * (input.C + input.SR)` |
| 6*（5.1）* | 4*（四声道）* | *从 5.1 到四声道的向下混频。*输入的低音炮声道（LFE）将会被抛弃。对于每侧的输出声道（L 或
 R）：由输入的中央声道（C）加权（权值为√2/2）后，与输入的同侧非环绕声道（L
 或 R）混合后得到。对于每侧的输出环绕声道（SL 或
 SR）：由同侧输入环绕声道（SL 或 SR）会不经改变直接传入。`output.L = input.L + 0.7071 * input.Coutput.R = input.R +
0.7071 * input.C`output.SL = input.SLoutput.SR = input.SR`` |
| 其他，非标准声道配置 |
 非标准的声道配置输入将会被按照
 `channelInterpretation` 属性设置为
 `discrete` 时的情况处理。

 W3C
 规范中明确指出允许未来定义新的声道配置标准，因此未来在浏览器中使用此项的输出结果可能与现在不相同。

 |
| `discrete`
（离散声道） | 任意（x 个声道） | 任意（y 个声道）其中`x<y` | *向上混频离散的声道。*根据相应的频道序号，将输入声道一对一的填入到输出声道中。对于没有输入声道能够对应的，该输出声道将保持静音。 |
| 任意（x 个声道） | 任意（y 个声道）其中`x>y` | *向下混频离散的声道。*根据相应的频道序号，将输入声道一对一的填入到输出声道中。对于没有输出声道能够对应的，该输入声道将被抛弃。 |

可视化
---

一般来说，可视化是通过获取各个时间上的音频数据（通常是振幅或频率），之后运用图像技术将其处理为视觉输出（例如一个图像）来实现的。网页音频接口提供了一个不会改变输入信号的音频节点 `AnalyserNode`，通过它可以获取声音数据并传递到像 `<canvas>` 等等一样的可视化工具。

![Without modifying the audio stream, the node allows to get the frequency and time-domain data associated to it, using a FFT.](/zh-CN/docs/Web/API/Web_Audio_API/Basic_concepts_behind_Web_Audio_API/fttaudiodata_en.svg)

你可以通过如下方法获取需要的音频数据：

`AnalyserNode.getFloatFrequencyData()`

返回一个`Float32Array` (en-US) 数组，其中包含传递到此音频节点声音的实时频率数据。

`AnalyserNode.getByteFrequencyData()`

返回一个`Uint8Array` (en-US) 无符号字节数组 (unsigned byte array)，其中包含传递到此音频节点声音的实时频率数据。

`AnalyserNode.getFloatTimeDomainData()` (en-US)

返回一个`Float32Array` (en-US) 数组，其中包含传递到此音频节点声音的实时波形，时间数据。

`AnalyserNode.getByteTimeDomainData()`

返回一个`Uint8Array` (en-US) 无符号字节数组 (unsigned byte array)，其中包含传递到此音频节点声音的实时波形，时间数据。

**备注：** 更多信息可以参考我们的这篇文章：基于 Web Audio API 实现音频可视化效果 。

空间位置化
-----

音频的空间化（由网页音频接口的 `PannerNode` (en-US) 和 `AudioListener` 节点处理）允许我们对空间中某一点的音频信号，以及这一信号的接听者建立位置和行为模型。

声相控制器的位置可以通过笛卡尔坐标系进行描述，控制器的运动可以由速度向量来表示，这会引起多普勒效应，它的传播方向可以用一个方向圆锥来表示，当它是一个全方向声源时，圆锥会变得非常大。

![The PannerNode brings a spatial position and velocity and a directionality for a given signal.](/zh-CN/docs/Web/API/Web_Audio_API/Basic_concepts_behind_Web_Audio_API/pannernode_en.svg)

接听者的位置可以用笛卡尔坐标系来表示；他的运动可以用方向向量表示；头部姿态可以用两个向量表示：一个向上向量表示头顶正对的方向，一个向前向量表示鼻子所指向的方向（面向的方向），这两个向量应该互相垂直。

![The PannerNode brings a spatial position and velocity and a directionality for a given signal.](/zh-CN/docs/Web/API/Web_Audio_API/Basic_concepts_behind_Web_Audio_API/listener.svg)

**备注：** 更多信息可以参考我们的这篇文章：网络音频位置空间化入门（英文）。

扇入与扇出
-----

对于音频来说，**扇入**是指 `ChannelMergerNode` 节点接收一系列单声道输入声源，并将它们整合输出为一个多声道音频信号的过程：

![](/zh-CN/docs/Web/API/Web_Audio_API/Basic_concepts_behind_Web_Audio_API/fanin.svg)

**扇出**恰恰相反，是指一个`ChannelSplitterNode` (en-US) 节点接收一个多声道输入声源并将它分离成多个单声道音频信号的过程：

![](/zh-CN/docs/Web/API/Web_Audio_API/Basic_concepts_behind_Web_Audio_API/fanout.svg)


Web Audio API 最佳实践 - Web API 接口参考

Web Audio API 最佳实践
==================

在创意编程中（creative coding）没有严格的对错之分。只要你充分考虑安全性、性能和 accessibility，你可以用自己的办法来编写代码。在这篇文章中，我们会分享一些最佳实践——包含使用 Web Audio API 的指导、小知识和小技巧。

加载声音/声音文件
---------

使用 Web Audio API 加载声音的主要方式有四种，你可能会对于应当使用哪种方式有些困惑。

在从文件中加载声音时，你可能会采取从`HTMLMediaElement` (即 `<audio>` 或`<video>` ) 中抓取的方式，或提取文件并将其解码到缓冲区。两种工作方式都是合理的，然而，在处理全长音轨时，前一种方法会更加常见。而后一种方法更常见于处理更短的（例如样本）的音轨。

多媒体类 HTML 元素有开箱即用的媒体流支持。音频会在浏览器判断可以在播放完成之前加载文件的剩余部分时进行播放 (when the browser determines it can load the rest of the file before playing finishes.)。你可以在Using the Web Audio API tutorial这篇文档中看到一个把多媒体类 HTML 元素与 Web Audio API 结合使用的例子。

如果你使用缓冲节点（buffer node）来加载音频，你将会有更多的控制权。虽然你需要请求这个文件，然后等待它加载完成 (我们的这篇进阶文章中的这一节 (en-US)介绍了一个好办法)。但是，随后你可以直接访问数据，这意味着你能进行更精确，更精确的操作。

对于来自用户的摄像头或麦克风的音频，你可以考虑通过Media Stream API和`MediaStreamAudioSourceNode`接口来访问。这在与 WebRTC 协作以及你想录制或分析音频的场合下很管用。

最后一个要介绍的方法时如何生成声音。这可以通过`OscillatorNode`和创建一个缓冲区 (buffer) 然后向其中填充你的数据来完成。你可以在这篇指导你如何创建自己的乐器的文章 (en-US)中学习到用这两个工具创建声音的知识。

Cross browser & legacy support
------------------------------

The Web Audio API specification is constantly evolving and like most things on the web, there are some issues with it working consistently across browsers. Here we'll look at options for getting around cross-browser problems.

There's the `standardised-audio-context` npm package, which creates API functionality consistently across browsers, full holes as they are found. It's constantly in development and endeavours to keep up with the current specification.

There is also the option of libraries, of which there are a few depending on your use case. For a good all-rounder, howler.js is a good choice. It has cross-browser support and, provides a useful subset of functionality. Although it doesn't harness the full gamut of filters and other effects the Web Audio API comes with, you can do most of what you'd want to do.

If you are looking for sound creation or a more instrument-based option, tone.js is a great library. It provides advanced scheduling capabilities, synths, and effects, and intuitive musical abstractions built on top of the Web Audio API.

R-audio, from the BBC's Research & Development department, is a library of React components aiming to provide a "more intuitive, declarative interface to Web Audio". If you're used to writing JSX it might be worth looking at.

自动播放策略
------

浏览器已经开始实施自动播放策略，这一策略通常可以概括为：

>
> "Create or resume context from inside a user gesture".
>
>
>

这在实践中意味着什么呢？user gesture 可以解释为用户触发的事件（一般来说，是`click`事件）。浏览器厂商判定不应该允许 Web Audio 上下文自动播放音频，而应该由用户开始播放。这是因为自动播放音频非常烦人且令人讨厌。那么，我们该如何处理 (handle) 呢？

无论是 offline 还是 online，当你创建了一个音频上下文 (audio context)，它会有一个内部状态 (`state`)，这个状态有`暂停 (suspend)、播放 (running)、关闭 (closed)`三种可能。

(When you create an audio context (either offline or online) it is created with a `state`, which can be `suspended`, `running`, or `closed`.)

例如，在使用 `AudioContext`时，如果你在`click`事件中创建了音频上下文，它的内部状态应该会被自动设置成`播放 (running)`。这里有一个在`click`事件中创建音频上下文简单的例子：

```js
const button = document.querySelector("button");
button.addEventListener(
  "click",
  function () {
    const audioCtx = new AudioContext();
  },
  false,
);
```

如果你在用户动作之外创建上下文 (create the context outside of a user gesture)，它的内部状态会被设置为`暂停 (suspend)`。这里我们可以同样用 click 事件的例子。我们会检查这个上下文的状态，并且启动它。如果它是`暂停 (suspend)`的状态，使用`resume()` (en-US)方法来恢复。

```js
const audioCtx = new AudioContext();
const button = document.querySelector("button");

button.addEventListener(
  "click",
  function () {
    // check if context is in suspended state (autoplay policy)
    if (audioCtx.state === "suspended") {
      audioCtx.resume();
    }
  },
  false,
);
```

对于`OfflineAudioContext`，你也可以使用`startRendering()` (en-US)方法来恢复到播放状态。

User control
------------

If your website or application contains sound, you should allow the user control over it, otherwise again, it will become annoying. This can be achieved by play/stop and volume/mute controls. The Using the Web Audio API tutorial goes over how to do this.

If you have buttons that switch audio on and off, using the ARIA `role="switch"` (en-US) attribute on them is a good option for signalling to assistive technology what the button's exact purpose is, and therefore making the app more accessible. There's a demo of how to use it here.

As you work with a lot of changing values within the Web Audio API and will want to provide users with control over these, the `range input` is often a good choice of control to use. It's a good option as you can set minimum and maximum values, as well as increments with the `step` attribute.

Setting AudioParam values
-------------------------

There are two ways to manipulate `AudioNode` values, which are themselves objects of type `AudioParam` interface. The first is to set the value directly via the property. So for instance if we want to change the `gain` value of a `GainNode` we would do so thus:

```js
gainNode.gain.value = 0.5;
```

This will set our volume to half. However, if you're using any of the `AudioParam`'s defined methods to set these values, they will take precedence over the above property setting. If for example, you want the `gain` value to be raised to 1 in 2 seconds time, you can do this:

```js
gainNode.gain.setValueAtTime(1, audioCtx.currentTime + 2);
```

It will override the previous example (as it should), even if it were to come later in your code.

Bearing this in mind, if your website or application requires timing and scheduling, it's best to stick with the `AudioParam` methods for setting values. If you're sure it doesn't, setting it with the `value` property is fine.