【WebGL之巅】(完结篇)30-层次模型-多节点模型

By yesmore on 2021-08-08
阅读时间 58 分钟
文章共 11.5k
阅读量

对应《WebGL编程指南》第九章 44-MultiJointModel、45-MultiJointModel_segment

要点:层次结构模型、多节点模型

知识点

一、多节点模型

这一节将把 JointModel 扩展为 MultiJointModel,后者绘制一个具有多个关节的完整的机器人手臂,包括基座 base,上臂 arm1,前臂 arm2,手掌 palm,两根手指 finger1 & finger2,全部可以通过键盘来控制。arm1 和 arm2 的连接关节 joint1 位于 arm1 顶部,arm2 和 palm 的连接关节 joint2 位于 arm2 顶部,finger1 和 finger2 位于 palm 一段,如下图所示:
MultiJointModel中的层次结构

实现功能:用户可以通过键盘操纵机器人手臂,arm1 和 arm2 的操作和 JointModel 一样,此外,还可以使用 x 和 z 键旋转 Joint2,使用 C 和 V 键旋转 finger1 和 finger2。控制这些小部件旋转角度的全局变量。

1.1 程序分析

对应代码:MultiJointModel

示例程序 MultiJointModelJointModel 相比,主要有两处不同:keydown()函数响应更多的键盘情况,draw()函数绘制各部件的逻辑更复杂了。

keydown()函数
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
var ANGLE_STEP = 3.0;    // 每次按键转动的角度
var g_arm1Angle = 90.0; // arm1的当前角度
var g_joint1Angle = 45.0; // joint1的当前角度
var g_joint2Angle = 0.0; // joint2
var g_joint3Angle = 0.0; // joint3

function keydown(ev, gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix) {
switch (ev.keyCode) {
case 40: // 上方向键 - joint1绕z轴正向转动
if (g_joint1Angle < 135.0) g_joint1Angle += ANGLE_STEP;
break;
case 38: // 下方向键 - joint1绕z轴负向转动
if (g_joint1Angle > -135.0) g_joint1Angle -= ANGLE_STEP;
break;
case 39: // 右方向键 - arm1绕y轴正向转动
g_arm1Angle = (g_arm1Angle + ANGLE_STEP) % 360;
break;
case 37: // 左方向键 - arm1绕y轴负向转动
g_arm1Angle = (g_arm1Angle - ANGLE_STEP) % 360;
break;
case 90: // 'z'key -> 使joint2正向旋转
g_joint2Angle = (g_joint2Angle + ANGLE_STEP) % 360;
break;
case 88: // 'x'key -> 使joint2负向旋转
g_joint2Angle = (g_joint2Angle - ANGLE_STEP) % 360;
break;
case 86: // 'v'key -> 使joint3正向旋转
if (g_joint3Angle < 60.0) g_joint3Angle = (g_joint3Angle + ANGLE_STEP) % 360;
break;
case 67: // 'c'key -> 使joint3负向旋转
if (g_joint3Angle > -60.0) g_joint3Angle = (g_joint3Angle - ANGLE_STEP) % 360;
break;
default: return; // Skip drawing at no effective action
}
// Draw the robot arm
draw(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix);
}

本例的 keydown()函数,除了需要在方向键被按下时做出响应,更新 g_arm1Angleg_joint1Angle 变量,还需要在Z键、X键、V键和C键被按下时做出响应,更新 g_joint2Angleg_joint3Angle 变量。在此之后,就调用 draw()函数,把整个模型画出来。

模型的各个部件 base、arm1、arm2、palm、finger1 和 finger2 等虽然都是立方体,但是长宽高各不相同。所以本例扩展了 drawBox()函数,添加了3个参数:

1
function drawBox(gl, n, width, height, depth, viewProjMatrix, u_MvpMatrix, u_NormalMatrix)

新增加的3个参数表示部件的宽度、高度和长度(深度),drawBox()会根据这3个参数,将部件分毫不差地绘制出来。

draw()函数—绘制模型部分
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
// 变换坐标的矩阵
var g_modelMatrix = new Matrix4(), g_mvpMatrix = new Matrix4();

function draw(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix) {
// 用以清空颜色缓冲区和深度缓冲区的背景颜色
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

// base绘制
var baseHeight = 2.0;
g_modelMatrix.setTranslate(0.0, -12.0, 0.0);
drawBox(gl, n, 10.0, baseHeight, 10.0, viewProjMatrix, u_MvpMatrix, u_NormalMatrix);

// Arm1
var arm1Length = 10.0;
g_modelMatrix.translate(0.0, baseHeight, 0.0); // Move onto the base
g_modelMatrix.rotate(g_arm1Angle, 0.0, 1.0, 0.0); // Rotate around the y-axis
drawBox(gl, n, 3.0, arm1Length, 3.0, viewProjMatrix, u_MvpMatrix, u_NormalMatrix); // Draw

// Arm2
var arm2Length = 10.0;
g_modelMatrix.translate(0.0, arm1Length, 0.0); // Move to joint1
g_modelMatrix.rotate(g_joint1Angle, 0.0, 0.0, 1.0); // Rotate around the z-axis
drawBox(gl, n, 4.0, arm2Length, 4.0, viewProjMatrix, u_MvpMatrix, u_NormalMatrix); // Draw

// A palm
var palmLength = 2.0;
g_modelMatrix.translate(0.0, arm2Length, 0.0); // Move to palm
g_modelMatrix.rotate(g_joint2Angle, 0.0, 1.0, 0.0); // Rotate around the y-axis
drawBox(gl, n, 2.0, palmLength, 6.0, viewProjMatrix, u_MvpMatrix, u_NormalMatrix); // Draw

// Move to the center of the tip of the palm
g_modelMatrix.translate(0.0, palmLength, 0.0);

// Draw finger1
pushMatrix(g_modelMatrix);
g_modelMatrix.translate(0.0, 0.0, 2.0);
g_modelMatrix.rotate(g_joint3Angle, 1.0, 0.0, 0.0); // Rotate around the x-axis
drawBox(gl, n, 1.0, 2.0, 1.0, viewProjMatrix, u_MvpMatrix, u_NormalMatrix);
g_modelMatrix = popMatrix();

// Draw finger2
g_modelMatrix.translate(0.0, 0.0, -2.0);
g_modelMatrix.rotate(-g_joint3Angle, 1.0, 0.0, 0.0); // Rotate around the x-axis
drawBox(gl, n, 1.0, 2.0, 1.0, viewProjMatrix, u_MvpMatrix, u_NormalMatrix);
}

draw()函数的任务和 JointModel 中的相同,就是对每个部件进行:

  • 平移
  • 旋转
  • 绘制

