2.WebGL:实时 3D 渲染

1.准备 3D 渲染

1.搭基础框架

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
42
<head>
<meta charset="UTF-8">
<title>Title</title>
<script type="javascript">
var gl;
function start(){
var canvas = document.getElementById("glcanvas");
gl = initWebGL(canvas); // 初始化上下文
if(gl){
// 设置清除颜色为黑色,不透明
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 开启“深度测试”, Z-缓存
gl.enable(gl.DEPTH_TEST);
// 设置深度测试,近的物体遮挡远的物体
gl.depthFunc(gl.LEQUAL);
// 清除颜色和深度缓存 -- 如果是开始绘画的话,需要把这一步被搬到创建完顶点后,开始绘画之前
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
}
}
function initWebGL(canvas) {
winodw.gl = null;
try {
// 尝试获取标准上下文,如果失败,回退到试验性上下文
gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
}
catch(e) {}

// 如果没有GL上下文,马上放弃
if (!gl) {
alert("WebGL初始化失败,可能是因为您的浏览器不支持。");
gl = null;
}
return gl;
}
</script>
</head>

<body onload="start()">
<canvas id="glcanvas" width="640" height="480">
Your browser doesn't appear to support the HTML5 <code>&lt;canvas&gt;</code> element.
</canvas>
</body>

2.调整 WebGL 上下文尺寸

初始视窗分辨率 = canvas 元素的高度和宽度;
修改css不会改变分辨率
修改canvas宽高也不会改变分辨率

使用上面例子中用到的 ‘gl’ 和 ‘canvas’ 修改 WebGL 的渲染分辨率:
注意:这修改的是里边图像的尺寸,而不是画布的尺寸!

1
gl.viewport(0, 0, canvas.width, canvas.height);

2.渲染场景

创建着色器,通过它来渲染我们的简单场景并画出我们的物体

1.初始化着色器

为了更方便地处理和更新内容,事实上我们可以将着色器代码写在HTML文档上,而不是把所有代码都写在 JavaScript里

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
function initShaders() {
// getShader() 函数,实现将着色器程序从DOM元素加载到着色器。shader-fs为id选择器
// 片段着色器
var fragmentShader = getShader(gl, "shader-fs");
// 顶点着色器
var vertexShader = getShader(gl, "shader-vs");

// createProgram()创建着色器
shaderProgram = gl.createProgram();
// 关联着色器
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
// 链接着色器
gl.linkProgram(shaderProgram);

// 如果创建着色器失败
// gl.LINK_STATUS检查是否链接成功
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert("Unable to initialize the shader program.");
}
// 设置待用的着色器
gl.useProgram(shaderProgram);
// 获取指向着色器参数的指针
vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
gl.enableVertexAttribArray(vertexPositionAttribute);
}

2.从DOM中加载着色器

创建片段和顶点着色器用

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
42
43
44
45
46
47
48
function getShader(gl, id) {
var shaderScript, theSource, currentChild, shader;

shaderScript = document.getElementById(id);

if (!shaderScript) {
return null;
}

theSource = "";
currentChild = shaderScript.firstChild;

while(currentChild) {
if (currentChild.nodeType == currentChild.TEXT_NODE) {
theSource += currentChild.textContent;
}

currentChild = currentChild.nextSibling;
}

// 一旦找到指定ID的元素,其文本内容将被读取保存到变量 theSource
// 根据着色器对象的 MIME 属性来判断它是什么着色器 (MIME type "x-shader/x-vertex")
if (shaderScript.type == "x-shader/x-fragment") {
// 片断
shader = gl.createShader(gl.FRAGMENT_SHADER);
} else if (shaderScript.type == "x-shader/x-vertex") {
// 顶点
shader = gl.createShader(gl.VERTEX_SHADER);
} else {
// Unknown shader type
return null;
}

// 将源码传到着色器
gl.shaderSource(shader, theSource);

// 编译着色器
gl.compileShader(shader);

// gl.COMPILE_STATUS 检查编译是否成功
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert("An error occurred compiling the shaders: " + gl.getShaderInfoLog(shader));
return null;
}

return shader;

}

3.着色器

我们需要将着色器程序代码加入到 HTML 文档,也可以写到js
着色器的语法请看OpenGL

片段着色器

每一个像素都叫一个片段
gl_FragColor 是一个 GL 内置的特殊变量,用于片段的色彩填充。
设定它的值就是设定每个像素的颜色。

1
2
3
4
5
<script id="shader-fs" type="x-shader/x-fragment">
void main(void) {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
</script>

顶点着色器

定义了每个顶点的位置和形状

1
2
3
4
5
6
7
8
9
10
<script id="shader-vs" type="x-shader/x-vertex">
attribute vec3 aVertexPosition;

uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
}
</script>

3.创建对象-绑定顶点

创建一个缓冲器来存储它的顶点
用到名字为 initBuffers() 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var horizAspect = 480.0/640.0;

function initBuffers() {
squareVerticesBuffer = gl.createBuffer();
// 选择squareVerticesBuffer作为一个顶点
gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesBuffer);
// 现在创建一个数组顶点的方形
var vertices = [
1.0, 1.0, 0.0,
-1.0, 1.0, 0.0,
1.0, -1.0, 0.0,
-1.0, -1.0, 0.0
];
// 现在通过顶点的列表构建WebGL形状。我们通过JavaScript数组创建一个Float32Array,然后使用它来填补当前顶点缓冲。
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
}

首先,它调用 gl 的成员函数 createBuffer() 得到了缓冲对象并存储在顶点缓冲器。
然后调用 bindBuffer() 函数绑定上下文。
创建一个Javascript 数组去记录每一个正方体的每一个顶点。
然后将其转化为 WebGL 浮点型类型的数组,并将其传到 gl 对象的 bufferData() 方法来建立对象的顶点。

4.绘制场景

当着色器和物体都创建好后,我们可以开始渲染这个场景了。
因为我们这个例子不会产生动画,所以 drawScene() 方法非常简单。

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
function drawScene() {
// 开始绘画前先清除画布 --清除颜色和深度缓存
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 创建相机透视角矩阵,角度,宽高比,对象所处距离范围
perspectiveMatrix = makePerspective(45, 640.0/480.0, 0.1, 100.0);
// 将图纸位置设置为“身份”,这是场景的中心
loadIdentity();
// 现在将这个物体挪动到可视范围
mvTranslate([-0.0, 0.0, -6.0]);
// 选择squareVerticesBuffer作为一个顶点
gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesBuffer);
// 建立着色器参数之间的关联:顶点和投影/模型矩阵
gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
//
setMatrixUniforms();
// 绘制物
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
// 补:
function loadIdentity() {
mvMatrix = Matrix.I(4);
}
function mvTranslate(v) {
multMatrix(Matrix.Translation($V([v[0], v[1], v[2]])).ensure4x4());
}
function multMatrix(m) {
mvMatrix = mvMatrix.x(m);
}
function setMatrixUniforms() {
// 获取 GLSL程序中定义的各个变量的句柄,从而可以用 JavaScript中定义的数值来初始化这些变量
var pUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
gl.uniformMatrix4fv(pUniform, false, new Float32Array(perspectiveMatrix.flatten()));

var mvUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
gl.uniformMatrix4fv(mvUniform, false, new Float32Array(mvMatrix.flatten()));
}

第一步,用背景色擦除上下文,接着建立摄像机透视矩阵
设置45度的视图角度,并且宽高比设为 640/480(画布尺寸)。
指定在摄像机距离0.1到100单位长度的范围内,物体可见。

接着加载特定位置,并把正方形放在距离摄像机6个单位的的位置。

然后,我们绑定正方形的顶点缓冲到上下文,并配置好,再通过调用 drawArrays() 方法来画出对象。

5.正方形源码

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="sylvester.js" type="text/javascript"></script>
<script src="glUtils.js" type="text/javascript"></script>
<!-- Fragment shader program -->

<script id="shader-fs" type="x-shader/x-fragment">
void main(void) {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
</script>

<!-- Vertex shader program -->

<script id="shader-vs" type="x-shader/x-vertex">
attribute vec3 aVertexPosition;

uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
}
</script>
<script type="text/javascript">
var canvas;
var gl;
var squareVerticesBuffer;
var mvMatrix;
var shaderProgram;
var vertexPositionAttribute;
var perspectiveMatrix;
function start(){
var canvas = document.getElementById("glcanvas");
gl = initWebGL(canvas); // 初始化上下文
if(gl){
// 设置清除颜色为黑色,不透明
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// gl.clearDepth(1.0); // Clear everything-add
// 开启“深度测试”, Z-缓存
gl.enable(gl.DEPTH_TEST);
// 设置深度测试,近的物体遮挡远的物体
gl.depthFunc(gl.LEQUAL);
// 清除颜色和深度缓存 -- 这一步被搬到创建完顶点后,开始绘画之前去了
// gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);

// ---------------------------
// 开始初始化着色器
var fragmentShader = getShader(gl,"shader-fs");
var vertexShader = getShader(gl, "shader-vs");

// 创建着色器
shaderProgram = gl.createProgram();

// 关联着色器
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);

//链接着色器
gl.linkProgram(shaderProgram)

// 如果创建着色器失败
if(!gl.getProgramParameter(shaderProgram,gl.LINK_STATUS)){
alert("Unable to initialize the shader program: " + gl.getProgramInfoLog(shader));
}
// 设置待用的着色器
gl.useProgram(shaderProgram);
// 获取指向着色器参数的指针
vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
gl.enableVertexAttribArray(vertexPositionAttribute);

// -----------创建顶点对象
squareVerticesBuffer = gl.createBuffer();
// 选择squareVerticesBuffer作为一个顶点
gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesBuffer);

// 现在创建一个数组顶点的方形。
var vertices = [
1.0, 1.0, 0.0,
-1.0, 1.0, 0.0,
1.0, -1.0, 0.0,
-1.0, -1.0, 0.0
];
// 现在通过顶点的列表构建WebGL形状。我们通过JavaScript数组创建一个Float32Array,然后使用它来填补当前顶点缓冲。
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

