层次模型就是由多个简单的部件组合成的复杂模型,有了层次模型,可以在三维场景中处理复杂模型,比如游戏角色、机器人、甚至是人类角色,其关键之处在于如何处理模型之间的变换关系。
本文将从以下几个方面介绍
- 有多个简单的部件组成的复杂模型
- 为复杂模型建立具有层次结构的三维模型
- 使用模型矩阵、模拟机器人手臂上的关节运动
- 研究initShader函数的实现,了解初始化着色器的内部实现
矩阵库Matrix
1 多个简单模型组成的复杂模型
图1.1
以图1.1的机器手臂为例,绘制机器人手臂这样一个复杂的模型,最常用的方法就是按照模型中各个部件的层次顺序,从高到低逐一绘制,并在每个关节上应用模型矩阵。比如,在图1.1中,肩关节肘关节、腕关节,指关节都有各自的旋转矩阵。
注意,三维模型和现实中的人类或机器人不一样,它的部件并没有真正连接在一起。如果直接转动上臂,那么肘部以下的部分,包括前臂、手掌和手指,只会留在原地,这样手臂就断开了。所以,当上臂绕肩关节转动时,你需要在代码中实现“肘部以下部分跟随上臂转动”的逻辑。具体地,上臂绕肩关节转动了多少度,肘部以下的部分也应该绕肩关节转动多少度。
图1.2
当情况较为简单时,实现“部件A转动带动部件B转动”可以很直接,只要对部件B也施以部件A的旋转矩阵即可。比如,使用模型矩阵使上臂绕肩关节转动30度,然后在绘制肘关节以下的各部位时,为它们施加同一个模型矩阵,也令其绕肩关节转动30度,如图1.2所示。这样,肘关节以下的部分就能自动跟随上臂转动了。
1.1 代码实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<style>
#webgl {
width: 600px;
height: 600px;
position: absolute;
top: calc(50% - 300px);
left: calc(50% - 300px);
background-color: black;
}
</style>
<body onload="main()">
<canvas id="webgl" width="600" height="600">
Please use a browser that supports "canvas"
</canvas>
<script src="./matrix.js"></script>
<script>
// 顶点着色器
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" +
" vec3 lightDirection = normalize(vec3(0.0, 0.5, 0.7));\n" +
" vec4 color = vec4(1.0, 0.4, 0.0, 1.0);\n" +
" 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";
// 片元着色器
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 createProgram(gl, vshader, fshader) {
var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
return program;
}
function initShaders(gl, vshader, fshader) {
var program = createProgram(gl, vshader, fshader);
gl.useProgram(program);
gl.program = program;
return true;
}
function loadShader(gl, type, source) {
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
return shader;
}
function main() {
var canvas = document.getElementById("webgl");
const gl = canvas.getContext("webgl");
// 初始化着色器
initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);
// 初始化buffer
var n = initVertexBuffers(gl);
// 清除canvas
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.enable(gl.DEPTH_TEST);
var u_MvpMatrix = gl.getUniformLocation(gl.program, "u_MvpMatrix");
var u_NormalMatrix = gl.getUniformLocation(
gl.program,
"u_NormalMatrix"
);
// 计算视图投影矩阵
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);
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 = 0.0; // joint1旋转角度
function keydown(ev, gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix) {
switch (ev.keyCode) {
case 38: // 向上箭头键->关节t1围绕z轴的正旋转
if (g_joint1Angle < 135.0) g_joint1Angle += ANGLE_STEP;
break;
case 40: // 向下箭头键-> 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;
default:
return; // 无有效动作时跳过绘图
}
// 绘制机械臂
draw(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix);
}
function initVertexBuffers(gl) {
// 顶点坐标: 长方体宽3.0,高10.0,长3.0,原点在底部的中心)
var vertices = new Float32Array([
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 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
]);
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
]);
if (!initArrayBuffer(gl, "a_Position", vertices, 3, gl.FLOAT))
return -1;
if (!initArrayBuffer(gl, "a_Normal", normals, 3, gl.FLOAT)) return -1;
var indexBuffer = gl.createBuffer();
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, num, type) {
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
var a_attribute = gl.getAttribLocation(gl.program, attribute);
gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0);
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);
// Arm1
var arm1Length = 10.0; // arm1长度
g_modelMatrix.setTranslate(0.0, -12.0, 0.0);
g_modelMatrix.rotate(g_arm1Angle, 0.0, 1.0, 0.0); // 绕y轴旋转
drawBox(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix);
// Arm2
g_modelMatrix.translate(0.0, arm1Length, 0.0); // 移动到关节1
g_modelMatrix.rotate(g_joint1Angle, 0.0, 0.0, 1.0); // 绕z轴旋转
g_modelMatrix.scale(1.3, 1.0, 1.3); //放大
drawBox(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix);
}
var g_normalMatrix = new Matrix4(); // 法线的坐标变换矩阵
// 绘制立方体
function drawBox(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix) {
// 计算模型视图项目矩阵并将其传递给u_MvpMatrix
g_mvpMatrix.set(viewProjMatrix);
g_mvpMatrix.multiply(g_modelMatrix);
gl.uniformMatrix4fv(u_MvpMatrix, false, g_mvpMatrix.elements);
// 计算法向变换矩阵并将其传递给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);
}
</script>
</body>
</html>
立方体
1.2 效果图
2 多节点模型
前面只是绘制了机器人手臂的上臂和前臂,该节绘制一个具有多个关节的完整的机器人手臂,包括基座 (base)、上臂(anm1),前臂(arm2)、手掌(palm),两根手指(fngerl& finger2),全部可以通过键盘来控制。arml和arm2的连接关节jointl 位于 arm1 顶部,arm2 和 palm 的连接关节joint2 位于 arm2 顶部,fngerl 和 fnger2 位于 palm 一端,如下图所示。
2.1 代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<style>
#webgl {
width: 600px;
height: 600px;
position: absolute;
top: calc(50% - 300px);
left: calc(50% - 300px);
background-color: black;
}
</style>
<body onload="main()">
<canvas id="webgl" width="600" height="600">
Please use a browser that supports "canvas"
</canvas>
<script src="./matrix.js"></script>
<script>
// 顶点着色器
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" +
" vec3 lightDirection = normalize(vec3(0.0, 0.5, 0.7));\n" +
" vec4 color = vec4(1.0, 0.4, 0.0, 1.0);\n" +
" 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";
// 片元着色器
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 createProgram(gl, vshader, fshader) {
var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
return program;
}
function initShaders(gl, vshader, fshader) {
var program = createProgram(gl, vshader, fshader);
gl.useProgram(program);
gl.program = program;
return true;
}
function loadShader(gl, type, source) {
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
return shader;
}
function main() {
var canvas = document.getElementById("webgl");
const gl = canvas.getContext("webgl");
// 初始化着色器
initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);
// 初始化buffer
var n = initVertexBuffers(gl);
// 清除canvas
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.enable(gl.DEPTH_TEST);
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"
);
// 计算视图投影矩阵
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);
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; // 角度增量
var g_arm1Angle = 90.0; // 前臂旋转角度
var g_joint1Angle = 45.0; // 关节t1的旋转角度
var g_joint2Angle = 0.0; // 关节t2的旋转角度
var g_joint3Angle = 0.0; // 关节t3的旋转角度
function keydown(ev, gl, o, viewProjMatrix, a_Position, 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'键->关节joint2的正旋转
g_joint2Angle = (g_joint2Angle + ANGLE_STEP) % 360;
break;
case 88: // 'x'键->关节joint2的负旋转
g_joint2Angle = (g_joint2Angle - ANGLE_STEP) % 360;
break;
case 86: // 'v'键->关节joint3的正旋转
if (g_joint3Angle < 60.0) g_joint3Angle = (g_joint3Angle + ANGLE_STEP) % 360;
break;
case 67: // 'c'键->关节joint3的负旋转
if (g_joint3Angle > -60.0) g_joint3Angle = (g_joint3Angle - ANGLE_STEP) % 360;
break;
default: return;
}
// Draw
draw(gl, o, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix);
}
var g_baseBuffer = null; // 基座 Buffer
var g_arm1Buffer = null; // 前臂 Buffer
var g_arm2Buffer = null; // 上臂 Buffer
var g_palmBuffer = null; // 手掌 Buffer
var g_fingerBuffer = null; // 手指 Buffer
function initVertexBuffers(gl) {
// 顶点坐标: 长方体宽3.0,高10.0,长3.0,原点在底部的中心)
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
]);
//将字符串写入缓冲区,但不给属性变量赋值
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 (!initArrayBuffer(gl, "a_Normal", normals, 3, gl.FLOAT)) return -1;
var indexBuffer = gl.createBuffer();
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();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
// 存储必要的信息,以便稍后将对象分配给属性变量
buffer.num = num;
buffer.type = type;
return buffer;
}
function initArrayBuffer(gl, attribute, data, num, type) {
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
var a_attribute = gl.getAttribLocation(gl.program, attribute);
gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0);
gl.enableVertexAttribArray(a_attribute);
return true;
}
// 坐标变换矩阵
var g_modelMatrix = new Matrix4(), g_mvpMatrix = new Matrix4();
function draw(gl, n, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix) {
// 清除颜色和深度缓冲区
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 绘制基座
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);
// 绘制前臂
var arm1Length = 10.0;
g_modelMatrix.translate(0.0, baseHeight, 0.0); // 移到底部
g_modelMatrix.rotate(g_arm1Angle, 0.0, 1.0, 0.0); // 绕y轴旋转
drawSegment(gl, n, g_arm1Buffer, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix); // Draw
// 绘制上臂
var arm2Length = 10.0;
g_modelMatrix.translate(0.0, arm1Length, 0.0); // 移动到关节1
g_modelMatrix.rotate(g_joint1Angle, 0.0, 0.0, 1.0); // 绕z轴旋转
drawSegment(gl, n, g_arm2Buffer, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix); // Draw
// 绘制手掌
var palmLength = 2.0;
g_modelMatrix.translate(0.0, arm2Length, 0.0); // 移动到手掌
g_modelMatrix.rotate(g_joint2Angle, 0.0, 1.0, 0.0); // 绕y轴旋转
drawSegment(gl, n, g_palmBuffer, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix); // Draw
// 移动到手掌尖端的中心
g_modelMatrix.translate(0.0, palmLength, 0.0);
// 绘制手指1
pushMatrix(g_modelMatrix);
g_modelMatrix.translate(0.0, 0.0, 2.0);
g_modelMatrix.rotate(g_joint3Angle, 1.0, 0.0, 0.0); // 绕x轴旋转
drawSegment(gl, n, g_fingerBuffer, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix);
g_modelMatrix = popMatrix();
// 绘制手指2
g_modelMatrix.translate(0.0, 0.0, -2.0);
g_modelMatrix.rotate(-g_joint3Angle, 1.0, 0.0, 0.0); // 绕z轴旋转
drawSegment(gl, n, g_fingerBuffer, viewProjMatrix, a_Position, 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(); // 法线的坐标变换矩阵
// 绘制部件
function drawSegment(gl, n, buffer, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// 将缓冲区对象分配给属性变量
gl.vertexAttribPointer(a_Position, buffer.num, buffer.type, false, 0, 0);
// 启用缓冲区对象对属性变量的赋值
gl.enableVertexAttribArray(a_Position);
// 计算模型视图投影矩阵并将其传递给u_MvpMatrix
g_mvpMatrix.set(viewProjMatrix);
g_mvpMatrix.multiply(g_modelMatrix);
gl.uniformMatrix4fv(u_MvpMatrix, false, g_mvpMatrix.elements);
// 计算矩阵的法线,并将其传递给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);
}
</script>
</body>
</html>
整个绘制流程是:基座 ----> 前臂----> 上臂----> 手掌----> 手指
g_modelMatrix,模型变换矩阵,会随着绘制流程,不断更改。
平移
首先看平移,所有部件的平移类似于搭积木一样,从原点开始,根据基座位置一点点累加,比如:
基座:最开始,基座下移12,基座高度是2;
上臂:要衔接基座,由于基座高度是2,所以需要在基座位置的基础之上,上移2,也就是说,对于上臂,相对于原点,需要下移10;
前臂:由于上臂高度10,所以前臂需要在前臂的基础之上移动10,也就是说相对于原点,不用移动。
后续的手指跟手指以此类推
旋转
旋转会影响到与之关联的部件,整个手臂涉及到的有肩关节、肘关节、手腕、手指这几个衔接处,
肩关节的旋转会影响到整个手臂,肘关节的旋转只会影响到肘关节以下部件,而手腕会影响到手掌和手指,手指的旋转之后影响到自己。从整个流程上看,所有部件共享同一个模型矩阵变量,但是这整个模型矩阵都是从整体到局部的变化,整体的变化会影响到局部。
虽然示例代码中的draw达到了预期效果,但是这种矩阵变换的方式,在部件很多的时候会增加心智负担,而且这种通过全局变量g_arm1Angle的方式来记录部件的状态,不利于管理,容易造成内存泄漏;此外,一旦层次过多,不易理解各部件之间的关系。
实际上这种部件与部件之间的关系可以抽象为父子关系,整个手臂可以抽象为一棵树形结构,示意图如下:
如何理解关节跟部件呢?
首先我们可以看部件,不同部件,包括上臂、前臂、手掌、手指,都是具体的独立部件(可以理解为一个独特的模型),这些模型各自独立。再看关节,关节自带变换矩阵,它可以包含其它部件,同时也包括关节,因此我们可以把关节理解为一个引用,它会引用一个extract,ref与extract的关系也可以理解为父子关系, 那么这个extract是什么呢?可以理解为一个容器,这个容器可以容纳任何对象,包括上臂、前臂、手掌、手指,以及ref等。所有的ref其实可以理解为一个独立的坐标系,我们在定义这个extract时,无需考虑extract内部件它在整个手臂中的变换矩阵,只是相对于(0,0,0)定义即可,变换矩阵的定义在关节,由关节来决定它相当于父类的相对位置。
由此,我们各个关节有各自的变换矩阵,如果我们想要转动整个手腕,只需给手腕的变换矩阵添加一个旋转角度即可,如果想要手指转动,只需给指关节的变换矩阵添加一个旋转角度即可。
如何渲染呢?
渲染从肩关节ref出发,从下往上渲染,深度优先遍历,ref自带变换矩阵,变换矩阵自下而上累乘,比如当渲染到肘关节时,获取到肘关节的渲染数据之后,再将渲染数据乘以肩关节变换矩阵。
2.2 效果图
3 initShander函数的作用
initshaders()函数的作用是,编译GLSLES代码,创建和初始化着色器供 WebGL使用。具体地,分为以下7个步骤:
- 创建着色器对象(gl.createshader())。
- 向着色器对象中填充着色器程序的源代码(gl.shadersource())。
- 编译将色器(gl.compileshader())。
- 创建程序对象(gl.createProgram())。
- 为程序对象分配着色器(gl.attachshader())。
- 连接程序对象(gl.1inkProgram())。
- 使用程序对象(gl.useProgram())。
虽然每一步看上去都比较简单,但是放在一起显得复杂了,我们将逐条讨论。首先,你需要知道这里出现了两种对象:着色器对象(shader object)和程序对象(programobject)。
着色器对象:着色器对象管理一个顶点着色器或一个片元着色器。每一个着色器都有一个着色器对象。
程序对象:程序对象是管理着色器对象的容器。WebGL中,一个程序对象必须包含一个顶点着色器和一个片元着色器。
3.1 gl.createshader(type)
| type | 指定创建的着色器类型,gl.VERTEX_SHADER表示顶点着色器,gl.FRAGMENT_SHADER表示片段着色器 |
|---|---|
| 返回值 | 创建的着色器 |
可以使用gl.deleteshader(shader)删除不需要的着色器
3.2 gl.shadersource()
| shader | 指定需要传入代码的着色器对象 |
|---|---|
| source | 指定字符串形式的代码 |
3.3 gl.compileshader()
| shader | 待编译着色器 |
|---|
向着色器对象传人源代码之后,还需要对其进行编译才能够使用。GLSLES语言和JavaScript 不同而更接近C或C++,在使用之前需要编译成二进制的可执行格式,WebGL系统真正使用的是这种可执行格式。使用g1.compileshader()函数进行编译注意,如果你通过调用g1.shadersource(),用新的代码替换掉了着色器中旧的代码,WebGL系统中的用旧的代码编译出的可执行部分不会被自动替换,你需要手动地重新进行编译。
3.4 gl.createProgram()
如前所述,程序对象包含了顶点着色器和片元着色器,可以调用gl.createProgram()来创建程序对象。
3.5 gl.attachshader()
WebGL系统要运行起来,必须要有两个着色器:一个顶点着色器和一个片元着色器可以使用gl.attachshader()函数为程序对象分配这两个着色器。
| program | 指定程序对象 |
|---|---|
| shader | 指定着色器对象 |
着色器在附给程序对象前,并不一定要为其指定代码或进行编译(也就是说,把空的着色器附给程序对象也是可以的)。类似地,可以使用g1.detachshader()函数来解除分配给程序对象的着色器。
3.6 gl.1inkProgram()
在为程序对象分配了两个着色器对象后,还需要将(顶点着色器和片元)着色器连接起来。使用gl.1inkProgram()所数来进行这一步操作。
程序对象进行着色器连接操作,目的是保证:
(1) 顶点着色器和片元着色器的varying 变量同名同类型,且一一对应;
(2) 顶点着色器对每个varying 变量赋了值;
(3) 顶点着色器和片元着色器中的同名uniform变量也是同类型的(无需一一对应,即某些uniform变量可以出现在一个着色器中而不出现在另一个中);
(4) 着色器中的 attzibute变量、uniform 变量和 varying 变量的个数没有超过着色器的上限,等等
在着色器连接之后,应当检查是否连接成功。通过调用g1.getProgramPara-meters()函数来实现。
如果程序已经成功连接,就得到了一个二进制的可执行模块供 WebGL 系统使用,如果连接失败了,也可以通过调用g1.getProgramInfoLog()从信息日志中获取连接出错信息。
3.7 gl.useProgram()
| program | 指定带使用的程序对象 |
|---|
这个函数的存在使得WebGL具有了一个强大的特性,那就是在绘制前准备多个程序对象,然后在绘制的时候根据需要切换程序对象。
这样,建立和初始化着色器的任务就算完成了。如你所见,initshaders()函数隐藏了大量的细节,我们可以放心地使用该函数来创建和初始化着色器,而不必考虑这些细节。本质上,在该函数顺利执行后,顶点着色器和片元着色器就已经就位了,只需要调用g1.drawrrays()或gl.drawElements()来使整个WcbGL系统运行起来。