首先,base 不会旋转,所以只需要将其移动到合适的位置,再调用 drawBox()进行绘制。通过向drawBox()传入参数,指定 base 的宽度是10,高度是2,长度是10,即一个扁平的基座。

然后,按照 arm1、arm2 和 palm 这些部件在模型中的层次顺序,对每一个部件都进行上述三个步骤,这与 JointModel 中的是一样的。

比较麻烦的是 finger1 和 finger2因为它们并不是上下层的关系,而是都连接在 palm 上,此时要格外注意计算模型矩阵的过程。首先来看 finger1,它相对于 palm 原点沿Z 轴平移了2.0单位,并且可以绕X轴旋转,我们执行上述三个步骤。

1
2
3
g_modelMatrix.translate(0.0, 0.0, 2.0);
g_modelMatrix.rotate(g_joint3Angle, 1.0, 0.0, 0.0); // Rotate around the x-axis
drawBox(gl, n, 1.0, 2.0, 1.0, viewProjMatrix, u_MvpMatrix, u_NormalMatrix);

接着看 finger2,如果遵循上述同样的步骤,沿 z 轴平移 -2.0 个单位并绕X轴旋转就会出现问题。在将模型矩阵“沿Z轴平移-2.0个单位”之前,模型矩阵实际上处于绘制 finger1 的状态,这会导致 finger2 连接在 finger1 而不是 palm上,使得 finger1 转动带动 finger2

所以,我们在绘制 finger1 之前,先将模型矩阵保存起来;绘制完 finger1 后,再将保存的模型矩阵取出来作为当前的模型矩阵并继续绘制 finger2。可以使用一个来完成这项操作:调用 pushMatrix() 并将模型矩阵 g_modelMatrix 作为参数传入,将当时模型矩阵的状态保存起来,然后绘制完 finger1后,调popMatrix()获取之前保存的矩阵,并赋给 g_modelMatrix,使模型矩阵又回到绘制 finger1 之前的状态,在此基础上绘制 finger2。

pushMatrix()函数和popMatrix()函数如下所示,它们使用全局变量 g_matrixStack 来存储矩阵,前者向栈中压入一个矩阵,而后者从栈中取出一个。

1
2
3
4
5
6
7
8
9
var g_matrixStack = []; // 存储矩阵的栈
function pushMatrix(m) { // 将矩阵压入栈
var m2 = new Matrix4(m);
g_matrixStack.push(m2);
}

function popMatrix() { // 从栈中弹出矩阵
return g_matrixStack.pop();
}

只要栈足够深,用这种方法就可以绘制任意复杂的层次 结构模型。我们只需要按照层次顺序,从高到低绘制部件,并在绘制“具有兄弟部件”的部件前将模型矩阵压入栈,绘制完再弹出即可。

drawBox()绘制部件

最后看一下 drawBox()函数,该函数的任务是绘制机器人手臂的一个部件,它接受若干个参数:

1
function drawBox(gl, n, width, height, depth, viewProjMatrix, u_MvpMatrix, u_NormalMatrix)

参数 widthheightdepth 分别表示待绘制部件的宽度、高度和深度。其他的参数与 JointMode.js 中无异:

  • 参数 viewMatrix 表示视图矩阵
  • u_MvpMatrix 表示模型视图投影矩阵
  • u_NormalMatrix 表示用来计算变换后的法向量矩阵
  • 后两者被传给顶点着色器中相应的同名 uniform 变量。

此外,与 JointModel 不同的是,本例中部件的三维模型是标准化的立方体,其边长为1,原点位于底面。drawBox()函数的定义如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Draw rectangular solid
function drawBox(gl, n, width, height, depth, viewProjMatrix, u_MvpMatrix, u_NormalMatrix) {
pushMatrix(g_modelMatrix); // Save the model matrix
// Scale a cube and draw
g_modelMatrix.scale(width, height, depth);
// Calculate the model view project matrix and pass it to u_MvpMatrix
g_mvpMatrix.set(viewProjMatrix);
g_mvpMatrix.multiply(g_modelMatrix);
gl.uniformMatrix4fv(u_MvpMatrix, false, g_mvpMatrix.elements);
// Calculate the normal transformation matrix and pass it to u_NormalMatrix
g_normalMatrix.setInverseOf(g_modelMatrix);
g_normalMatrix.transpose();
gl.uniformMatrix4fv(u_NormalMatrix, false, g_normalMatrix.elements);
// Draw
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
g_modelMatrix = popMatrix(); // Retrieve the model matrix
}

drawBox()函数首先将模型矩阵乘以由 width、height 和 depth 参数生成的缩放矩阵,使绘制处的立方体尺寸与设想的一样。然后使用 pushMatrix()函数将模型矩阵压入栈中,使用 popMatrix()再重新获得之。如果不这样做,当绘制 arm2 的时候,对 arm1的拉伸效果还会仍然留在模型矩阵中,并影响 arm2 的绘制。

虽然 pushMareix()函数和 popMatrix()函数使代码变得更复杂了,但这是值得的,因为你只用了一组顶点数据就绘制了好几个大小位置各不相同的立方体部件。或者,我们也可以对每一个部件都单独使用一组顶点数据,接下来看看如何实现。


1.2 drawSegments()—绘制部件

对应代码:MultiJointModel_segment

分析

这一节将换一种方式来绘制机器人手臂,那就是,对每一个部件,都定义一组顶点数据,并存储在一个单独的缓冲区对象中。通常,一个部件的定点数据包括坐标、法向量、索引值等,但是这里的每个部件都是立方体,所以你可以让各部件共享法向量和索引值,而仅仅为个部件单独定义顶点坐标。每个部件的顶点坐标分别存储在对应的缓冲区中,在绘制整条机器人手臂时轮流使用。

实例程序的关键点是:

  • 为每个部件单独创建一个缓冲区,在其中存储顶点的坐标数据

  • 绘制部件之前,将相应缓冲区对象分配给 a_Position 变量

  • 开启 a_Position 变量并绘制该部件

main()函数的流程很简明,包括初始化缓冲区,获取 a_Position 的存储地址,然后调用 draw()函数进行绘制等。

initVertexBuffers()

接着来看 initVertexBuffers()函数,该函数之前定义了若干全局变量,白哦啊是存储各个部件顶点坐标数据的缓冲区对象。本例与 MultiJointModel.js 的主要区别在顶点坐标上,我们不再使用一个立方体经过不同变换来绘制不同的部件,而是将每个部件的顶点坐标分开定义在不同的数组中

initArrayBufferForLatreUse()

真正创建这些缓冲对象是由 initArrayBufferForLatreUse()函数完成的,该函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function initArrayBufferForLaterUse(gl, data, num, type){
var buffer = gl.createBuffer(); // Create a buffer object
if (!buffer) {
console.log('Failed to create the buffer object');
return null;
}
// Write date into the buffer object
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

// Store the necessary information to assign the object to the attribute variable later
buffer.num = num;
buffer.type = type;

return buffer;
}