// ------开始画图
setInterval(function () {
// 开始绘画前先清除画布
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 创建相机透视角矩阵,角度,宽高比,对象所处距离范围
perspectiveMatrix = makePerspective(45, 640.0/480.0, 0.1, 100.0);

mvMatrix = Matrix.I(4);
// 现在将这个物体挪动到可视范围
function multMatrix(m) {
mvMatrix = mvMatrix.x(m);
}
multMatrix(Matrix.Translation($V([-0.0, 0.0, -6.0])).ensure4x4());
gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesBuffer);
gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
// 获取 GLSL程序中定义的各个变量的句柄,从而可以用 JavaScript中定义的数值来初始化这些变量
var pUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
gl.uniformMatrix4fv(pUniform, false, new Float32Array(perspectiveMatrix.flatten()));

var mvUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
gl.uniformMatrix4fv(mvUniform, false, new Float32Array(mvMatrix.flatten()));
// 绘制物
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}, 15);
}

}
function initWebGL(canvas) {
window.gl = null;
try {
// 尝试获取标准上下文,如果失败,回退到试验性上下文
gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
}
catch(e) {}

// 如果没有GL上下文,马上放弃
if (!gl) {
alert("WebGL初始化失败,可能是因为您的浏览器不支持。");
gl = null;
}
return gl;
}
function getShader(gl, id) {
var shaderScript = document.getElementById(id);

// Didn't find an element with the specified ID; abort.

if (!shaderScript) {
return null;
}

// Walk through the source element's children, building the
// shader source string.

var theSource = "";
var currentChild = shaderScript.firstChild;

while(currentChild) {
if (currentChild.nodeType == 3) {
theSource += currentChild.textContent;
}

currentChild = currentChild.nextSibling;
}

// Now figure out what type of shader script we have,
// based on its MIME type.

var shader;

if (shaderScript.type == "x-shader/x-fragment") {
shader = gl.createShader(gl.FRAGMENT_SHADER);
} else if (shaderScript.type == "x-shader/x-vertex") {
shader = gl.createShader(gl.VERTEX_SHADER);
} else {
return null; // Unknown shader type
}

// Send the source to the shader object

gl.shaderSource(shader, theSource);

// 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));
return null;
}

return shader;
}

</script>
</head>

<body onload="start()">
<canvas id="glcanvas" width="640" height="480">
Your browser doesn't appear to support the HTML5 <code>&lt;canvas&gt;</code> element.
</canvas>
</body>

6.给顶点着色

添加颜色可以通过修改着色器来实现。
我们以前的顶点着色器没有给顶点添加任何特定的颜色——在顶点着色器与片段着色器之间给每个像素着白色(片段着色器有定义),于是整个正方形被渲染成纯白。

1.首先我们要创建一个顶点颜色数组

1
2
3
4
5
6
7
8
9
10
11
var 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 // 蓝色
];

squareVerticesColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesColorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
}

然后将它们存在WebGL的缓冲区中
写法和顶点放到缓存区是一样的

2.修改顶点着色器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script id="shader-vs" type="x-shader/x-vertex">
attribute vec3 aVertexPosition;
attribute vec4 aVertexColor;

uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

varying lowp vec4 vColor;

void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
vColor = aVertexColor;
}
</script>

3.修改片段着色器

1
2
3
4
5
6
7
<script id="shader-fs" type="x-shader/x-fragment">
varying lowp vec4 vColor;

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

4.颜色的绘制
要在 initShader() 中初始化颜色属性,以便着色器程序使用

1
2
3
// 获取指向着色器参数的指针
vertexColorAttribute = gl.getAttribLocation(shaderProgram, "aVertexColor");
gl.enableVertexAttribArray(vertexColorAttribute);

5.修改 drawScene() 使之在绘制正方形时使用这些颜色:

1
2
gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesColorBuffer);
gl.vertexAttribPointer(vertexColorAttribute, 4, gl.FLOAT, false, 0, 0);

7.让目标动起来

1.旋转正方形

首先我们需要一个变量来追踪正方形当前的旋转情况:

1
var squareRotation = 0.0;

更新 drawScene()进行旋转绘制,使用如下方法进行旋转:

1
2
mvPushMatrix();   // 存储了当前模型视图矩阵
mvRotate(squareRotation, [1, 0, 1]);

首先存储了当前模型视图矩阵
然后按照squareRotation的值绕X轴和Z轴旋转矩阵

绘制完成后,需要加载回之前的矩阵:

1
mvPopMatrix();

存储后再重加载原始矩阵的目的是:避免绘制其它图形时也受到这次旋转的影响。
为了让物体动起来,我们需要随时改变squareRotation的值。
我们可以建立一个新的变量来追踪上一次动作的时间(可以称这个变量为 lastSquareUpdateTime ),
用当前时间与上次动作的时间差更新 squareRotation 决定旋转的角度。
并将如下代码添加到 drawScene() 函数的末尾:

1
2
3
4
5
6
7
8
var currentTime = Date.now();
if (lastSquareUpdateTime) {
var delta = currentTime - lastSquareUpdateTime;

squareRotation += (30 * delta) / 1000.0;
}

lastSquareUpdateTime = currentTime;

2.移动正方形

在绘制正方形之前,可以先将图形移动到其它位置。
让我们用一些新的变量来追踪每个轴的偏移量:

1
2
3
var squareXOffset = 0.0;
var squareYOffset = 0.0;
var squareZOffset = 0.0;

用如下的变量来表示每个轴上的变化量:

1
2
3
var xIncValue = 0.2;
var yIncValue = -0.4;
var zIncValue = 0.3;

在之前的旋转代码中加入如下的部分:

1
2
3
4
5
6
7
8
9
squareXOffset += xIncValue * ((30 * delta) / 1000.0);
squareYOffset += yIncValue * ((30 * delta) / 1000.0);
squareZOffset += zIncValue * ((30 * delta) / 1000.0);

if (Math.abs(squareYOffset) > 2.5) {
xIncValue = -xIncValue;
yIncValue = -yIncValue;
zIncValue = -zIncValue;
}

最后,将下面的代码加入 drawScene() 函数中:

1
mvTranslate([squareXOffset, squareYOffset, squareZOffset]);

3.源码展示

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
var squareRotation = 0.0;
var squareXOffset = 0.0;
var squareYOffset = 0.0;
var squareZOffset = 0.0;
var xIncValue = 0.2;
var yIncValue = -0.4;
var zIncValue = 0.3;
var lastSquareUpdateTime = 0;
// 2.添加动画------开始
var mvMatrixStack = [];
function mvPushMatrix(m) {
if (m) {
mvMatrixStack.push(m.dup());
mvMatrix = m.dup();
} else {
mvMatrixStack.push(mvMatrix.dup());
}
}
mvPushMatrix();
function mvRotate(angle, v) {
var inRadians = angle * Math.PI / 180.0;

var m = Matrix.Rotation(inRadians, $V([v[0], v[1], v[2]])).ensure4x4();
multMatrix(m);
}
function mvTranslate(v) {
multMatrix(Matrix.Translation($V([v[0], v[1], v[2]])).ensure4x4());
}
mvRotate(squareRotation, [1, 0, 1]);
mvTranslate([squareXOffset, squareYOffset, squareZOffset]);
// 2.添加动画------结束

gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
// 2.添加动画------开始
function mvPopMatrix() {
if (!mvMatrixStack.length) {
throw("Can't pop from an empty matrix stack.");
}

mvMatrix = mvMatrixStack.pop();
return mvMatrix;
}
mvPopMatrix();
var currentTime = Date.now();
if (lastSquareUpdateTime) {
var delta = currentTime - lastSquareUpdateTime;

squareRotation += (30 * delta) / 1000.0;
squareXOffset += xIncValue * ((30 * delta) / 1000.0);
squareYOffset += yIncValue * ((30 * delta) / 1000.0);
squareZOffset += zIncValue * ((30 * delta) / 1000.0);

if (Math.abs(squareYOffset) > 2.5) {
xIncValue = -xIncValue;
yIncValue = -yIncValue;
zIncValue = -zIncValue;
}
}
lastSquareUpdateTime = currentTime;
// 2.添加动画------结束

8.创建三维立方体

给之前的正方形添加五个面从而可以创建一个三维的立方体
最简单的方式就是通过调用方法 gl.drawElements() 使用顶点数组列表来替换之前的通过方法 gl.drawArrays() 直接使用顶点数组。

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

1.定义立方体顶点位置

更新 initBuffers() 函数代码创建顶点位置数据缓存。
现在的代码看起来和渲染正方形时的代码很相似,只是比之前的代码更长因为现在有了24个顶点(每个面使用4个顶点):

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
// 例:正面有4个顶点,分别为x,y,z的坐标
var vertices = [
// 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
];

2.定义顶点颜色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var colors = [
[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
];

var generatedColors = [];

for (j=0; j<6; j++) {
var c = colors[j];

for (var i=0; i<4; i++) {
generatedColors = generatedColors.concat(c);
}
}

var cubeVerticesColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVerticesColorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(generatedColors), gl.STATIC_DRAW);

3.定义元素(三角形)数组

既然已经创建好了顶点数组,接下来就要创建元素(三角形)数组了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var cubeVerticesIndexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVerticesIndexBuffer);

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

var cubeVertexIndices = [
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(cubeVertexIndices), gl.STATIC_DRAW);

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

4.渲染立方体

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

1
2
3
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVerticesIndexBuffer);
setMatrixUniforms();
gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);

9.纹理贴图

1.加载纹理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function initTextures() {
cubeTexture = gl.createTexture();
cubeImage = new Image();
cubeImage.onload = function() { handleTextureLoaded(cubeImage, cubeTexture); }
cubeImage.src = "cubetexture.png";
}

function handleTextureLoaded(image, texture) {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
gl.generateMipmap(gl.TEXTURE_2D);
gl.bindTexture(gl.TEXTURE_2D, null);
}

函数 initTextures() 首先调用 GL createTexture() 函数来创建一个GL纹理对象 cubeTexture 。为了把图片文件加载到纹理,代码首先创建了一个 Image 对象然后把需要当作纹理使用的图形文件加载了进来。当图片加载完成后回调函数 handleTextureLoaded() 就会执行。
接下来为了真正地形成纹理,我们通过把新创建的纹理对象绑定到 gl.TEXTURE_2D 来让它成为当前操作纹理。然后通过调用 texImage2D() 把已经加载的图片图形数据写到纹理。
注意:在多数情况下,纹理的宽和高都必须是2的幂(如:1,2,4,8,16等等)。如果有什么特殊情况请参考下面的“非2的幂纹理”小节。
代码的接下来两行设置了纹理过滤器,过滤器用来控制当图片缩放时像素如何生成如何插值。在这个例子里,我们对图片放大使用的是线性过滤,而对图片缩小使用的是多级渐进纹理过滤。接下来我们通过调用 generateMipMap() 来生成多级渐进纹理,接着通过给 gl.TEXTURE_2D 绑定值 null 来告诉 WebGL 我们对当前纹理的操作已经结束了。

2.非2的幂纹理

一般来说,宽和高都是2的幂的纹理使用是最理想的。
当使用到第三方的资源时,一般来说最好的方式就是先使用HTML5的画布把图片修正成2的幂然后再放到WebGL中进行渲染使用
但是,如果你一定要使用非2的幂纹理的话,WebGL也有原生支持,不过这些支持是受限的。
这种非2的幂纹理不能用来生成多级渐进纹理,而且不能使用纹理重复(重复纹理寻址等)。

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

1
2
3
4
5
6
// 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的幂纹理使用都会失败然后返回一张纯黑图片。

3.映射纹理到面

我们还要创建好纹理坐标到立方体各个面的顶点的映射关系。下面的代码通过替换之前的设置每个面颜色的代码,当然还是在 initBuffers() 函数里。

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
cubeVerticesTextureCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVerticesTextureCoordBuffer);

var 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);

首先,代码创建了一个GL缓存区,用来保存每个面的纹理坐标信息,然后把这个缓存区绑定到GL以方便我们写入数据。

