[译]WebGL 基础系列:WebGL 基本原理

原文地址:http://webglfundamentals.org/webgl/lessons/webgl-fundamentals.html

WebGL 架构

WebGL makes it possible to display amazing realtime 3D graphics in your
browser but what many people don’t know is that
WebGL is actually a rasterization API, not a 3D API.

WebGL 让你在浏览器中可以渲染出令人惊叹的 3D 图形效果,但是大多数人都不知道WebGL 其实只是光栅化 API ,而不是 3D API

Let me explain.

下面我来详细解释下。

WebGL only cares about 2 things. Clipspace coordinates and colors.
Your job as a programmer using WebGL is to provide WebGL with those 2 things.
You provide 2 “shaders” to do this. A Vertex shader which provides the
clipspace coordinates and a fragment shader that provides the color.

WebGL 只关心2件事情:空间坐标和颜色。作为 WebGL 的开发者,你的工作就是把这2个信息提供给 WebGL。具体做法就是使用2种着色器:顶点着色器和片元着色器。顶点着色器包含了空间坐标信息,片元着色器包含了颜色信息。

Clipspace coordinates always go from -1 to +1 no matter what size your
canvas is. Here is a simple WebGL example that shows WebGL in its simplest form.

需要注意,在 WebGL 中,无论 canvas 有多大,空间坐标系的坐标范围都是[-1, 1]。
下面的代码是一个简单的例子,展示了 WebGL 编程的基本流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 获取 WebGL 上下文
var canvas = document.getElementById("canvas");
var gl = canvas.getContext("experimental-webgl");

// 设置 GLSL 程序
var program = createProgramFromScripts(gl, ["2d-vertex-shader", "2d-fragment-shader"]);
gl.useProgram(program);

// 绑定顶点数据
var positionLocation = gl.getAttribLocation(program, "a_position");

// 创建缓存对象,并把矩形的顶点坐标放入缓存对象
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
-1.0, -1.0,
1.0, -1.0,
-1.0, 1.0,
-1.0, 1.0,
1.0, -1.0,
1.0, 1.0]),
gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

// 绘制图形
gl.drawArrays(gl.TRIANGLES, 0, 6);

Here’s the 2 shaders

着色器代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

void main() {
gl_Position = vec4(a_position, 0, 1);
}
</script>

<script id="2d-fragment-shader" type="x-shader/x-fragment">
void main() {
gl_FragColor = vec4(0, 1, 0, 1); // green
}
</script>

This will draw a green rectangle the entire size of the canvas. Here it is

上面的代码会在 canvas 上画一个绿色的矩形。示例如下:

Not very exciting :-p

看起来似乎并不是很令人兴奋。

Again, clipspace coordinates always go from -1 to +1 regardless of the
size of the canvas. In the case above you can see we are doing nothing
but passing on our position data directly. Since the position data is
already in clipspace there is no work to do. If you want 3D it’s up to you
to supply shaders that convert from 3D to clipspace because WebGL is only
a rasterization API
.

再次提醒下,空间坐标范围永远都是[-1, 1],而不受 canvas 大小影响。从上面的例子看出,我们其实没做任何事情,只是把位置数据传递了过去。这是因为这些位置数据已经是符合要求的坐标数据。如果你想实现 3D 效果,那么你需要将 3D 数据转换为 WebGL 坐标数据给着色器,因为 WebGL 只是光栅化 API

For 2D stuff you would probably rather work in pixels than clipspace so
let’s change the shader so we can supply rectangles in pixels and have
it convert to clipspace for us. Here’s the new vertex shader

对于 2D 开发者,更多时间会同像素打交道而非这里的空间坐标。因此,我们这里需要修改下着色器,当我们传递的是像素信息时,它能自动为我们转换成合适的空间坐标。下面是新的顶点着色器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform vec2 u_resolution;

void main() {
// convert the rectangle from pixels to 0.0 to 1.0
vec2 zeroToOne = a_position / u_resolution;

// convert from 0->1 to 0->2
vec2 zeroToTwo = zeroToOne * 2.0;

// convert from 0->2 to -1->+1 (clipspace)
vec2 clipSpace = zeroToTwo - 1.0;

gl_Position = vec4(clipSpace, 0, 1);
}
</script>

Now we can change our data from clipspace to pixels

现在我们可以把数据直接从空间坐标修改为像素值了。

1
2
3
4
5
6
7
8
9
10
11
12
// set the resolution
var resolutionLocation = gl.getUniformLocation(program, "u_resolution");
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);

// setup a rectangle from 10,20 to 80,30 in pixels
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
10, 20,
80, 20,
10, 30,
10, 30,
80, 20,
80, 30]), gl.STATIC_DRAW);

And here it is

示例如下:

You might notice the rectangle is near the bottom of that area. WebGL considers the bottom left
corner to be 0,0. To get it to be the more traditional top left corner used for 2d graphics APIs
we just flip the y coordinate.

你可能已经注意到了矩形位于区域的左下角,WebGL 将左下角坐标设置为(0,0)。而在 2d 图形 API 中均把左上角看做原点。为了符合这样的习惯,我们只需翻转下 y 轴坐标即可。

1
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);

And now our rectangle is where we expect it.

现在矩形就显示在了我们期望的位置上。

Let’s make the code that defines a rectangle into a function so
we can call it for different sized rectangles. While we’re at it
we’ll make the color settable.

现在我们修改下代码,将定义矩形的代码放入函数,这样每次调用函数都可以得到不同大小的矩形。同时,我们用变量来存储颜色值。

First we make the fragment shader take a color uniform input.

首先,我们在片元着色器中设置一个 uniform 类型的变量来存储颜色值。

1
2
3
4
5
6
7
8
9
<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

uniform vec4 u_color;

void main() {
gl_FragColor = u_color;
}
</script>

And here’s the new code that draws 50 rectangles in random places and random colors.

下面的新代码绘制了50个矩形,每个矩形都有随机的位置和颜色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
  var colorLocation = gl.getUniformLocation(program, "u_color");
...
// 创建缓存对象
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

// 绘制50个随机位置和颜色的矩形
for (var ii = 0; ii < 50; ++ii) {
// 随机产生一个位置
setRectangle(
gl, randomInt(300), randomInt(300), randomInt(300), randomInt(300));

// 随机产生一个颜色
gl.uniform4f(colorLocation, Math.random(), Math.random(), Math.random(), 1);

// 绘制矩形
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
}

// 随机返回0或-1
function randomInt(range) {
return Math.floor(Math.random() * range);
}

// 将一个矩形的相关信息放入缓存对象
function setRectangle(gl, x, y, width, height) {
var x1 = x;
var x2 = x + width;
var y1 = y;
var y2 = y + height;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
x1, y1,
x2, y1,
x1, y2,
x1, y2,
x2, y1,
x2, y2]), gl.STATIC_DRAW);
}

And here’s the rectangles.

下面的示例展示了这些矩形效果:

I hope you can see that WebGL is actually a pretty simple API.
While it can get more complicated to do 3D that complication is
added by you, the programmer, in the form of more complex shaders.
The WebGL API itself is 2D and fairly simple.

我希望你能认识到,WebGL 本身的 API 非常简单。但当它处理 3D 效果时又变得非常复杂。这种复杂性完全是由开发者自己提升上去的,因为在开发过程中使用了很多复杂的着色器。WebGL API 本身是 2D 的,并且非常简单。

If you’re 100% new to WebGL and have no idea what GLSL is or shaders or what the GPU does
then checkout the basics of how WebGL really works.

如果你完全不了解 WegGL,不知道 GLSL 和着色器,也不明白 GPU 是做什么的,那么你可以先去看看WebGL 工作的基本原理

Otherwise from here you can go in 2 directions. If you are interested in image procesing
I’ll show you how to do some 2D image processing.
If you are interesting in learning about translation,
rotation and scale then start here.

从这里开始,你有两个方向可以选择。如果你对图像处理感兴趣,可以先看看如何处理 2D 图像。如果你对学习变换、旋转和缩放感兴趣,那么可以看看WebGL 2D 变换


type=”x-shader/x-vertex” 和 type=”x-shader/x-fragment” 的作用是什么?

<script> tags default to having JavaScript in them. You can put no type or you can put type=”javascript” or type=”text/javascript” and the browser will interpret the contents as JavaScript. If you put anything else the browser ignores the contents of the script tag.

<script> 标签默认情况下里面包含了 JavaScript 代码。你可以通过设置 type 的值来让浏览器正确处理里面的 JavaScript 代码。如果你设置的是其他值,那么浏览器就会忽略处理标签内的内容。

We can use this feature to store shaders in script tags. Even better, we can make up our own type and in our javascript look for that to decide whether to compile the shader as a vertex shader or a fragment shader.

我们可以使用这个特性在 script 标签中存储着色器代码。甚至,我们还可以自定义 type 类型,从而告诉后续的编译器将着色器代码编译为顶点着色器还是片元着色器。

In this case the function createProgramFromScripts looks for scripts with specified ids and then looks at the type to decide what type of shader to create.

在上面的例子中,createProgramFromScripts 方法通过特殊id来查找 script 标签,然后根据 type 值来创建不同类型的着色器。

createProgramFromScripts is part of some boilerplate like code that almost every WebGL program needs.

所有 WebGL 程序需要 createProgramFromScripts 方法,可参考WebGL Boilerplate