initArrayBufferForLatreUse()函数首先创建了缓冲区对象,然后向其中写入数据。

注意,函数并没有将缓冲区对象分配给 attribute 变量(gl.vertexAtteibPointer())或开启attribute 变量(gl.enableVertexAttribArray()),这两个步骤将留到真正进行绘制之前再完成。

另外,为了便于将缓冲区分配给 attribute 变量,我们手动为其添加了两个属性 num 和 type。

这里利用了 JS 的一个有趣的特性,就是可以自由地对对象添加新的属性。你可以直接通过属性名为对象添加新属性,并向其赋值。如你所见,我们为缓冲区对象添加了新的 num 属性并保存其中顶点的个数,添加了 type 属性以保存数据类型。当然,也可以通过相同的方式访问这些属性。注意,在使用 JS 的这项特性时应格外小心,如果不小心拼错了属性名,浏览器也不会报错。同样你也应该记得,这样做会增加性能开销。

最后,调用 draw()函数绘制整个模型,与 MultiJointModel 中一样。但是调用 drawSegments()函数的方式与前例调用 drawBox()函数的方式有所不同,第3个参数是存储了顶点坐标数据的缓冲区对象:

1
drawSegment(gl, n, buffer, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix) 

drawSegments()将缓冲区对象分配给 a_Position 变量并开启之,然后调用 gl.drawElements()进行绘制操作。这里使用了之前为缓冲区对象添加的 num 和 type 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 绘制部件
function drawSegment(gl, n, buffer, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// Assign the buffer object to the attribute variable
gl.vertexAttribPointer(a_Position, buffer.num, buffer.type, false, 0, 0);
// Enable the assignment of the buffer object to the attribute variable
gl.enableVertexAttribArray(a_Position);

// Calculate the model view project matrix and pass it to u_MvpMatrix
g_mvpMatrix.set(viewProjMatrix);
g_mvpMatrix.multiply(g_modelMatrix);
gl.uniformMatrix4fv(u_MvpMatrix, false, g_mvpMatrix.elements);
// Calculate matrix for normal and pass it to u_NormalMatrix
g_normalMatrix.setInverseOf(g_modelMatrix);
g_normalMatrix.transpose();
gl.uniformMatrix4fv(u_NormalMatrix, false, g_normalMatrix.elements);
// Draw
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
}

这一次,你不必再像前例中那样,在绘制每个部件时对模型矩阵进行缩放操作了,因为每个部件的顶点坐标都已经事先定义好了。同样也没必要再使用来管理模型矩阵,所以 pushMatrix()函数和 popMatrix()也不需要了。

二、着色器和着色器程序对象:initShaders()函数的作用

最后,本章来研究一下以前一直使用的辅助函数 initShaders()。以前的所有程序都使用了这个函数,它隐藏了建立和初始化着色器的细节。本书故意将这一部分内容留到最后,是为了确保你在学习 initShaders()函数中的复杂细节时,对 WebGL 已经有了比较深入的交接。掌握这部分内容并不是必须的,直接使用 initShaders()函数也能够编写处相当不错的 WebGL 程序,但如果你确实很想知道 WebGL 原生 API 是如何将字符串形式的 GLSL ES 代码编译为显卡中运行的着色器程序,那么这一节的内容将大大满足你的好奇心。
initShaders()函数的作用是,编译 GLSL ES 代码,创建和初始化着色器供 WebGL 使用。具体地,分为以下7个步骤:

  • 创建着色器对象(gl.createShader)
  • 向着色器对象中填充着色器程序的源代码(gl.shaderSource)
  • 编译着色器(gl.compileShader)
  • 创建程序对象(gl.createProgram)
  • 为程序对象分配着色器(gl.attachShader)
  • 连接程序对象(gl.linkProgram)
  • 使用程序对象(gl.useProgram)

虽然每一步看上去都比较简单,但放在一起显得复杂了,我们将逐条讨论。

2.1 着色器对象和程序对象

首先,你需要知道这里出现了两种对象:着色器对象程序对象

着色器对象:着色器对象管理一个顶点着色器或一个片元着色器。每一个着色器都有一个着色器对象。

程序对象:程序对象是管理着色器对象的容器。WebGL 中,一个程序对象必须包含一个顶点着色器和一个片元着色器。

着色器对象和程序对象间的关系:

着色器对象和程序对象

2.2 创建着色器对象(gl.createShader())

所有的着色器对象都必须通过调用 gl.createShader()来创建。

gl.createShader

gl.createShader()函数根据传入的参数创建一个顶点着色器或者片元着色器。如果不再需要这个着色器,可以调用 gl.deleteShader()函数来删除着色器。

gl.deleteShader

注意,如果着色器对象还在使用,那么 gl.deleteShader()并不会立刻删除着色器,而是要等到程序对象不再使用该着色器后,才将其删除。

2.3 指定着色器对象的代码(gk.shaderSource)

通过 gl.shaderSource()函数向着色器制动 GLSL ES 源代码。在 JS 程序中,源代码以字符串的形式存储。

gl.shaderSource

2.4 编译着色器(gl.compileShader)

向着色器对象传入源代码之后,还需要对其进行编译才能够使用。GLSL ES 语言和 JS 不同而更接近 C 或 C++,在使用之前需要编译成二进制的可执行格式,WebGL 系统真正使用的是这种可执行格式。使用 gl.compileShader()函数进行编译。注意,如果你通过调用 gl.shaderSource(),用新的代码替换掉了着色器中旧的代码,WebGL 系统中的用旧的代码编译处可执行部分不会被自动替换,你需要手动地重新进行编译。

gl.compileShader

当调用 gl.compileShader()函数时,如果着色器代码中存在错误,那么就会出现编译错误。可以调用 gl.getShaderParameter()函数来检查着色器的状态。

gl.getShaderParameter

调用 gl.getShaderParameter()并将参数 pname 指定为 gl.COMPILE_STATUS,就可以检查着色器编译是否成功。

如果编译失败,gl.getShaderParameter()会返回 false,WebGL 系统会把编译错误的具体内容写入着色器的信息日志,我们可以通过 gl.getShaderInfoLog()来获取之。

gl.getShaderInfoLog

虽然日志信息的具体格式依赖于浏览器对 WebGL 的实现,但大多数 WebGL 系统给出的错误信息都会包含代码出错行的行号。比如,如果你试图编译如下这样一个着色器:

1
2
3
4
var FSHADER_SOURCE = 
'void main(){\n' +
' gl.FragColor = vec4(1.0,0.0,0.0,1.0);\n' +
'}\n';