数组变量 textureCoordinates 中定义好了与每个面上的每个顶点一一对应的纹理坐标。请注意,纹理坐标的取值范围只能从0.0到1.0,所以不论纹理贴图的实际大小是多少,为了实现纹理映射,我们要使用的纹理坐标只能规范化到0.0到1.0的范围内。
纹理坐标信息给定了之后,把这个数组里的数据都写到GL缓存区,这样GL就能使用这个坐标数据了。

4.更新着色器

1
2
3
textureCoordAttribute = gl.getAttribLocation(shaderProgram, "aTextureCoord");
gl.enableVertexAttribArray(textureCoordAttribute);
gl.vertexAttribPointer(texCoordAttribute, 2, gl.FLOAT, false, 0, 0);

这段代码中我们使用包含纹理坐标信息的属性替换之前使用的顶点颜色属性。

顶点着色器

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

uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

varying highp vec2 vTextureCoord;

void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
vTextureCoord = aTextureCoord;
}
</script>

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

#### 片段着色器

1
2
3
4
5
6
7
8
9
<script id="shader-fs" type="x-shader/x-fragment">
varying highp vec2 vTextureCoord;

uniform sampler2D uSampler;

void main(void) {
gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
}
</script>

片段的颜色是通过采样器使用最好的映射方式从纹理中的每一个像素计算出来的。

5.绘制具体纹理贴图的立方体

接下来是对 drawScene() 函数的更改,为了使整体的代码看起来更简洁,我们去掉了让立方体位置变化的代码,现在它只会随着时间的变化进行旋转,而为了使用纹理,所要进行的代码更改确是很少的。

1
2
3
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, cubeTexture);
gl.uniform1i(gl.getUniformLocation(shaderProgram, "uSampler"), 0);

GL 最多可同时注册32张纹理;gl.TEXTURE0 是第一张。我们把我们之前加载的纹理绑定到了第一个寄存器,然后着色器程序里的采样器 uSampler 就会完成它的功能:使用纹理。

6.源码展示

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="sylvester.js" type="text/javascript"></script>
<script src="glUtils.js" type="text/javascript"></script>
<!-- Fragment shader program -->

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

uniform sampler2D uSampler;

void main(void) {
gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
}
</script>

<!-- Vertex shader program -->

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

uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

varying highp vec2 vTextureCoord;

void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
vTextureCoord = aTextureCoord;
}
</script>
<script type="text/javascript">
var canvas;
var gl;
var cubeVerticesBuffer;
var cubeVerticesTextureCoordBuffer;
var cubeVerticesIndexBuffer;
var cubeRotation = 0.0;
var lastCubeUpdateTime = 0;
var cubeImage;
var cubeTexture;
var mvMatrix;
var shaderProgram;
var vertexPositionAttribute;
var textureCoordAttribute;
var perspectiveMatrix;
function start(){
canvas = document.getElementById("glcanvas");
gl = initWebGL(canvas); // 初始化上下文
if(gl){
// 设置清除颜色为黑色,不透明
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clearDepth(1.0); // Clear everything-add
// 开启“深度测试”, Z-缓存
gl.enable(gl.DEPTH_TEST);
// 设置深度测试,近的物体遮挡远的物体
gl.depthFunc(gl.LEQUAL);
// 清除颜色和深度缓存 -- 这一步被搬到创建完顶点后,开始绘画之前去了
// gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);

// -----------------开始创建着色器----------
var fragmentShader = getShader(gl,"shader-fs");
var vertexShader = getShader(gl, "shader-vs");

// 创建着色器
shaderProgram = gl.createProgram();

// 关联着色器
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);

//链接着色器
gl.linkProgram(shaderProgram)

// 如果创建着色器失败
if(!gl.getProgramParameter(shaderProgram,gl.LINK_STATUS)){
alert("Unable to initialize the shader program: " + gl.getProgramInfoLog(shader));
}
// 设置待用的着色器
gl.useProgram(shaderProgram);
// 获取指向着色器参数的指针--开始
vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
gl.enableVertexAttribArray(vertexPositionAttribute);
// 获取指向着色器参数的指针--结束

// 添加颜色------开始
textureCoordAttribute = gl.getAttribLocation(shaderProgram, "aTextureCoord");
gl.enableVertexAttribArray(textureCoordAttribute);
// 添加颜色------结束
//-----------------着色器创建完成-------------------


// ----------------创建顶点对象Buffer---------------
var vertices = [
// 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
];
cubeVerticesBuffer = gl.createBuffer();
// 选择cubeVerticesBuffer作为一个顶点
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVerticesBuffer);
// 现在通过顶点的列表构建WebGL形状。我们通过JavaScript数组创建一个Float32Array,然后使用它来填补当前顶点缓冲。
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

// ----------------创建颜色对象Buffer---------------
var 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
];
cubeVerticesTextureCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVerticesTextureCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates), gl.STATIC_DRAW);

// ----------------创建索引对象Buffer---------------
// 每个三角形的顶点索引
var cubeVertexIndices = [
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
];
cubeVerticesIndexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVerticesIndexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeVertexIndices), gl.STATIC_DRAW);

initTextures();
// ----------------开始画图drawScene---------------
setInterval(function () {
// 开始绘画前先清除画布
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 创建相机透视角矩阵,角度,宽高比,对象所处距离范围
perspectiveMatrix = makePerspective(45, 640.0/480.0, 0.1, 100.0);
// loadIdentity + multMatrix 都是操作的Matrix,所以封装了起来
function loadIdentity() {
mvMatrix = Matrix.I(4);
}
loadIdentity()
// 现在将这个物体挪动到可视范围 --其实就是 mvTranslate([-0.0, 0.0, -6.0]); 只是给展开了
function multMatrix(m) {
mvMatrix = mvMatrix.x(m);
}
multMatrix(Matrix.Translation($V([-0.0, 0.0, -6.0])).ensure4x4());



// 2.添加动画------开始
// 恢复原始矩阵,避免绘制其它图形时也受到这次旋转的影响
var mvMatrixStack = [];
function mvPushMatrix(m) {
if (m) {
mvMatrixStack.push(m.dup());
mvMatrix = m.dup();
} else {
mvMatrixStack.push(mvMatrix.dup());
}
}
function mvPopMatrix() {
if (!mvMatrixStack.length) {
throw("Can't pop from an empty matrix stack.");
}

mvMatrix = mvMatrixStack.pop();
return mvMatrix;
}
function mvRotate(angle, v) {
var inRadians = angle * Math.PI / 180.0;
var m = Matrix.Rotation(inRadians, $V([v[0], v[1], v[2]])).ensure4x4();
multMatrix(m);
}
mvPushMatrix();
mvRotate(cubeRotation, [1, 0, 1]);


// ------------------关联数据格式和位置(必须在这里)-------------------
// 指定了渲染时索引值为 index 的顶点属性数组的数据格式和位置
// 顶点
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVerticesBuffer);
gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
// 纹理
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVerticesTextureCoordBuffer);
gl.vertexAttribPointer(textureCoordAttribute, 2, gl.FLOAT, false, 0, 0);

gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, cubeTexture);
gl.uniform1i(gl.getUniformLocation(shaderProgram, "uSampler"), 0);

// 索引
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVerticesIndexBuffer);

// 获取 GLSL程序中定义的各个变量的句柄,从而可以用 JavaScript中定义的数值来初始化这些变量
var pUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
gl.uniformMatrix4fv(pUniform, false, new Float32Array(perspectiveMatrix.flatten()));
var mvUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
gl.uniformMatrix4fv(mvUniform, false, new Float32Array(mvMatrix.flatten()));

// ---------------------绘制物---------------------------
gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);

// 这下边必须放在后边
mvPopMatrix();
var currentTime = Date.now();
if (lastCubeUpdateTime) {
var delta = currentTime - lastCubeUpdateTime;

cubeRotation += (30 * delta) / 1000.0;
}

lastCubeUpdateTime = currentTime;
// 2.添加动画------结束
}, 15);
}

}
function initTextures() {
cubeTexture = gl.createTexture();
cubeImage = new Image();
cubeImage.onload = function() { handleTextureLoaded(cubeImage, cubeTexture); }
cubeImage.src = "cubetexture.png";
}

function handleTextureLoaded(image, texture) {
console.log("handleTextureLoaded, image = " + image);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,
gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
gl.generateMipmap(gl.TEXTURE_2D);
gl.bindTexture(gl.TEXTURE_2D, null);
}
function initWebGL(canvas) {
window.gl = null;
try {
// 尝试获取标准上下文,如果失败,回退到试验性上下文
gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
}
catch(e) {}

// 如果没有GL上下文,马上放弃
if (!gl) {
alert("WebGL初始化失败,可能是因为您的浏览器不支持。");
gl = null;
}
return gl;
}
function getShader(gl, id) {
var shaderScript = document.getElementById(id);

// Didn't find an element with the specified ID; abort.

if (!shaderScript) {
return null;
}

// Walk through the source element's children, building the
// shader source string.

var theSource = "";
var currentChild = shaderScript.firstChild;

while(currentChild) {
if (currentChild.nodeType == 3) {
theSource += currentChild.textContent;
}

currentChild = currentChild.nextSibling;
}

// Now figure out what type of shader script we have,
// based on its MIME type.

var shader;

if (shaderScript.type == "x-shader/x-fragment") {
shader = gl.createShader(gl.FRAGMENT_SHADER);
} else if (shaderScript.type == "x-shader/x-vertex") {
shader = gl.createShader(gl.VERTEX_SHADER);
} else {
return null; // Unknown shader type
}

// Send the source to the shader object

gl.shaderSource(shader, theSource);

// 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));
return null;
}

return shader;
}

</script>
</head>

<body onload="start()">
<canvas id="glcanvas" width="640" height="480">
Your browser doesn't appear to support the HTML5 <code>&lt;canvas&gt;</code> element.
</canvas>
</body>

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

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

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

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

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

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

1.建立一个数组来存放立方体所有顶点所在面的法线
(注:译者调试后发现此处new WebGLFloatArray(…) 可能应该使用new Float32Array())

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
42
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);

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

1
2
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVerticesNormalBuffer);
gl.vertexAttribPointer(vertexNormalAttribute, 3, gl.FLOAT, false, 0, 0);

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

1
2
3
4
var normalMatrix = mvMatrix.inverse();
normalMatrix = normalMatrix.transpose();
var nUniform = gl.getUniformLocation(shaderProgram, "uNormalMatrix");
gl.uniformMatrix4fv(nUniform, false, new WebGLFloatArray(normalMatrix.flatten()));

3.更新着色器
顶点着色器

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
<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>

片段着色器

1
2
3
4
5
6
7
8
9
10
11
12
<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>

参考文献:https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL_API/Tutorial

3 Three.js