错误信息

2.5 创建程序对象(gl.createProgram)

如前所述,程序对象包含了顶点着色器和片元着色器,可以调用 gl.createProgram()来创建程序对象。事实上,之前使用程序对象,gl.getAttribLocation()函数和 gl.getUniformLocation()函数的第1个参数,就是这个程序对象。

gl.createProgram

类似地,可以使用 gl.deleteProgram()函数来删除程序对象。

gl.deleteProgram

一旦程序对象被创建之后,需要向程序附上两个着色器。

2.6 为程序对象分配着色器对象(gl.attachShader)

WebGL 系统要运行起来,必须要有两个着色器:一个顶点着色器和一个片元着色器。可以使用 gl.attachShader()函数为程序对象分配这两个着色器。

gl.attachShader

着色器在附给程序对象前,并不一定要为其指定代码或进行编译。也就是说,把空的着色器赋给程序对象也是可以的。类似的,可以使用 gl.derachShader()函数来解除分配给程序对象的着色器。

gl.derachShader

2.7 连接程序对象(gl.linkProgram)

在为程序对象分配了两个着色器对象后,还需要将着色器连接起来。使用 gl.linkProgram()函数来进行这一步操作。

gl.linkProgram

程序对象进行着色器连接操作,目的是保证:

  • 顶点着色器和片元着色器的 varying 变量同名同类型,且一一对应;

  • 顶点着色器对每个varying 变量赋了值;

  • 顶点着色器和片元着色器的同名uniform 变量也是同类型的,无需一一对应,即某些 uniform 变量可以出现在一个着色器中而不出现在另一个中;

  • 着色器中的 attribute 变量、uniform 变量和 varying 变量的个数没有超过着色器的上学,等等。

在着色器连接之后,应当检查是否连接成功。通过调用 gl.getProgramParameters()函数来实现。
gl.getProgramParameters

如果程序已经成功连接,我们就得到了一个二进制的可执行模块供 WebGL 系统使用。如果连接失败了,也可以通过调用 gl.getProgramInfoLog()从信息日志中获取连接错误的信息。

gl.getProgramInfoLog

2.8 告知 WebGL 系统所使用的程序对象(gl.useProgram)

最后,通过调用 gl.useProgram()告知 WebGL 系统绘制时使用哪个程序对象。

gl.useProgram

这个函数的存在使得 WebGL 具有了一个强大的特性,那就是在会之前准备多个程序对象,然后在绘制的时候根据需要切换程序对象。

这样,建立和初始化着色器的任务就算完成了。如你所见,initShaders()函数隐藏了大量的细节,我们可以放心地使用该函数来创建和初始化着色器,而不必考虑这些细节。本质上,在该函数顺利执行后,顶点着色器和偏远着色器就已经就位了,只需要调用 gl.drawArryas()或 gl.drawElements()来使整个 WebGL 系统运行起来。

2.9 initShaders()函数的内部流程

cuon-utils.js 中 initShaders()函数的内部流程。

initShaders() 函数将调用 createProgram()函数,后者负责创建一个连接好的程序对象;createProgram()函数则又会调用 loadShader()函数,后者负责创建一个编译好的着色器对象;这3个函数被一次定义在 cuon-utils.js 文件中。initShaders() 函数定义在该文件的顶部,注意该文件中每个函数前面的注释是按照 JavaDoc 的格式编写,它们可以用来自动化地生成文档。

1
2
3
4
5
6
7
8
9
10
11
12
function initShaders(gl, vshader, fshader) {
var program = createProgram(gl, vshader, fshader);
if (!program) {
console.log('Failed to create program');
return false;
}

gl.useProgram(program);
gl.program = program;

return true;
}

initShaders() 函数本身很简单,首先调用 createProgram()函数创建一个连接好的额程序对象,然后告诉 WebGL 系统来使用这个程序对象,最后将程序对象设为 gl 对象的 program 属性。

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
function createProgram(gl, vshader, fshader) {
// Create shader object
var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
if (!vertexShader || !fragmentShader) {
return null;
}

// Create a program object
var program = gl.createProgram();
if (!program) {
return null;
}

// Attach the shader objects
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);

// Link the program object
gl.linkProgram(program);

// Check the result of linking
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
var error = gl.getProgramInfoLog(program);
console.log('Failed to link program: ' + error);
gl.deleteProgram(program);
gl.deleteShader(fragmentShader);
gl.deleteShader(vertexShader);
return null;
}
return program;
}

createProgram()函数通过调用 loadShader()函数,创建顶点着色器和片元着色器的着色器对象。loadShader()函数返回的着色器对象已经制定过源码并已经成功编译了。

createProgram()函数自己负责创建程序对象,然后将前面创建的顶点着色器和偏远着色器分配给程序对象。

接着,该函数连接程序对象,并检查是否连接成功。如果连接成功,就会返回程序对象。

最后来看一下 loadShader()函数:

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
function loadShader(gl, type, source) {
// Create shader object
var shader = gl.createShader(type);
if (shader == null) {
console.log('unable to create shader');
return null;
}

// Set the shader program
gl.shaderSource(shader, source);

// Compile the shader
gl.compileShader(shader);

// Check the result of compilation
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
var error = gl.getShaderInfoLog(shader);
console.log('Failed to compile shader: ' + error);
gl.deleteShader(shader);
return null;
}

return shader;
}

oadShader()函数首先创建了一个着色器对象,然后为该着色器对象指定源代码,并进行编译,接着检查编译是否成功,如果成功编译,没有出错,就返回着色器对象。

【WebGL之巅】系列完结

实例

代码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
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
// Vertex shader program
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Normal;\n' +
'uniform mat4 u_MvpMatrix;\n' +
'uniform mat4 u_NormalMatrix;\n' +
'varying vec4 v_Color;\n' +
'void main() {\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
// Shading calculation to make the arm look three-dimensional
' vec3 lightDirection = normalize(vec3(0.0, 0.5, 0.7));\n' + // Light direction
' vec4 color = vec4(1.0, 0.4, 0.0, 1.0);\n' + // Robot color
' vec3 normal = normalize((u_NormalMatrix * a_Normal).xyz);\n' +
' float nDotL = max(dot(normal, lightDirection), 0.0);\n' +
' v_Color = vec4(color.rgb * nDotL + vec3(0.1), color.a);\n' +
'}\n';

// Fragment shader program
var FSHADER_SOURCE =
'#ifdef GL_ES\n' +
'precision mediump float;\n' +
'#endif\n' +
'varying vec4 v_Color;\n' +
'void main() {\n' +
' gl_FragColor = v_Color;\n' +
'}\n';

function main() {
// Retrieve <canvas> element
var canvas = document.getElementById('webgl');

// Get the rendering context for WebGL
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('Failed to get the rendering context for WebGL');
return;
}

// Initialize shaders
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.');
return;
}

// Set the vertex information
var n = initVertexBuffers(gl);
if (n < 0) {
console.log('Failed to set the vertex information');
return;
}

// Set the clear color and enable the depth test
//gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.enable(gl.DEPTH_TEST);

// Get the storage locations of uniform variables
var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
var u_NormalMatrix = gl.getUniformLocation(gl.program, 'u_NormalMatrix');
if (!u_MvpMatrix || !u_NormalMatrix) {
console.log('Failed to get the storage location');
return;
}

// 计算视图投影矩阵
var viewProjMatrix = new Matrix4();
viewProjMatrix.setPerspective(50.0, canvas.width / canvas.height, 1.0, 100.0);
viewProjMatrix.lookAt(20.0, 10.0, 30.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

// Register the event handler to be called on key press
document.onkeydown = function(ev){ keydown(ev, gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix); };

 draw(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix); // Draw the robot arm
}

var ANGLE_STEP = 3.0; // 每次按键转动的角度
var g_arm1Angle = 90.0; // arm1的当前角度
var g_joint1Angle = 45.0; // joint1的当前角度
var g_joint2Angle = 0.0; // joint2
var g_joint3Angle = 0.0; // joint3

function keydown(ev, gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix) {
switch (ev.keyCode) {
case 40: // 上方向键 - joint1绕z轴正向转动
if (g_joint1Angle < 135.0) g_joint1Angle += ANGLE_STEP;
break;
case 38: // 下方向键 - joint1绕z轴负向转动
if (g_joint1Angle > -135.0) g_joint1Angle -= ANGLE_STEP;
break;
case 39: // 右方向键 - arm1绕y轴正向转动
g_arm1Angle = (g_arm1Angle + ANGLE_STEP) % 360;
break;
case 37: // 左方向键 - arm1绕y轴负向转动
g_arm1Angle = (g_arm1Angle - ANGLE_STEP) % 360;
break;
case 90: // 'z'key -> 使joint2正向旋转
g_joint2Angle = (g_joint2Angle + ANGLE_STEP) % 360;
break;
case 88: // 'x'key -> 使joint2负向旋转
g_joint2Angle = (g_joint2Angle - ANGLE_STEP) % 360;
break;
case 86: // 'v'key -> 使joint3正向旋转
if (g_joint3Angle < 60.0) g_joint3Angle = (g_joint3Angle + ANGLE_STEP) % 360;
break;
case 67: // 'c'key -> 使joint3负向旋转
if (g_joint3Angle > -60.0) g_joint3Angle = (g_joint3Angle - ANGLE_STEP) % 360;
break;
default: return; // Skip drawing at no effective action
}
// Draw the robot arm
draw(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix);
}

function initVertexBuffers(gl) {
// Coordinates(Cube which length of one side is 1 with the origin on the center of the bottom)
var vertices = new Float32Array([
0.5, 1.0, 0.5, -0.5, 1.0, 0.5, -0.5, 0.0, 0.5, 0.5, 0.0, 0.5, // v0-v1-v2-v3 front
0.5, 1.0, 0.5, 0.5, 0.0, 0.5, 0.5, 0.0,-0.5, 0.5, 1.0,-0.5, // v0-v3-v4-v5 right
0.5, 1.0, 0.5, 0.5, 1.0,-0.5, -0.5, 1.0,-0.5, -0.5, 1.0, 0.5, // v0-v5-v6-v1 up
-0.5, 1.0, 0.5, -0.5, 1.0,-0.5, -0.5, 0.0,-0.5, -0.5, 0.0, 0.5, // v1-v6-v7-v2 left
-0.5, 0.0,-0.5, 0.5, 0.0,-0.5, 0.5, 0.0, 0.5, -0.5, 0.0, 0.5, // v7-v4-v3-v2 down
0.5, 0.0,-0.5, -0.5, 0.0,-0.5, -0.5, 1.0,-0.5, 0.5, 1.0,-0.5 // v4-v7-v6-v5 back
]);

// Normal
var normals = new Float32Array([
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, // v0-v1-v2-v3 front
1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, // v0-v3-v4-v5 right
0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, // v0-v5-v6-v1 up
-1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, // v1-v6-v7-v2 left
0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, // v7-v4-v3-v2 down
0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0 // v4-v7-v6-v5 back
]);

// Indices of the vertices
var indices = new Uint8Array([
0, 1, 2, 0, 2, 3, // front
4, 5, 6, 4, 6, 7, // right
8, 9,10, 8,10,11, // up
12,13,14, 12,14,15, // left
16,17,18, 16,18,19, // down
20,21,22, 20,22,23 // back
]);

// Write the vertex property to buffers (coordinates and normals)
if (!initArrayBuffer(gl, 'a_Position', vertices, gl.FLOAT, 3)) return -1;
if (!initArrayBuffer(gl, 'a_Normal', normals, gl.FLOAT, 3)) return -1;

// Unbind the buffer object
gl.bindBuffer(gl.ARRAY_BUFFER, null);

// Write the indices to the buffer object
var indexBuffer = gl.createBuffer();
if (!indexBuffer) {
console.log('Failed to create the buffer object');
return -1;
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

return indices.length;
}

function initArrayBuffer(gl, attribute, data, type, num) {
// Create a buffer object
var buffer = gl.createBuffer();
if (!buffer) {
console.log('Failed to create the buffer object');
return false;
}
// Write date into the buffer object
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

// Assign the buffer object to the attribute variable
var a_attribute = gl.getAttribLocation(gl.program, attribute);
if (a_attribute < 0) {
console.log('Failed to get the storage location of ' + attribute);
return false;
}
gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0);
// Enable the assignment of the buffer object to the attribute variable
gl.enableVertexAttribArray(a_attribute);

return true;
}

// 变换坐标的矩阵
var g_modelMatrix = new Matrix4(), g_mvpMatrix = new Matrix4();