Three.js 隐藏了 WebGL 渲染中的底层细节。
Three.js简化了 WebGL API 的细节,将 3D场景表示为网格、材质、灯光(即图形开发者常用的各种对象类型)。
Three.js 非常强大。
Three.js 不仅仅是 WebGL的一个封装,它内置了许多可用于游戏开发、动画、演示、数据可视化、建模工具应用的非常有用的模型对象,还提供了用于特殊效果的后渲染机制。除了它本身的能力,Three.js还拥有丰富的示例,你可以在你的项目中使用这些示例的代码。
Three.js 非常易用。
Three.js 的 API基于友好和易学的理念设计。随库提供的许多示例能够帮助你更好地入门。
Three.js 运行速度很快。
Three.js 采用了 3D图形的最佳实践,既保证了高性能,又不因此牺牲可用性。
Three.js 非常稳定。
它包含完备的错误检查、异常和控制台警告,让开发者可以准确地跟踪程序问题。
Three.js 支持交互。
WebGL不具备原生的选中支持,这意味着你无法获取到鼠标经过了当前场景中的哪个物体。Three.js提供了对选中的支持,使得为应用添加交互变得更加简单。
Three.js 支持数学计算。
Three.js 包含强大易用的 3D数学运算对象,如矩阵、映射和向量。
Three.js 内置第三方文件格式支持。
你可以利用 Three.js提供的接口载入以文本格式存储的、用流行的建模工具导出的 3D 模型。Three.js也定义了自己专属的 JSON 和二进制 3D 模型文件格式。
Three.js 是面向对象的。
开发者基于设计优良的 JavaScript对象编写代码,而不是仅仅去调用一些函数。
Three.js 是可扩展的。
无论你希望为 Three.js添加一些特性,还是定义自己的个性化Three.js,都是非常简单的事。如果现有的数据格式支持无法满足你的需求,那么你可以为特定的数据格式编写相应的插件。
Three.js 包含 2D Canvas、SVG、CSS 的渲染引擎。
尽管 WebGL已经非常流行,但它并未被所有的浏览器支持,这表示对某些应用来说,WebGL 并不是最好的选择。幸好Three.js 还可以使用 2D Canvas 和 SVG元素来渲染大部分的内容。当运行环境不支持 3D Canvas的时候,这为你的代码提供了一个优雅的降级方案。Three.js仍然可以用来渲染和变换 CSS 元素,我们将在第 6章对此进行详细说明

three.js
中文文档

用法

6
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
42
43
44
45
46
47
48
var camera, scene, renderer;    // 相机, 场景, 渲染器
var geometry, material, mesh; // 几何, 材质, 网格
// 拿电影来类比的话,场景对应于整个布景空间,相机是拍摄镜头,渲染器用来把拍摄好的场景转换成胶卷(对于网页来讲,是电脑屏幕)。 场景和相机代表了3D观察空间和数据模型,渲染器则包含了WebGL绘图上下文和着色器。

init();
animate();

function init() {
// 创建远景相机(PerspectiveCamera)
// 第一个属性70设置的是视角
// 第二个属性设置的是相机拍摄面的长宽比(aspect ratio)。我们几乎总是会使用元素的宽除以高,否则会出现挤压变形。
// 接下来的2个属性是近裁剪面(near clipping plane) 和 远裁剪面(far clipping plane)。
camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.01, 10 );
camera.position.z = 1; // 为了避免相机和立方体发生空间重叠,我们把相机(camera)的位置移出来一些。

scene = new THREE.Scene();

//除了创建这个几何模型(geometry)外,我们还需要一个材料(material)来对其着色。
geometry = new THREE.BoxGeometry( 0.2, 0.2, 0.2 );
material = new THREE.MeshNormalMaterial();

// 网孔是用来承载几何模型的一个对象,还可以把材料应用到它上面,然后添加到场景中完成旋转动画。
mesh = new THREE.Mesh( geometry, material );
// 当我们调用 scene.add() 时,对象将被添加到原点处,即坐标点(0,0,0),这将导致相机和立方体发生空间重叠。
scene.add( mesh );

// 创建renderer实例。还支持一些其它渲染器,基本上只是用来回退处理那些不支持WebGL的旧式,在参数中设置
// var canvas = document.getElementById("webglcanvas");
// renderer = new THREE.WebGLRenderer( { canvas: canvas, antialias: true } );
renderer = new THREE.WebGLRenderer( { antialias: true } );
// 设置渲染空间的尺寸
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

}

function animate() {

requestAnimationFrame( animate );

// 添加一点动画,让它转动起来
mesh.rotation.x += 0.01;
mesh.rotation.y += 0.02;

// 创建一个循环,以每秒60次的频率来绘制场景
renderer.render( scene, camera );

}

3.3.1 创建渲染器

我们需要创建一个渲染器。
ree.js
使用插件式的渲染系统。我们可以使用不同的 API来渲染相同的场景,例如对同一个场景,我们既可以使用WebGL API 来渲染,也可以使用 2D Canvas API来渲染。
在这个示例中,我们创建了一个THREE.WebGLRenderer对象,并传入两个参数来对其进行初始化:
canvas,正如字面上的意思,代表 HTML 文件中的 元素;
antialias 标记,用于告知 Three.js 开启基于硬件的多重采样抗锯齿(multi-sample antialiasing,MSAA)策略。
抗锯齿选项用于使渲染的物体边缘平滑,避免呈现锯齿。
Three.js 使用这些参数来构建一个 WebGL绘图上下文,并将这个绘图上下文添加到渲染器对象中

渲染器的全部设置只需两行代码:

1
2
3
4
5
6
// 创建Three.js渲染器并将其添加到canvas中
renderer = new THREE.WebGLRenderer(
  { canvas: canvas, antialias: true }
);
// 设置视口尺寸
renderer.setSize(canvas.width, canvas.height);

3.3.2 创建场景

通过创建一个 THREE.Scene 对象来创建一个场景(scene)。这个场景是 Three.js图形层级结构的最顶层。它包含其他的全部图形对象
现在我们有了一个场景,我们将为这个场景添加两个对象:
一个相机(camera)和一个网格(mesh)。
相机定义了我们观察场景的位置:在这个示例中,我们让相机停留在它的默认位置,即场景坐标系统的原点
我们定义了一个 THREE.PerspectiveCamera类型的相机,并用四个参数来初始化它。
这四个参数分别是:
度的视野角、宽高比、最近平面和最远平面的位置坐标值
用于创建场景和添加相机的代码同样十分简洁

1
2
3
4
5
6
// 创建一个新的Three.js场景
scene = new THREE.Scene();
// 添加一个相机以便观察整个场景
camera = new THREE.PerspectiveCamera( 45,
  canvas.width / canvas.height, 1, 4000 );
scene.add(camera);

把网格添加到场景中.
一个网格包括一个几何形状(geometry)和一个材质(material)
CubeGeometry对象来创建一个形状
MeshBasicMaterial对象来创建材质

WebGL 提供了一系列纹理相关的API,并提供了重要的安全特性,例如在纹理使用中对跨域访问的限制。令人兴
的是,Three.js
提供了一个非常简单的 API,这个 API
纹理映射,又称纹理,是用来表示 3D 网格模型表面属性的位图
(图片就称为纹理)
可以用来加载纹理,并便利地将其和材质进行结合。

Three.js 提供了一个非常简单的 API,这个 API可以用来加载纹理,并便利地将其和材质进行结合。
我们调用THREE.ImageUtils.loadTexture()
来从图片文件加载纹理,并通过材质构造时传入的 map 参数将得到的纹理和材质结合起来:

1
2
3
4
5
6
// 创建一个纹理映射的立方体并将其添加到场景中
// 首先,创建纹理映射
var mapUrl = "../images/webgl-logo-256.jpg";
var map = THREE.ImageUtils.loadTexture(mapUrl);
// 其次,创建一个基础材质,传入纹理映射参数
var material = new THREE.MeshBasicMaterial({ map: map });

它将JPEG图片的像素点映射到立方体每个表面的正确位置上:
图片既没有被拉伸覆盖整个立方体,
每个面上的图片也没有颠倒或翻转

最后我们创建立方体的网格
到这里我们已经构建了几何形状、材质和纹理。
我们将它们整合到一个变量名为 cube 的THREE.Mesh对象上。

将其添加到场景,我们将立方体设置在相机后方八个单位长度的位置上
我们不必在面对那些繁琐的矩阵计算,只需简单地设置立方体的 position.z 属性。
同时我们将立方体向观看者方向倾斜了一个角度
以便可以看清立方体的顶面,这只需设置 立方体的 rotation.x 属性就可以做到。

1
2
3
4
5
6
// 将网格移动到与相机有一段距离的位置,并朝向观察者倾斜
cube.position.z = -8;
cube.rotation.x = Math.PI / 5;
cube.rotation.y = Math.PI / 5;
// 最后,将网格添加到场景中
scene.add( cube );

3.3.3 运行循环的实现

上一章用的setTimeout,这里我们使用requestAnimationFrame()
上一章我们的 draw()函数需要设置缓冲、设置渲染状态、清空视口、设置着色器和纹理,等等
Three.js,我们只需短短的一行代码:

1
renderer.render( scene, camera );

最后我们要展示的是将立方体旋转一个角度,以便能够更好地看清它的 3D 结构。
实现非常简单:只需将 rotation.y属性设置为新的旋转角度,Three.js
底层代码就会处理相关的矩阵运算,而不用我们自己动手去处理它。
在下一次运行循环执行的时候,render()函数会使用新的 y旋转值,从而立方体会持续旋转。
以下是 animate() 和render() 函数的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var duration = 5000; // ms
var currentTime = Date.now();
function animate() {
    var now = Date.now();
    var deltat = now - currentTime;
    currentTime = now;
    var fract = deltat / duration;
    var angle = Math.PI * 2 * fract;
    cube.rotation.y += angle;
}
function run() {
    requestAnimationFrame(function() { run(); });
        // 渲染场景
        renderer.render( scene, camera );
        // 为下一帧动画旋转立方体
        animate();
}

3.3.4 为场景添加光照

实时3D渲染中令人惊叹的一点是:你可以使用灯光来打造一个物体表面的明部和暗部。
在第 2章的例子中添加灯光。但为此我需要重写顶点和片段着色器,并编写大量的代码用于更新顶点缓冲数据,这显然并不值得;
Three.js,这简直不费吹灰之力就可以实现。

1
2
3
4
5
6
7
8
9
10
11
// 添加用于突出显示物体的定向光
var light = new THREE.DirectionalLight( 0xffffff, 1.5);
// 将灯光放置在场景外,指向原点
light.position.set(0, 0, 1);
scene.add( light );
// 创建一个带明暗的纹理映射立方体,并将其添加到场景中
// 首先,创建纹理映射
var mapUrl = "../images/webgl-logo-256.jpg";
var map = THREE.ImageUtils.loadTexture(mapUrl);
// 其次,创建一个Phong材质来展示明暗效果,传入纹理映射参数
var material = new THREE.MeshPhongMaterial({ map: map });

在这个示例中,我们使用了一个定向光(directional light)。
这是一束来自特定方向的平行光线。
Three.js用于构建定向光的语法(在我看来)有点违反直觉:
你需要定义灯光的位置,以及照射的位置(默认位置是原点,故而此处省略了这一步)
然后 Three.js通过用灯光位置减去照射位置来计算出定向光的方向
在我们的示例中,灯光从屏幕的 (0, 0, 1) 位置指向 (0, 0, 0),即直射位于原点位置的立方体。

为了看到灯光的效果,我们需要做一些事情。
我们用一个Phong 材质代替了上一个示例中使用的基础材质
在 Three.js中,一个物体如何被照亮,不但依赖于添加到场景中的灯光,同时依赖于它自身的材质类型
Phong材质类型实现了一个简单但看起来非常逼真的着色模型,简称Phong 着色法
它的性能非常优秀。我们现在可以看清立方体的边缘

Phong算法在当时是非常先进的,而如今也是许多渲染应用的一个标准渲染方式,尤其是在实时渲染中,以支持高效的真实明暗计算

源码

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
var renderer = null, 
scene = null,
camera = null,
cube = null;

var duration = 5000; // ms
var currentTime = Date.now();
function animate() {

var now = Date.now();
var deltat = now - currentTime;
currentTime = now;
var fract = deltat / duration; // 求出执行
var angle = Math.PI * 2 * fract; // 这里是弧度制得到的而不是圆周率π,所以2π是360度
cube.rotation.y += angle;
}

function run() {
requestAnimationFrame(function() { run(); });

// Render the scene
renderer.render( scene, camera );

// Spin the cube for next frame
animate();

}

$(document).ready(
function() {

var canvas = document.getElementById("webglcanvas");

// Create the Three.js renderer and attach it to our canvas
// 创建渲染器
renderer = new THREE.WebGLRenderer( { canvas: canvas, antialias: true } ); // 抗锯齿

// Set the viewport size
// 设置视口尺寸
renderer.setSize(canvas.width, canvas.height);

// Create a new Three.js scene
// 创建场景
scene = new THREE.Scene();

// Add a camera so we can view the scene
// 添加一个相机以便观察整个场景
camera = new THREE.PerspectiveCamera( 45, canvas.width / canvas.height, 1, 4000 );
scene.add(camera);

// Add a directional light to show off the object
// 添加定向光
var light = new THREE.DirectionalLight( 0xffffff, 1.5);

// Position the light out from the scene, pointing at the origin
light.position.set(0, 0, 1);
scene.add( light );

// Create a shaded, texture-mapped cube and add it to the scene
// First, create the texture map
// 添加纹理
var mapUrl = "../images/webgl-logo-256.jpg";
var map = THREE.ImageUtils.loadTexture(mapUrl);

// Now, create a Phong material to show shading; pass in the map
// Phong材质替换基础材质
var material = new THREE.MeshPhongMaterial({ map: map,
color: 0xffffff });

// Create the cube geometry
// 创建一个立方体
var geometry = new THREE.CubeGeometry(2, 2, 2);

// And put the geometry and material together into a mesh
// 将形状材质纹理整合到网格上
cube = new THREE.Mesh(geometry, material);

// Move the mesh back from the camera and tilt it toward the viewer
// 移动网格来初始化位置
cube.position.z = -8;
cube.rotation.x = Math.PI / 5;
cube.rotation.y = Math.PI / 5;

// Finally, add the mesh to our scene
// 将网格放到场景中
scene.add( cube );

// Run the run loop
// 定时刷新形成转动
run();
}
);

4.Three.js中的图形和渲染

4.1 几何图形和网格

Three.js的一大便利在于它大大节省了我们用于构建几何形状的时间。
提供了包括预置的几何形状(如立方体和圆柱)、路径绘制形状、2D挤出几何体,以及用户可扩展的基类,以供我们自由添加自定义的几何形状

4.1.1 预置的几何形状类型

提供了许多内置的常见几何形状。
包括简单的立体,例如立方体、球体和圆柱;
一些包含更复杂变量的形状,包括基础形状以及基于路径的形状、圆环和纽结;
渲染在3D空间中的 2D 形状,例如圆、矩形和圆环;
甚至还包括从2D文字计算转化而来的 3D 挤出文字形状。
Three.js同时支持绘制3D点和线。

体验 Three.js 预置的几何形状,请运行Three.js项目中的 examples/webgl_geometries.ht
网址访问:https://threejs.org/examples/

4.1.2 路径、形状和挤出

通过曲线来创建挤出几何体。
图展示了一个基于样条线的挤出形状,也可以访问
examples/webgl_geometry_extrude_shapes.html
或者
examples/webgl_geometry_extrude_splines.html
这个示例允许你选择各种样条函数来生成挤出形状,并提供了用于查看曲线形状的运动相机

样条与挤出的结合是用于构建有机形状的重要技术

Shape 类还可以用于创建 2D 平面形状或者基于这些形状的3D 挤出。
假设你有一个2D多边形数据的库(例如地理版图边缘或矢量艺术形状),
你可以非常简单地利用 Three.js 提供的 Path类将这些数据导入到 Three.js 环境中。
Path类同时提供了一些简单的路径绘制函数,例如 moveTo()lineTo()

为如果你有一个现成的 2D形状,那么你可以利用它来创建一个存在于 3D空间中的平面网格
它可以与其他 3D物体同样的方式(平移、旋转、缩放)进行变换;
它可以利用材质来进行绘制,并像场景中的其他物体一样被照亮和着色。
你还可以对它进行挤出,从而创建一个基于外轮廓的真正的 3D 形状。
examples/webgl_geometry_shapes.html 中的演示

我们可以看到加利福尼亚州的轮廓、
一些简单的多边形,以及特殊形状,如心形和笑脸。
这些形状分别以多种方式绘制,包括平面 2D网格、挤出并添加了切角的 3D网格,以及线条——所有这些都是由基于路径的数据派生而来.

4.1.3 几何形状基础类

Three.js 预置的几何体类型派生自 THREE.Geometry(src/core/Geometry.js)基类。

先来看一下预置几何形状的源代
几何形状类的代码存放在 Three.js 工程目录下的

1
src/extras/geometries/

我们来浏览一下其中一个比较简单的几何物体:
THREE.CircleGeometry圆形几何体代码

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
/**
 * @author hughes
 */
THREE.CircleGeometry = function ( radius, segments, thetaStart, thetaLength
) {
    THREE.Geometry.call( this );
    radius = radius || 50;
    thetaStart = thetaStart !== undefined ? thetaStart : 0;
    thetaLength = thetaLength !== undefined ? thetaLength : Math.PI * 2;
    segments = segments !== undefined ? Math.max( 3, segments ) : 8;
    var i, uvs = [],
    center = new THREE.Vector3(), centerUV = new THREE.Vector2( 0.5, 0.5 );
    this.vertices.push(center);
    uvs.push( centerUV );
    for ( i = 0; i <= segments; i ++ ) {
        var vertex = new THREE.Vector3();
        var segment = thetaStart + i / segments * thetaLength;
vertex.x = radius * Math.cos( segment );
        vertex.y = radius * Math.sin( segment );
        this.vertices.push( vertex );
        uvs.push( new THREE.Vector2( ( vertex.x / radius + 1 ) / 2,
            ( vertex.y / radius + 1 ) / 2 ) );
    }
    var n = new THREE.Vector3( 0, 0, 1 );
    for ( i = 1; i <= segments; i ++ ) {
        var v1 = i;
        var v2 = i + 1 ;
        var v3 = 0;
        this.faces.push( new THREE.Face3( v1, v2, v3, [ n, n, n ] ) );
        this.faceVertexUvs[ 0 ].push( [ uvs[ i ], uvs[ i + 1 ], centerUV ] );
    }
    this.computeCentroids();
    this.computeFaceNormals();
    this.boundingSphere = new THREE.Sphere( new THREE.Vector3(), radius
);
};
THREE.CircleGeometry.prototype = Object.create( THREE.Geometry.prototype
);

THREE.CircleGeometry 的构造函数在 xy 平面上生成了一个平面圆形;这表示该形状中的所有的 z值都被设成0
整个算法的核心是用于计算这样一个形状的顶点数据的代码,这段代码包含在第一个 for 循环中:

1
2
vertex.x = radius * Math.cos( segment );
vertex.y = radius * Math.sin( segment );

事实上,这个 3D圆形是由围绕中心旋转的多扇三角形构建而成的。三角形的数量越多,构造出来的圆形就更接近于光滑的圆周。

最后,构造函数为物体初始化一个包围盒,在这个示例中,包围盒是一个球体,包围盒在拾取、选中以及图形渲染优化中起到重要作用

4.1.4 用于优化网格渲染的BufferGeometry

THREE.BufferGeometry
将数据以类型数组的方式存储,这节省了用于处理 JavaScript数字类型数组的额外开销。
这个类也可以很方便地用于场景背景和支柱等顶点值不会发生改变并且不会在场景中移动的固定物体上。

4.1.5 从建模软件包中导入网格数据

多数时候,我们的应用并不直接使用编程方式来构造几何形状,而是直接在程序中载入用专业的建模软件(例如 3dsMax、Maya 和 Blender)所构建的 3D 模型。

Three.js提供了一系列用于转换和加载模型文件的静态函数
下面我们来观看一个加载网格的示例,包括其几何形状和材质。

1
examples/webgl_loader_obj_mtl.html

从 Wavefront OBJ 格式文件中加载的网格数据描绘的男性形象通过 Wavefront OBJ 格式(文件后缀.OBJ)的文件导入。
OBJ格式文件简单而有限,它仅仅包含几何形状的数据:顶点、法向量和纹理坐标。
Wavefront开发了另一种用于存储材质数据的配套文件格式MTL,用于将材质和 OBJ 格式的模型数据进行关联
Three.js 用于 OBJ 格式(包含材质)的加载器代码位于
examples/js/loaders/OBJMTLLoader.js。
稍微研究一下它的工作机制,你就会发现,Three.js的文件加载器使用预置的 geometry 类和 shape 类将载入的OBJ 格式文件转换为 Three.js 中的 THREE.Geometry
几何形状对象,MTL 格式转换器将 MTL 格式的文件转换为Three.js可以识别的材质格式。
生成的几何形状对象和材质对象最终被整合到一个THREE.Mesh 对象中,并添加到场景
Three.js为多种文件格式提供了加载器示例。
其中某些格式只提供了对几何形状和材质的支持,但另一些可能更为复杂:包括整个场景、相机、灯光和动画。
我们将在第 8章对这些格式(以及用于编辑它们的工具)进行详细说明,第8 章主要讲述创建内容的各种途径。
Three.js提供的多数文件加载器并不包含在核心的库代码中,而是包含在示例文件中

4.2 场景图和空间变换的层级结构

WebGL 并没有原生的 3D场景概念,它仅仅提供了将图像绘制到画布上的基础API。
场景的结构通常由应用自行提供。Three.js 基于成熟的场景图(scene graph)概念定义了一个结构化场景的模型。
场景图代表一个以父子关系层级结构存储的 3D 物体集合,场景图的根节点通常用 root 变量来表示。应用从根节点开始递归地渲染场景的每个子层级

4.2.1 利用场景图来管理复杂场景