function draw(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix) {
// 用以清空颜色缓冲区和深度缓冲区的背景颜色
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

// base绘制
var baseHeight = 2.0;
g_modelMatrix.setTranslate(0.0, -12.0, 0.0);
drawBox(gl, n, 10.0, baseHeight, 10.0, viewProjMatrix, u_MvpMatrix, u_NormalMatrix);

// Arm1
var arm1Length = 10.0;
g_modelMatrix.translate(0.0, baseHeight, 0.0); // Move onto the base
g_modelMatrix.rotate(g_arm1Angle, 0.0, 1.0, 0.0); // Rotate around the y-axis
drawBox(gl, n, 3.0, arm1Length, 3.0, viewProjMatrix, u_MvpMatrix, u_NormalMatrix); // Draw

// Arm2
var arm2Length = 10.0;
g_modelMatrix.translate(0.0, arm1Length, 0.0); // Move to joint1
g_modelMatrix.rotate(g_joint1Angle, 0.0, 0.0, 1.0); // Rotate around the z-axis
drawBox(gl, n, 4.0, arm2Length, 4.0, viewProjMatrix, u_MvpMatrix, u_NormalMatrix); // Draw

// A palm
var palmLength = 2.0;
g_modelMatrix.translate(0.0, arm2Length, 0.0); // Move to palm
g_modelMatrix.rotate(g_joint2Angle, 0.0, 1.0, 0.0); // Rotate around the y-axis
drawBox(gl, n, 2.0, palmLength, 6.0, viewProjMatrix, u_MvpMatrix, u_NormalMatrix); // Draw

// Move to the center of the tip of the palm
g_modelMatrix.translate(0.0, palmLength, 0.0);

// Draw finger1
pushMatrix(g_modelMatrix);
g_modelMatrix.translate(0.0, 0.0, 2.0);
g_modelMatrix.rotate(g_joint3Angle, 1.0, 0.0, 0.0); // Rotate around the x-axis
drawBox(gl, n, 1.0, 2.0, 1.0, viewProjMatrix, u_MvpMatrix, u_NormalMatrix);
g_modelMatrix = popMatrix();

// Draw finger2
g_modelMatrix.translate(0.0, 0.0, -2.0);
g_modelMatrix.rotate(-g_joint3Angle, 1.0, 0.0, 0.0); // Rotate around the x-axis
drawBox(gl, n, 1.0, 2.0, 1.0, viewProjMatrix, u_MvpMatrix, u_NormalMatrix);
}

var g_matrixStack = []; // 存储矩阵的栈
function pushMatrix(m) { // 将矩阵压入栈
var m2 = new Matrix4(m);
g_matrixStack.push(m2);
}

function popMatrix() { // 从栈中弹出矩阵
return g_matrixStack.pop();
}

var g_normalMatrix = new Matrix4(); // Coordinate transformation matrix for normals

// Draw rectangular solid
function drawBox(gl, n, width, height, depth, viewProjMatrix, u_MvpMatrix, u_NormalMatrix) {
pushMatrix(g_modelMatrix); // Save the model matrix
// Scale a cube and draw
g_modelMatrix.scale(width, height, depth);
// Calculate the model view project matrix and pass it to u_MvpMatrix
g_mvpMatrix.set(viewProjMatrix);
g_mvpMatrix.multiply(g_modelMatrix);
gl.uniformMatrix4fv(u_MvpMatrix, false, g_mvpMatrix.elements);
// Calculate the normal transformation matrix and pass it to u_NormalMatrix
g_normalMatrix.setInverseOf(g_modelMatrix);
g_normalMatrix.transpose();
gl.uniformMatrix4fv(u_NormalMatrix, false, g_normalMatrix.elements);
// Draw
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
g_modelMatrix = popMatrix(); // Retrieve the model matrix
}

效果1

通过键盘操纵机器人手臂:

  • 使用左右方向键控制 arm1(下半部分)水平转动;

  • 使用上下方方向键控制 arm2(上半部分) 绕 joint1 关节垂直转动;

  • 使用 x 和 z 键旋转 Joint2;

  • 使用 C 和 V 键旋转 finger1 和 finger2

LookAtTriangles
Please use the browser supporting "canvas".

代码2

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
// Vertex shader program
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Normal;\n' +
'uniform mat4 u_MvpMatrix;\n' +
'uniform mat4 u_NormalMatrix;\n' +
'varying vec4 v_Color;\n' +
'void main() {\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
// The followings are some shading calculation to make the arm look three-dimensional
' vec3 lightDirection = normalize(vec3(0.0, 0.5, 0.7));\n' + // Light direction
' vec4 color = vec4(1.0, 0.4, 0.0, 1.0);\n' + // Robot color
' vec3 normal = normalize((u_NormalMatrix * a_Normal).xyz);\n' +
' float nDotL = max(dot(normal, lightDirection), 0.0);\n' +
' v_Color = vec4(color.rgb * nDotL + vec3(0.1), color.a);\n' +
'}\n';

// Fragment shader program
var FSHADER_SOURCE =
'#ifdef GL_ES\n' +
'precision mediump float;\n' +
'#endif\n' +
'varying vec4 v_Color;\n' +
'void main() {\n' +
' gl_FragColor = v_Color;\n' +
'}\n';

function main() {
// Retrieve <canvas> element
var canvas = document.getElementById('webgl');

// Get the rendering context for WebGL
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('Failed to get the rendering context for WebGL');
return;
}

// Initialize shaders
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.');
return;
}

// Set the vertex information
var n = initVertexBuffers(gl);
if (n < 0) {
console.log('Failed to set the vertex information');
return;
}

// Set the clear color and enable the depth test
// gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.enable(gl.DEPTH_TEST);

// Get the storage locations of attribute and uniform variables
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
var u_NormalMatrix = gl.getUniformLocation(gl.program, 'u_NormalMatrix');
if (a_Position < 0 || !u_MvpMatrix || !u_NormalMatrix) {
console.log('Failed to get the storage location of attribute or uniform variable');
return;
}

// Calculate the view projection matrix
var viewProjMatrix = new Matrix4();
viewProjMatrix.setPerspective(50.0, canvas.width / canvas.height, 1.0, 100.0);
viewProjMatrix.lookAt(20.0, 10.0, 30.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

// Register the event handler to be called on key press
document.onkeydown = function(ev){ keydown(ev, gl, n, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix); };

draw(gl, n, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix);
}

var ANGLE_STEP = 3.0; // The increments of rotation angle (degrees)
var g_arm1Angle = 90.0; // The rotation angle of arm1 (degrees)
var g_joint1Angle = 45.0; // The rotation angle of joint1 (degrees)
var g_joint2Angle = 0.0; // The rotation angle of joint2 (degrees)
var g_joint3Angle = 0.0; // The rotation angle of joint3 (degrees)

function keydown(ev, gl, o, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix) {
switch (ev.keyCode) {
case 40: // Up arrow key -> the positive rotation of joint1 around the z-axis
if (g_joint1Angle < 135.0) g_joint1Angle += ANGLE_STEP;
break;
case 38: // Down arrow key -> the negative rotation of joint1 around the z-axis
if (g_joint1Angle > -135.0) g_joint1Angle -= ANGLE_STEP;
break;
case 39: // Right arrow key -> the positive rotation of arm1 around the y-axis
g_arm1Angle = (g_arm1Angle + ANGLE_STEP) % 360;
break;
case 37: // Left arrow key -> the negative rotation of arm1 around the y-axis
g_arm1Angle = (g_arm1Angle - ANGLE_STEP) % 360;
break;
case 90: // 'z'key -> the positive rotation of joint2
g_joint2Angle = (g_joint2Angle + ANGLE_STEP) % 360;
break;
case 88: // 'x'key -> the negative rotation of joint2
g_joint2Angle = (g_joint2Angle - ANGLE_STEP) % 360;
break;
case 86: // 'v'key -> the positive rotation of joint3
if (g_joint3Angle < 60.0) g_joint3Angle = (g_joint3Angle + ANGLE_STEP) % 360;
break;
case 67: // 'c'key -> the nagative rotation of joint3
if (g_joint3Angle > -60.0) g_joint3Angle = (g_joint3Angle - ANGLE_STEP) % 360;
break;
default: return; // Skip drawing at no effective action
}
// Draw
draw(gl, o, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix);
}

var g_baseBuffer = null; // Buffer object for a base
var g_arm1Buffer = null; // Buffer object for arm1
var g_arm2Buffer = null; // Buffer object for arm2
var g_palmBuffer = null; // Buffer object for a palm
var g_fingerBuffer = null; // Buffer object for fingers

function initVertexBuffers(gl){
// Vertex coordinate (prepare coordinates of cuboids for all segments)
var vertices_base = new Float32Array([ // Base(10x2x10)
5.0, 2.0, 5.0, -5.0, 2.0, 5.0, -5.0, 0.0, 5.0, 5.0, 0.0, 5.0, // v0-v1-v2-v3 front
5.0, 2.0, 5.0, 5.0, 0.0, 5.0, 5.0, 0.0,-5.0, 5.0, 2.0,-5.0, // v0-v3-v4-v5 right
5.0, 2.0, 5.0, 5.0, 2.0,-5.0, -5.0, 2.0,-5.0, -5.0, 2.0, 5.0, // v0-v5-v6-v1 up
-5.0, 2.0, 5.0, -5.0, 2.0,-5.0, -5.0, 0.0,-5.0, -5.0, 0.0, 5.0, // v1-v6-v7-v2 left
-5.0, 0.0,-5.0, 5.0, 0.0,-5.0, 5.0, 0.0, 5.0, -5.0, 0.0, 5.0, // v7-v4-v3-v2 down
5.0, 0.0,-5.0, -5.0, 0.0,-5.0, -5.0, 2.0,-5.0, 5.0, 2.0,-5.0 // v4-v7-v6-v5 back
]);

var vertices_arm1 = new Float32Array([ // Arm1(3x10x3)
1.5, 10.0, 1.5, -1.5, 10.0, 1.5, -1.5, 0.0, 1.5, 1.5, 0.0, 1.5, // v0-v1-v2-v3 front
1.5, 10.0, 1.5, 1.5, 0.0, 1.5, 1.5, 0.0,-1.5, 1.5, 10.0,-1.5, // v0-v3-v4-v5 right
1.5, 10.0, 1.5, 1.5, 10.0,-1.5, -1.5, 10.0,-1.5, -1.5, 10.0, 1.5, // v0-v5-v6-v1 up
-1.5, 10.0, 1.5, -1.5, 10.0,-1.5, -1.5, 0.0,-1.5, -1.5, 0.0, 1.5, // v1-v6-v7-v2 left
-1.5, 0.0,-1.5, 1.5, 0.0,-1.5, 1.5, 0.0, 1.5, -1.5, 0.0, 1.5, // v7-v4-v3-v2 down
1.5, 0.0,-1.5, -1.5, 0.0,-1.5, -1.5, 10.0,-1.5, 1.5, 10.0,-1.5 // v4-v7-v6-v5 back
]);

var vertices_arm2 = new Float32Array([ // Arm2(4x10x4)
2.0, 10.0, 2.0, -2.0, 10.0, 2.0, -2.0, 0.0, 2.0, 2.0, 0.0, 2.0, // v0-v1-v2-v3 front
2.0, 10.0, 2.0, 2.0, 0.0, 2.0, 2.0, 0.0,-2.0, 2.0, 10.0,-2.0, // v0-v3-v4-v5 right
2.0, 10.0, 2.0, 2.0, 10.0,-2.0, -2.0, 10.0,-2.0, -2.0, 10.0, 2.0, // v0-v5-v6-v1 up
-2.0, 10.0, 2.0, -2.0, 10.0,-2.0, -2.0, 0.0,-2.0, -2.0, 0.0, 2.0, // v1-v6-v7-v2 left
-2.0, 0.0,-2.0, 2.0, 0.0,-2.0, 2.0, 0.0, 2.0, -2.0, 0.0, 2.0, // v7-v4-v3-v2 down
2.0, 0.0,-2.0, -2.0, 0.0,-2.0, -2.0, 10.0,-2.0, 2.0, 10.0,-2.0 // v4-v7-v6-v5 back
]);

var vertices_palm = new Float32Array([ // Palm(2x2x6)
1.0, 2.0, 3.0, -1.0, 2.0, 3.0, -1.0, 0.0, 3.0, 1.0, 0.0, 3.0, // v0-v1-v2-v3 front
1.0, 2.0, 3.0, 1.0, 0.0, 3.0, 1.0, 0.0,-3.0, 1.0, 2.0,-3.0, // v0-v3-v4-v5 right
1.0, 2.0, 3.0, 1.0, 2.0,-3.0, -1.0, 2.0,-3.0, -1.0, 2.0, 3.0, // v0-v5-v6-v1 up
-1.0, 2.0, 3.0, -1.0, 2.0,-3.0, -1.0, 0.0,-3.0, -1.0, 0.0, 3.0, // v1-v6-v7-v2 left
-1.0, 0.0,-3.0, 1.0, 0.0,-3.0, 1.0, 0.0, 3.0, -1.0, 0.0, 3.0, // v7-v4-v3-v2 down
1.0, 0.0,-3.0, -1.0, 0.0,-3.0, -1.0, 2.0,-3.0, 1.0, 2.0,-3.0 // v4-v7-v6-v5 back
]);

var vertices_finger = new Float32Array([ // Fingers(1x2x1)
0.5, 2.0, 0.5, -0.5, 2.0, 0.5, -0.5, 0.0, 0.5, 0.5, 0.0, 0.5, // v0-v1-v2-v3 front
0.5, 2.0, 0.5, 0.5, 0.0, 0.5, 0.5, 0.0,-0.5, 0.5, 2.0,-0.5, // v0-v3-v4-v5 right
0.5, 2.0, 0.5, 0.5, 2.0,-0.5, -0.5, 2.0,-0.5, -0.5, 2.0, 0.5, // v0-v5-v6-v1 up
-0.5, 2.0, 0.5, -0.5, 2.0,-0.5, -0.5, 0.0,-0.5, -0.5, 0.0, 0.5, // v1-v6-v7-v2 left
-0.5, 0.0,-0.5, 0.5, 0.0,-0.5, 0.5, 0.0, 0.5, -0.5, 0.0, 0.5, // v7-v4-v3-v2 down
0.5, 0.0,-0.5, -0.5, 0.0,-0.5, -0.5, 2.0,-0.5, 0.5, 2.0,-0.5 // v4-v7-v6-v5 back
]);

// Normal
var normals = new Float32Array([
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, // v0-v1-v2-v3 front
1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, // v0-v3-v4-v5 right
0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, // v0-v5-v6-v1 up
-1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, // v1-v6-v7-v2 left
0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, // v7-v4-v3-v2 down
0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0 // v4-v7-v6-v5 back
]);

// Indices of the vertices
var indices = new Uint8Array([
0, 1, 2, 0, 2, 3, // front
4, 5, 6, 4, 6, 7, // right
8, 9,10, 8,10,11, // up
12,13,14, 12,14,15, // left
16,17,18, 16,18,19, // down
20,21,22, 20,22,23 // back
]);

// Write coords to buffers, but don't assign to attribute variables
g_baseBuffer = initArrayBufferForLaterUse(gl, vertices_base, 3, gl.FLOAT);
g_arm1Buffer = initArrayBufferForLaterUse(gl, vertices_arm1, 3, gl.FLOAT);
g_arm2Buffer = initArrayBufferForLaterUse(gl, vertices_arm2, 3, gl.FLOAT);
g_palmBuffer = initArrayBufferForLaterUse(gl, vertices_palm, 3, gl.FLOAT);
g_fingerBuffer = initArrayBufferForLaterUse(gl, vertices_finger, 3, gl.FLOAT);
if (!g_baseBuffer || !g_arm1Buffer || !g_arm2Buffer || !g_palmBuffer || !g_fingerBuffer) return -1;

// Write normals to a buffer, assign it to a_Normal and enable it
if (!initArrayBuffer(gl, 'a_Normal', normals, 3, gl.FLOAT)) return -1;

// Write the indices to the buffer object
var indexBuffer = gl.createBuffer();
if (!indexBuffer) {
console.log('Failed to create the buffer object');
return -1;
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

return indices.length;
}

function initArrayBufferForLaterUse(gl, data, num, type){
var buffer = gl.createBuffer(); // Create a buffer object
if (!buffer) {
console.log('Failed to create the buffer object');
return null;
}
// Write date into the buffer object
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

// Store the necessary information to assign the object to the attribute variable later
buffer.num = num;
buffer.type = type;

return buffer;
}

function initArrayBuffer(gl, attribute, data, num, type){
var buffer = gl.createBuffer(); // Create a buffer object
if (!buffer) {
console.log('Failed to create the buffer object');
return false;
}
// Write date into the buffer object
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

// Assign the buffer object to the attribute variable
var a_attribute = gl.getAttribLocation(gl.program, attribute);
if (a_attribute < 0) {
console.log('Failed to get the storage location of ' + attribute);
return false;
}
gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0);
// Enable the assignment of the buffer object to the attribute variable
gl.enableVertexAttribArray(a_attribute);

return true;
}