场景图同时允许这些物体按需被当作分离的组件或完整的组合来处理。
它同时提供了一项被称为变换层级(transform hierarchy)的重要能力,在这个体系中,个物体的子物体继承了父层级的变换信息(平移、旋转、缩放)。

将轮子设置为小汽车主体的子元素,你可以在你的代码中动态地控制小汽车沿着轨迹运动,而轮子也将跟随小汽车主体在 3D空间中沿同样轨迹运动;
你无需另行为轮子编写沿轨迹运动的动画,只需设定它们的旋转动画。

“图”这个词的使用,对于 Three.js的场景图来说并不十分精确。在 3D渲染中,场景图通常被表示为一个有向无环图
在这种数据结构中,节点以父子关系存储,任何一个物体都可能同时拥有多个父级元素。在 Three.js的场景图中,一个物体只能同时拥有一个父级元素

尽管出于技术术语统一的考虑,Three.js的层级结构被称为一个“图”,但从更准确的角度来说,这是一个树结构

4.2.2 Three.js中的场景图

Three.js 中最基本的对象类型是 THREE.Object3D,src/core/Object3D.js文件。
它是所有可见物体(如网格、线条、粒子系统)的基类,同时也是场景图层级结构组织的基本要素
每个 Object3D 对象都包含它自身的变换信息,这些信息分别存储在 position(位置,即平移)、rotationscale属性中。
通过设置这些属性,你可以移动、旋转和缩放相应的物体
。如果这个物体包含后代(子元素以及子元素的子元素),后代也将继承这些变换。
如果后代同时具备自己的变换,那么这个变换将与父级元素的变换叠加生效,变换通过整个层级结构层层传递

一个非常简单的变换层级:cube 是 cubeGroup的直接后代;
sphereGroup 也是 cubeGroup的直接后代(也就是说,它是 cube 的一个兄弟元素);
而sphere 和 cone 都是 sphereGroup 的后代。
通过加载示例文件 Chapter4/threejsscene.html来运行这个示例。你
会看到图中的立方体(cube)、球体(sphere)和圆锥(cone)分别在旋转。

展示了使用变换层级结构来创建和操作场景图的相关代码。
重要的代码行用粗体标明。
首先,我们通过创建一个 Object3D 对象 cubeGroup来初始化整个场景。这个对象将作为整个场景图的根。
随后我们将立方体(cube)网格以及另一个 Object3D 对象 sphereGroup 添加到 cubeGroup对象中。
球体(sphere)和圆锥(cone)被添加到 sphereGroup对象中。
同时我们将圆锥(cone)稍微往上挪了一些 —— 通过设置它的 position 属性。

一个包含变换层级的场景:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
function animate() {
        var now = Date.now();
        var deltat = now - currentTime;
        currentTime = now;
        var fract = deltat / duration;
        var angle = Math.PI * 2 * fract;
        // 绕y轴旋转立方体
        cube.rotation.y += angle;
        // 围绕其坐标y轴旋转球体分组
        sphereGroup.rotation.y -= angle / 2;
        // 围绕其坐标x轴旋转圆锥(向前翻滚)
        cone.rotation.x += angle;
    }
        function createScene(canvas) {
        // 创建Three.js渲染器并将其添加到canvas中
        renderer = new THREE.WebGLRenderer( { canvas: canvas, antialias:
true } );
        // 设置视口尺寸
        renderer.setSize(canvas.width, canvas.height);
        // 创建一个Three.js场景
        scene = new THREE.Scene();
        // 添加一个相机以便观察整个场景
        camera = new THREE.PerspectiveCamera( 45, canvas.width /
canvas.height,
            1, 4000 );
        camera.position.z = 10;
        scene.add(camera);
        // 创建一个用于容纳所有物体的分组
        cubeGroup = new THREE.Object3D;
        // 添加一个相机以便观察整个场景
        var light = new THREE.DirectionalLight( 0xffffff, 1.5);
        // 将灯光放置在场景外,指向原点
        light.position.set(.5, .2, 1);
        cubeGroup.add(light);
        // 为立方体创建一个带纹理的Phong材质
        // 首先,创建纹理贴图
        var mapUrl = "../images/ash_uvgrid01.jpg";
        var map = THREE.ImageUtils.loadTexture(mapUrl);
        var material = new THREE.MeshPhongMaterial({ map: map });
        // 创建立方体几何形状
        var geometry = new THREE.CubeGeometry(2, 2, 2);
        // 其次,将材质和几何形状整合到一个网格中
        cube = new THREE.Mesh(geometry, material);
        // 将网格向观察者倾斜
        cube.rotation.x = Math.PI / 5;
        cube.rotation.y = Math.PI / 5;
        // 将立方体网格添加到分组中
        cubeGroup.add( cube );
        // 为球体创建一个分组
        sphereGroup = new THREE.Object3D;
        cubeGroup.add(sphereGroup);
        // 将球体分组移动到立方体的后上方
        sphereGroup.position.set(0, 3, -4);
        // 创建球体几何形状
        geometry = new THREE.SphereGeometry(1, 20, 20);
        // 将材质和几何形状整合到一个网格中
        sphere = new THREE.Mesh(geometry, material);
        // 将球体网格添加到分组中
        sphereGroup.add( sphere );
        // 创建圆锥几何形状
        geometry = new THREE.CylinderGeometry(0, .333, .444, 20, 5);
        // 接着将材质和几何形状整合到一个网格中
        cone = new THREE.Mesh(geometry, material);
        // 将圆锥移动到球体的上方,并和球体保持一定距离
        cone.position.set(1, 1, -.667);
        // 将圆锥网格添加到分组中
        sphereGroup.add( cone );
        // 现在将分组添加到场景中
        scene.add( cubeGroup );
    }
    function rotateScene(deltax)
    {
        cubeGroup.rotation.y += deltax / 100;
        $("#rotation").html("rotation: 0," + cubeGroup.rotation.y.toFixed(2) + ",0");
    }
    function scaleScene(scale)
    {
        cubeGroup.scale.set(scale, scale, scale);
        $("#scale").html("scale: " + scale);
    }

4.2.3 平移、旋转和缩放的表示

(0,Math.PI / 2, 0) 表示物体围绕自身的 y 轴旋转了 90度。
我想,Mr.doob之所以采用欧拉角来作为默认的表示方式,是由于这种方式非常直观并且便于处理;
同时支持使用四元数来表示旋转。
在 Three.js 内部,每个 Object3D都持有一个用于存放各种变换信息的矩阵
拥有多重祖先元素的物体递归地用祖先元素的变换矩阵来乘自身的变换矩阵,从而得到自身最终的变换结果;
也就是说,Three.js在每次场景渲染的时候都遍历整个场景图(树结构)的叶节点来计算每个物体的变换矩阵。
Object3D对象定义了一个 matrixAutoUpdate 属性,当这个属性被设为false的时候,上述的行为(自动继承父级元素变换)被禁止
尽管如此,这个特性很有可能会引发一些奇怪的bug(“为什么我的动画不刷新了?”),所以请谨慎使用它

4.3 材质

如颜色、着色方法以及纹理(位图)。如果使用底层的WebGL API 来构建这些属性,我们需要自行编写 GLSL着色器代码,这需要相对高级的编程技巧,就算你需要创建的仅仅是最简单的视觉效果。
所幸 Three.js 预先为我们准备好了 GLSL 代码,并封装在材质(material)对象中。

4.3.1 标准网格材质

材质是一个定义了 3D 网格、点或者线元外观属性的对象,这些外观属性包括颜色(color)、透明度(transparency)以及发光(shininess)。
材质还包含(也可以不包含)纹理贴图,是一个覆盖物体表面的位图。
材质属性结合网格的顶点数据、场景的光照信息,可能还包括相机位置信息和其他全局属性,来决定每个物体的最终渲染效果

Three.js 提供了预置类 MeshBasicMaterialMeshPhongMaterialMeshLambertMaterial 来支持常用的材质类型
查看 Three.js 下的 src/materials 目录,以了解最新的材质集合。)
这些材质类型使用三种著名的材质技术来实现。

unlit(又称 prelit)

当使用这种材质类型时,仅纹理、
颜色和透明度属性会被用于物体的外观渲染。
场景的光照对物体的外观会产生影响。
这是用于扁平风格渲染效果或者绘制无需复杂着色效果的简
单几何形状的绝佳材质类型。
这种材质对已经将光照信息和材质结合计算并预先输出到纹
理中(例如使用 3D
建模工具创建并使用了烘培贴图)的情况同样适用,
因为此时物体的外观效果无需由渲染器再次进行计算。

Phong 着色法

这种材质提供了一个简单而优秀的仿真风格的高性能着色模
型。它已经成为迅速而简便地实现明暗视觉效果的常规方法
,同时许多游戏和应用也还在使用这种着色方法。用 Phong
着色法渲染的物体会在光线直接照射的地方显示高光区域(
镜面反射),物体表面的亮度随各点到光源的距离产生衰减
,而背光区域会被渲染成完全黑暗的效果。

朗伯反射(Lambertian reflectance)

在 Lambert
着色法中,物体外观的明暗不随观察者视角的改变而改变。
它非常适合于用来表现云朵等会漫反射大部分光线的物体,
或者像月球这类具有高反射率(表面反射光非常明亮)
的卫星。

运行本书示例中的 Chapter4/threejsmaterials.html,你可以更直观地感受!
展示了一个带月球表面纹理贴图的、被照亮的球体。

4.3.2 使用多重纹理增添逼真效果

凹凸贴图

用于替代物体表面的法向量来为表面提供凹凸的视觉效果。
一个值为 0 的像素点代表没有进行任何偏移
而一个非零的值可以用来表示相对物体表面的正向偏移
单通道的黑白位图可用于节省性能开销,而完整的、可以存储更多数值信息的 RGB 通道位图则可以用来表示更多的细节。
使用位图来代替 3D向量的原因是位图更紧凑,并且为着色器内部代码提供了一个用于计算正位移的更高效的方法。
打开示例文件 example Chapter4/threejsbumpmap.html,实际感受一下凹凸贴图的效果
打开 / 关闭月球的纹理,并改变漫反射和镜面反射颜色来观察不同的效果。

在 Three.js 中使用凹凸贴图非常简单。
只需在THREE.MeshPhongMaterial 构造器初始化的时候将传入参数的bumpMap 属性设置为一个有效纹理即可:

1
material= new THREE.MeshPhongMaterial({map: map,bumpMap: bumpMap });

法向量贴图

提供了一个能够比凹凸贴图展现更多细节的方式,无需使用更多的多边形
相比凹凸贴图,法向量贴图的体积更大,对计算性能的要求也更高。
法向量贴图的工作原理是将实际的顶点法向量信息编码到 RGB
格式位图的数据中,这通常会比相应的网格顶点数据有更高的分辨率。
着色器将法向量信息和光照计算(包括综合处理当前的相机和光源信息)相结合最终渲染出物体的外观细节。
打开示例文件 Chapter4/threejsnormalmap.html,查看法向量贴图的实际效果

注意地球模型的海拔信息轮廓