// Coordinate transformation matrix
var g_modelMatrix = new Matrix4(), g_mvpMatrix = new Matrix4();

function draw(gl, n, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix) {
// Clear color and depth buffer
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

// Draw a base
var baseHeight = 2.0;
g_modelMatrix.setTranslate(0.0, -12.0, 0.0);
drawSegment(gl, n, g_baseBuffer, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix);

// Arm1
var arm1Length = 10.0;
g_modelMatrix.translate(0.0, baseHeight, 0.0); // Move onto the base
g_modelMatrix.rotate(g_arm1Angle, 0.0, 1.0, 0.0); // Rotate around the y-axis
drawSegment(gl, n, g_arm1Buffer, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix); // Draw

// Arm2
var arm2Length = 10.0;
g_modelMatrix.translate(0.0, arm1Length, 0.0); // Move to joint1
g_modelMatrix.rotate(g_joint1Angle, 0.0, 0.0, 1.0); // Rotate around the z-axis
drawSegment(gl, n, g_arm2Buffer, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix); // Draw

// A palm
var palmLength = 2.0;
g_modelMatrix.translate(0.0, arm2Length, 0.0); // Move to palm
g_modelMatrix.rotate(g_joint2Angle, 0.0, 1.0, 0.0); // Rotate around the y-axis
drawSegment(gl, n, g_palmBuffer, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix); // Draw

// Move to the center of the tip of the palm
g_modelMatrix.translate(0.0, palmLength, 0.0);

// Draw finger1
pushMatrix(g_modelMatrix);
g_modelMatrix.translate(0.0, 0.0, 2.0);
g_modelMatrix.rotate(g_joint3Angle, 1.0, 0.0, 0.0); // Rotate around the x-axis
drawSegment(gl, n, g_fingerBuffer, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix);
g_modelMatrix = popMatrix();

// Finger2
g_modelMatrix.translate(0.0, 0.0, -2.0);
g_modelMatrix.rotate(-g_joint3Angle, 1.0, 0.0, 0.0); // Rotate around the x-axis
drawSegment(gl, n, g_fingerBuffer, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix);
}

var g_matrixStack = []; // Array for storing a matrix
function pushMatrix(m) { // Store the specified matrix to the array
var m2 = new Matrix4(m);
g_matrixStack.push(m2);
}

function popMatrix() { // Retrieve the matrix from the array
return g_matrixStack.pop();
}

var g_normalMatrix = new Matrix4(); // Coordinate transformation matrix for normals

// Draw segments
function drawSegment(gl, n, buffer, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// Assign the buffer object to the attribute variable
gl.vertexAttribPointer(a_Position, buffer.num, buffer.type, false, 0, 0);
// Enable the assignment of the buffer object to the attribute variable
gl.enableVertexAttribArray(a_Position);

// Calculate the model view project matrix and pass it to u_MvpMatrix
g_mvpMatrix.set(viewProjMatrix);
g_mvpMatrix.multiply(g_modelMatrix);
gl.uniformMatrix4fv(u_MvpMatrix, false, g_mvpMatrix.elements);
// Calculate matrix for normal and pass it to u_NormalMatrix
g_normalMatrix.setInverseOf(g_modelMatrix);
g_normalMatrix.transpose();
gl.uniformMatrix4fv(u_NormalMatrix, false, g_normalMatrix.elements);
// Draw
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
}

效果2

同上


Tips: Please indicate the source and original author when reprinting or quoting this article.