在 Three.js 中使用法向量贴图同样十分简单。
只需在构造THREE.MeshPhongMaterial对象的时候,将一个已有的纹理作为normalMap属性配置传入即可:

1
Material = new THREE.MeshPhongMaterial({ map: map,normalMap: normalMap });

环境贴图

为使用其他纹理来提升真实度提供了另一种途径。
环境贴图模拟了物体对周围环境的反射,而不是像凹凸贴图和纹理贴图那样旨在为物体表面添加更多的真实细节

打开 Chapter 4/threejsenvmap.html 查看环境贴图的示例
在内容区域拖拽鼠标来旋转场景,或者使用鼠标滚轮对场景进行放大缩小。
注意球表面的图像呈现为它周围天空背景的贴图(如图4-11)。事实上,它并没有真正进行这样的处理
整个场景的背景由一个立方体的内侧表面构成

而球体只是简单地将用于场景背景的纹理像素渲染到自身的表面上。诀窍在于,用于球体表面的材质是一个立方体纹理(cubetexture)

材质:这是一个由六个不同的位图组成的、可以在立方体内部形成一个连续图像效果的纹理贴图。
这个特殊的立方体材质被用于构建一个天空全景的背景。
查看images/cubemap/skybox/
文件中的不同文件,了解这个场景是如何被构建出来的。
由于采用了立方体纹理,这种类型的环境贴图被称为立方体环境映射(cubic environment mapping)。

在 Three.js中使用立方体纹理比使用凹凸贴图或法向量贴图要稍微复杂一些,首先,我们需要创建一个立方体纹理,而非普通的纹理。
我们需要使用 Three.js 的全局方法ImageUtils.loadTextureCube(),通过传入六个不同图像的 URL 来创建这个纹理。

然后,我们在创建 MeshPhongMaterial 对象的时候将 envMap参数设置为该纹理对象。
我们还指定了反射率reflectivity的值,用来定义立方体纹理在物体渲染时被物体表面材质“反射”的程度

1
2
3
4
5
6
7
8
9
ar path = "../images/cubemap/skybox/";
var urls = [ path + "px.jpg", path + "nx.jpg",
             path + "py.jpg", path + "ny.jpg",
             path + "pz.jpg", path + "nz.jpg" ];
envMap = THREE.ImageUtils.loadTextureCube( urls );
materials["phong-envmapped"] = new THREE.MeshBasicMaterial(
   { color: 0xffffff,
     envMap : envMap,
     reflectivity:1.3} );

为了使它贴近于真实的效果,我们还需要做一些事情。
我们需要将反射位图和周围环境进行绑定
为此我们创建了一个天空盒(skybox),这是一个内面贴图的背景立方体,使用了同样的位图图像,用于构建一个天空全景。
这本身需要非常繁杂的工作,不过所幸 Three.js提供了内建的辅助函数来帮助我们完成这件事情.

除了内置的标准材质 Basic、Phong 和 Lambert 以外,Three.js还提供了一个着色器的库,包含在全局变量 THREE.ShaderLib中。

我们使用立方体(cube geometry)来创建一个网格,并使用库中预先定义的“cube”着色器来作为立方体的材质。
它使用我们刚才用于环境贴图的纹理,来渲染立方体的内部.

1
2
3
4
5
6
7
8
9
10
11
12
// 创建天空盒
var shader = THREE.ShaderLib[ "cube" ];
shader.uniforms[ "tCube" ].value = envMap;
var material = new THREE.ShaderMaterial( {
    fragmentShader: shader.fragmentShader,
    vertexShader: shader.vertexShader,
    uniforms: shader.uniforms,
    side: THREE.BackSide
} ),
mesh = new THREE.Mesh(new THREE.CubeGeometry( 500, 500, 500 ),
material);
scene.add( mesh );

4.4 光源

Three.js定义了多种内置的光源类型。
最常用的光源类型有定向光(directional light)、点光源(point light)、聚光灯(spotlight)和环境光(ambient light)。

定向光

实现了一类产生定向平行光的光源。这类光源没有位置信息
,只有方向、颜色和强度信息。(事实上,在 Three.js
里面,定向光是包含一个位置信息的,
不过它仅仅被用来结合另一个向量——目标位置——
来计算光线的方向。
这显然是一个并不高明并违反直觉的处理方式,我希望
Mr.doob 后续能够修复它。)

点光源

包含位置信息,但不包含方向信息。

聚光灯

同时具备位置和方向信息。
同时它们提供了用于定义聚光灯内圆锥和外圆锥尺寸(角度)的参数,以及定义它们能够照亮多远距离的参数。

环境光

没有位置,也没有方向。它均匀地投射于整个场景
所有的 Three.js 光源类型都支持通用的 intensity属性,它定义了光源的强度、颜色和 RGB 值

光源自身并没有处理对场景的影响,它们的值和特定材质的属性结合起来,用来定义物体最终的视觉展示效果。
MeshPhongMaterialMeshLambertMaterial 定义了以下属性。
color
又称漫反射颜色(diffuse color),它定义了物体是如何反射来自一个方向上的光的(
例如定向光、点光源和聚光灯)。
ambient
物体对环境光的反射程度。
emissive
这个材质属性定义了一个物体自身发光的颜色,和整个场景的光源无关。
MeshPhongMaterial 材质同时支持一个镜面反射(specular)颜色属性,这个属性和场景灯光相结合,创建出物体朝向光源的顶点的高光效果。

MeshBasicMaterial 材质会忽略所有灯光的影响
打开 Chapter 4/threejslights.html
运行这个示例。这个场景包含四个不同类型的灯光、黑白纹理的背景平面和三个纯白的几何体,用来展示不同灯光的效果。
你可以通过页面上的拾色器控件动态地改变每个灯光的颜色

下面这段代码展示了灯光的设置
蓝色的点光源从后方照亮物体模型;
注意物体后方地板上的蓝色区域。
通过定义spotLight.target.position属性,绿色的聚光灯的锥体笼罩了靠近场景前方的地板区域

1
2
3
4
5
6
7
8
9
10
11
12
// 创建并将所有灯光添加到场景中
directionalLight.position.set(.5, 0, 3);
root.add(directionalLight);
pointLight = new THREE.PointLight (0x0000ff, 1, 20);
pointLight.position.set(-5, 2, -10);
root.add(pointLight);
spotLight = new THREE.SpotLight (0x00ff00);
spotLight.position.set(2, 2, 5);
spotLight.target.position.set(2, 0, 4);
root.add(spotLight);
ambientLight = new THREE.AmbientLight ( 0x888888 );
root.add(ambientLight);

4.5 阴影

Chapter4/threejsshadows.html中演示了如何在场景中增加实时阴影
Three.js 使用一项名为阴影贴图(shadow mapping)的技术来支持阴影效果。
渲染器会为阴影贴图保存一张额外的纹理,
渲染器会将阴影区域渲染到这个纹理中,
并在片段着色器中将其用于合成最终的图像。
故而,在Three.js 中开启阴影需要以下步骤。
(1) 在渲染器中开启阴影贴图。
(2) 在灯光中开启阴影并设置相关参数。THREE.DirectionalLightTHREE.SpotLight 两种类型都支持阴影。
(3) 设置哪些物体会产生和接受阴影

代码展示了在 createScene() 函数中添加的用于渲染阴影的代码。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
var SHADOW_MAP_WIDTH = 2048, SHADOW_MAP_HEIGHT = 2048;
function createScene(canvas) {
    // 创建Three.js渲染器并将其添加到canvas中
    renderer = new THREE.WebGLRenderer( { canvas: canvas, antialias: true
} );
    // 设置视口尺寸
    renderer.setSize(canvas.width, canvas.height);
    // 开启阴影
    renderer.shadowMapEnabled = true;
    renderer.shadowMapType = THREE.PCFSoftShadowMap;
    // 创建Three.js场景
    scene = new THREE.Scene();
    // 添加一个相机以便观察整个场景
    camera = new THREE.PerspectiveCamera( 45, canvas.width /
canvas.height,
        1, 4000 );
    camera.position.set(-2, 6, 12);
    scene.add(camera);
    // 创建一个用于容纳所有物体的分组
    root = new THREE.Object3D;
    // 添加一个相机以便观察整个场景
    directionalLight = new THREE.DirectionalLight( 0xffffff, 1);
    // 创建并将所有灯光添加到场景中
    directionalLight.position.set(.5, 0, 3);
    root.add(directionalLight);
    spotLight = new THREE.SpotLight (0xffffff);
    spotLight.position.set(2, 8, 15);
    spotLight.target.position.set(-2, 0, -2);
    root.add(spotLight);
    spotLight.castShadow = true;
    spotLight.shadowCameraNear = 1;
    spotLight.shadowCameraFar = 200;
    spotLight.shadowCameraFov = 45;
    spotLight.shadowDarkness = 0.5;
    spotLight.shadowMapWidth = SHADOW_MAP_WIDTH;
    spotLight.shadowMapHeight = SHADOW_MAP_HEIGHT;
    ambientLight = new THREE.AmbientLight ( 0x888888 );
    root.add(ambientLight);
    // 创建一个用于容纳球体的组
    group = new THREE.Object3D;
    root.add(group);
    // 创建一个纹理贴图
    var map = THREE.ImageUtils.loadTexture(mapUrl);
    map.wrapS = map.wrapT = THREE.RepeatWrapping;
    map.repeat.set(8, 8);
    var color = 0xffffff;
    var ambient = 0x888888;
    // 添加一个作为平面的地面,以便更好地观察灯光
    geometry = new THREE.PlaneGeometry(200, 200, 50, 50);
    var mesh = new THREE.Mesh(geometry, new
THREE.MeshPhongMaterial({color:color,
        ambient:ambient, map:map, side:THREE.DoubleSide}));
    mesh.rotation.x = -Math.PI / 2;
    mesh.position.y = -4.02;
    // 将网格添加到分组中
    group.add( mesh );
    mesh.castShadow = false;
    mesh.receiveShadow = true;
    // 创建立方体几何形状
    geometry = new THREE.CubeGeometry(2, 2, 2);
    // 然后将几何形状和材质整合到网格中
    mesh = new THREE.Mesh(geometry, new
THREE.MeshPhongMaterial({color:color,
        ambient:ambient}));
    mesh.position.y = 3;
    mesh.castShadow = true;
    mesh.receiveShadow = false;
    // 将网格添加到分组中
    group.add( mesh );
    // 将网格持久化到变量中以便对其进行旋转操作
    cube = mesh;
    // 创建球体几何形状
    geometry = new THREE.SphereGeometry(Math.sqrt(2), 50, 50);
    // 然后将几何形状和材质整合到网格中
    mesh = new THREE.Mesh(geometry, new
THREE.MeshPhongMaterial({color:color,
        ambient:ambient}));
    mesh.position.y = 0;
    mesh.castShadow = true;
    mesh.receiveShadow = false;
    // 将网格添加到分组中
    group.add( mesh );
    // 创建圆锥几何形状
    geometry = new THREE.CylinderGeometry(1, 2, 2, 50, 10);
    // 然后将几何形状和材质整合到网格中
    mesh = new THREE.Mesh(geometry, new
THREE.MeshPhongMaterial({color:color,
        ambient:ambient}));
    mesh.position.y = -3;
    mesh.castShadow = true;
    mesh.receiveShadow = false;
    // 将网格添加到分组中
    group.add( mesh );
    // 现在将分组添加到场景中
    scene.add( root );
}

支持三种类型的阴影贴图算法:
基础(basic)、
PCF(percentage close filtering)和
PCF soft shadows。每种算法都比前一种更接近真实效果,但会导致复杂度的上升和性能的下降

通过从灯光位置引一条通过目标物体的射线的方式来渲染阴影。
从本质上说,它将聚光灯当成另一种类型的“相机”来处理。
所以我们需要设置一些类似相机设置的参数,包括近和远裁剪平面以及视野。
为阴影设置一个暗度值,Three.js 默认的 0.5 对这个应用来说很合适
随后,我们需要设置Three.js阴影贴图尺寸的大小属性。
阴影贴图是一个额外的位图,专门用于渲染阴影的暗区域并最终和每个物体最终的渲染图像混合。
我们将 SHADOW_MAP_WIDTH 和 SHADOW_MAP_HEIGHT 的值设为 2048,比 Three.js 默认的512要
这将提供非常平滑的阴影;这个值越小,产生的阴影就会呈现越多的锯齿
最后,我们需要告诉 Three.js 哪些对象产生和接收阴影。

我们希望将三个几何体的阴影到地板上,然后地板接收这些阴影,
所以,我们将地板的
mesh.castShadow 属性设置为 false,
mesh.receiveShadow 属性设置为 true;
将立方体、球体、圆锥的
mesh.castShadow 属性设置为 true,
mesh.receiveShadow 属性设置为 false。

最后的最后,我们希望阴影的强度随着聚光灯的光线强度变化而变化.
阴影贴图在渲染的时候并不会自动随光线强度变化而调整。
实际上,它依赖于灯光的 shadowDarkness 属性。
因此当灯光的颜色被用户改变的时候,我们需要手动去更新 shadowDarkness的值。
接下来的这段代码展示了 setShadowDarkness()函数的实现,
它基于光源 RGB 值的平均值来计算阴影的暗度。
当你将聚光灯的颜色调暗时,会发现阴影也随着变淡了

1
2
3
4
5
6
7
8
function setShadowDarkness(light, r, g, b)
{
    r /= 255;
    g /= 255;
    b /= 255;
    var avg = (r + g + b) / 3;
    light.shadowDarkness = avg * 0.5;
}

实时阴影是 WebGL可视化体验中的一项神奇的增强技术,而 Three.js 可以帮助我们更方便地实现它。
然而,它是有代价的。首先,阴影贴图是另一种类型的纹理贴图,它需要更多的显存
对于一个 2048×2048 的阴影贴图,我们需要额外的 4 MB 显存空间。

4.6 着色器

内置了一套强大的材质集合。它们都是基于库中预置的 GLSL着色器来实现的。
这些着色器可以用来支持常见的着色需求,如 unlit、Phong 和 Lambert。

举例来说,一个用于模拟风吹草动的着色器,可能需要支持调整草的高度和密度,以及调整风速和风向的属性
着色技术不再是艺术产品的尝试,而是成为了一个通用的程序问题

业界联合起来创建了一项可编程的管道技术,称为可编程着色器er),而不是尝试预测每种可能的材质属性组合,并包含在运行时引擎的代码中
着色器允许开发者使用类 C
语言编写实现顶点级和像素级的复杂效果,并编译成可以在GPU中运行的代码。
使用可编程着色器,开发者可以创建出高性能、高度逼真的视觉效果,从预置的材质和灯光模型的限制中解放出来

4.6.1 ShaderMaterial类:编写你自己的着色器代码

GL着色语言(GLSL)是为 OpenGL 和 OpenGL ES开发的一种着色器语言(WebGL API 的基础)。

如果我们的应用需要一些预置类型中没有提供的效果,Three.js 也允许我们使用 THREE.ShaderMaterial 来进行定制化的着色器来发。
examples/webgl_materials_shaders_fresnel.html
文件中,演示了一个 Fresnel 着色器。
Fresnel着色器用于模拟光线穿过水和玻璃等透明介质时产生的反射和折射效果。

Fresnel 着色器的 GLSL 源代码可以在
examples/js/shaders/FresnelShader.js
文件中找到

1
2
3
4
5
6
7
8
9
10
11
12
/**
 * @author alteredq / http://alteredqualia.com/
 * Based on Nvidia Cg tutorial
 */
THREE.FresnelShader = {
    uniforms: {
        "mRefractionRatio": { type: "f", value: 1.02 },
        "mFresnelBias": { type: "f", value: 0.1 },
        "mFresnelPower": { type: "f", value: 2.0 },
        "mFresnelScale": { type: "f", value: 1.0 },
        "tCube": { type: "t", value: null }
    },

uniforms 属性指定了 Three.js 在着色器被使用时会传给 WebGL的值。
回忆一下,着色器代码会对每个顶点和像素(片段)都执行一次。
正如字面上的意思那样,着色器中的 uniform 属性的值不会随着顶点的切换而改变
它们本质上是作用于全部顶点和像素的恒定全局变量
Fresnel着色器定义了用于控制反射率和折射率的 uniform 属性(例如mRefractionRatio 和 mFresnelScale)。

4.6.2 在Three.js中使用GLSL着色器代码

现在该来设置顶点和片段着色器了。
暂时省略···
创建定制化的着色器看起来需要很多工作,但这是值得的,
因为它输出了一个非常接近真实世界的光学模拟效果
Three.js帮忙完成了其他的机械工作——
更新每个物体的世界矩阵,
追踪相机,预定义许多 GLSL变量,
编译和链接着色器代码——
为我们省下了大量的开发和调试时间,
使得着色器的开发工作变得方便且吸引人。
有了这个框架,你可以轻松尝试编写你自己的着色器

4.7 渲染

这一章我们使用了 Three.js中的很多方法来不断提升真实度,从简单的几何形状到材质、纹理、灯光和阴影,最终使用 GLSL编写了我们自己的着色器。
每一步,我们都比上一步创造了更具真实感的图形,但我们还差最后一步:渲染。
Three.js 3D 场景控制的最终输出是渲染在浏览器 Canvas元素上的 2D 图像。

我们的最终目的都是绘制像素点。我们选择WebGL,只是因为它具有高性能.
即便使用了WebGL,我们在具体渲染图像的时候也有很多选择。
例如,API 允许我们选择是否使用深度缓冲(Z-buffering)渲染(这是一种通过在硬件中使用额外的存储空间来存储物体的深度信息,以实现只绘制场景中最前面像素的技术)。
这是可选的。如果我们没有启用深度缓冲,我们的应用则需要自己处理物体的排序,这可能需要深入到面片级别

结合高级的渲染技术,例如后处理、多道渲染以及延迟渲染,我们可以创建高度仿真的效果

4.7.1 后处理和多道渲染

有时候,一次渲染并不够。
一个场景经常需要通过多次渲染来创建高质量的、真实的图像。
这些独立的渲染最终在一个被称为多道渲染(multipass rendering)的流程中合成到一起,产生最终的图像。
许多多道渲染方法使用后处理(post-processing)技术,或者通过图像处理技术来提升图像质量。
examples/webgl_terrain_dynamic.html通过后处理编写。
单纯基于噪声程序生成的地形并不足以令人印象深刻,该场景特别使用了多道渲染工序,包括使用 bloom 着色法来强调阳光在晨雾中的漫射效果,以及使用高斯滤镜让场景产生轻微模糊来增强场景的宁静氛围。

Three.js 的后处理依赖于以下特性。

通过 THREE.WebGLRenderTarget 对象支持多重渲染目标(multiple render target)。
通过多重渲染目标,场景可以被多次渲染到离屏位图上,并合成到最终的图像中。
(源文件:src/renderers/WebGLRenderTarget.js。)
THREE.EffectComposer类实现了一个多道渲染循环。
这个对象包含一个或多个渲染工序(renderpass)对象,它们会被依次调用来进行场景渲染。
每一道工序都拥有访问整个场景以及上一道工序处理产生的图像数据的权限,从而进一步细化图像。
THREE.EffectComposer,以及使用它来实现多道渲染的示例,可以在 Three.js工程目录中的
examples/js/postprocessing/ 和
examples/js/shaders/
文件夹中找到。

4.7.2 延迟渲染

延迟渲染这种方法直到通过不同的来源计算出最终的图像结果,才把这个结果渲染到 WebGL canvas上。
延迟渲染在初始阶段使用了多个缓冲(就是纹理贴图)来收集着色器的计算结果数据,并在接下来的阶段中,使用初始阶段收集的结果来计算像素值,这种方式非常耗费存储空间和计算能力,但它也可以创造高度真实的效果,尤其是对于光线和阴影的处理

参考文献

「译」ThreeJS 入门教程

补充

通过核心Geometry创建正方形

所有形状的原理:
1.创建顶点位置数组
2.创建三角面数组

6
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
42
43
44
45
46
47
48
49
50
51
52
53
54
$(document).ready(
function() {

var canvas = document.getElementById("webglcanvas");
renderer = new THREE.WebGLRenderer( { canvas: canvas, antialias: true } );
// Set the viewport size
renderer.setSize(canvas.width, canvas.height);

// Create a new Three.js scene
scene = new THREE.Scene();

// Add a camera so we can view the scene
camera = new THREE.PerspectiveCamera( 45, canvas.width / canvas.height, 1, 4000 );
camera.position.z = 10;
scene.add(camera);

// 必须开灯光
var light = new THREE.DirectionalLight( 0xffffff, 1.5);
light.position.set(0, 0, 1);
scene.add( light );

// Now, create a Phong material to show shading; pass in the map
var material = new THREE.MeshPhongMaterial({
color: 0x2194ce });
// 顶点位置的数组
var vertices = [
new THREE.Vector3( 0, 1, 0 ),
new THREE.Vector3( 0, -1, 0 ),
new THREE.Vector3( 1, -1, 0 ),
new THREE.Vector3( 1, 1, 0 ),
];
// 三角面数组
var faces = [
new THREE.Face3(0, 1, 2),
new THREE.Face3(2, 3, 0),
];

var geom = new THREE.Geometry();
geom.vertices = vertices;
geom.faces = faces;
geom.computeFaceNormals(); //注意要加这个
// And put the geometry and material together into a mesh
cube = new THREE.Mesh(geom, material);

// Move the mesh back from the camera and tilt it toward the viewer
// cube.rotation.x = Math.PI / 2; // 不要旋转,否则看不到了

// Finally, add the mesh to our scene
scene.add( cube );

// Run the run loop
run();
}
);

ThreeJS 开发实例

其他方法

  1. THREE.Shape()构建形状
  2. THREE.ExtrudeGeometry()l拉伸

纹理贴图和UV贴图

Three.js进阶篇之9 - 纹理映射和UV映射

脑图

百度脑图内有总结

← Prev Next →