【笔记】《WebGL编程指南》学习-第9章层次模型(1-单关节模型)

我们已经知道如何平移、旋转简单的模型,比如二维的三角形或三维的立方体。但是,实际用到的很多三维模型,如3D游戏中的人物角色模型等,都是由多个小的立方体模型组成的。

绘制由多个小部件组成的复杂模型,最关键的问题是如何处理模型的整体移动,以及各个小部件间的相对移动。这一节就来研究这个问题。首先,考虑一下人类的手臂:从肩部到之间,包括上臂、前臂、手掌和手指,如下图所示:

这里写图片描述

手臂的每个部分可以围绕关节运动,如上图所示:

  • 上臂可以绕肩关节旋转运动,并带动前臂、手掌和手指一起运动。
  • 前臂可以绕肘关节运动,并带动手掌和手指一起运动,但不影响上臂。
  • 手掌绕腕关节运动,并带到手指一起运动,但不影响上臂和前臂。
  • 手指运动不影响手臂、前臂和手掌。

总之,当手臂的某个部位运动时,位于该部位以下的其他部位会随之一起运动,且位于该部位以上的其他部位不受影响。此外,这里的所有运动,都是围绕某个关节的转动。


层次结构模型

绘制机器人手臂这样一个复杂的模型,最常用的方法就是按照模型中各个部件的层次关系,从高到低逐一绘制,并在每个关节上应用模型矩阵。

注意,三维模型和现实中的人类或机器人不一样,它的部件并没有真正连接在一起。如果直接转动上臂,那么肘部以下的部分,包括前臂、手掌和手指,只会留在原地,这样手臂就断开了。所以,当上臂绕肩关节转动时,你需要在实现”肘部以下部分跟随上臂转动的逻辑。具体地,上臂绕肩关节转动了多少度,肘部以下部分也应该绕肩关节转动多少度。

当情况较为简单时,实现”部件A转动带动部件B转动“可以很直接,只要对部件B也施以部件A的旋转矩阵即可。比如,使用模型矩阵使上臂绕肩关节转动30度,然后在绘制肘关节以下的各部位时,为它们施加同一个模型矩阵,也令其绕肩关节转动30度,如下图所示。这样,肘关节以下的部分就能自动跟随上臂转动了。

这里写图片描述

如果情况更复杂一些,比如先使上臂绕肩关节转动30度,然后使前臂绕肘关节转动10度,那么对肘关节以下的部分,你就得先施加上臂绕肩关节转动30度的矩阵,然后再施加前臂绕肘关节转动10度的矩阵。将这两个矩阵相乘,其结果可称为”肘关节模型矩阵“,那么在绘制肘关节以下部分的时候,直接应用这个所谓的”肘关节模型矩阵“作为模型矩阵就可以了。

按照上述方式变成,三维场景中的肩关节就能影响肘关节,使得上臂的运动带动前臂的运动;反过来,不管前臂如何运动都不会影响上臂。这就与现实中的情况相符合了。

现在你已经对这种由多个小模型组成的复杂模型的运动规律有了一些了解,下面来看一下示例程序。


单关节模型

先来看一个单关节模型的例子。示例程序 JoinModel 绘制了一个仅由两个立方体部件组成的机器人手臂,其运行结果如下图左所示;手臂的两个部件为 arm1 与 arm2,arm1 接在 arm2 的上面,如图右所示。你可以把 arm1 想象成上臂,而把 arm2 想象成前臂,而肩关节在最下面。

这里写图片描述

运行程序,用户可以使用左右方向键控制 arm1 水平转动,使用上下方方向键控制 arm2 绕 joint1 关节垂直转动。比如,先按下方向键,arm2 逐渐向前倾斜,然后按右方向键,arm1 向右旋转。

这里写图片描述

如你所见,arm2 绕 joint1 的转动并不影响 arm1,而 arm1 的转动会带动 arm2 一起转动。

示例程序(JointModel.js)

JointModel.js

//顶点着色器程序
var VSHADER_SOURCE =
    'attribute vec4 a_Position;'+
    'attribute vec4 a_Normal;'+    //法向量
    'uniform mat4 u_MvpMatrix;'+
    'uniform mat4 u_NormalMatrix;\n' +
    'varying vec4 v_Color;'+
    'void main(){'+
    'gl_Position = u_MvpMatrix * a_Position;'+

    'vec3 lightDirection = normalize(vec3(0.0, 0.5, 0.7));' + // Light direction
    'vec4 color = vec4(1.0, 0.4, 0.0, 1.0);' +
    'vec3 ambientLight = vec3(0.2, 0.2, 0.2);'+
    //对法向量进行归一化
    'vec3 normal = normalize((u_NormalMatrix * a_Normal).xyz);'+
    //计算法向量和光线方向的点积
    'float nDotL = max(dot(normal, lightDirection), 0.0);'+
    //计算漫反射光的颜色
    'vec3 diffuse = vec3(color) * nDotL;'+
    //计算环境光产生的反射颜色
    'vec3 ambient = ambientLight * color.rgb;'+

    'v_Color = vec4(diffuse + ambient, color.a);'+
    '}';

//片元着色器程序
var FSHADER_SOURCE=
    '#ifdef GL_ES\n' +
    'precision mediump float;\n' +
    '#endif\n' +
    'varying vec4 v_Color;' +
    'void main() {'+
    'gl_FragColor = v_Color;'+
    '}';

function main() {
    //获取canvas元素
    var canvas = document.getElementById("webgl");
    if(!canvas){
        console.log("Failed to retrieve the <canvas> element");
        return;
    }

    //获取WebGL绘图上下文
    var gl = getWebGLContext(canvas);
    if(!gl){
        console.log("Failed to get the rendering context for WebGL");
        return;
    }

    //初始化着色器
    if(!initShaders(gl,VSHADER_SOURCE,FSHADER_SOURCE)){
        console.log("Failed to initialize shaders.");
        return;
    }

    //设置顶点位置
    var n = initVertexBuffers(gl);
    if (n < 0) {
        console.log('Failed to set the positions of the vertices');
        return;
    }

    //指定清空<canvas>颜色
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.enable(gl.DEPTH_TEST);

    //获取 u_MvpMatrix 、u_LightColor u_LightDirection u_AmbientLight 变量的存储位置

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

    //注册键盘事件
    document.onkeydown = function(ev){ keydown(ev, gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix); };

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

var ANGLE_STEP = 3.0;  //每次按键转动的角度
var g_arm1Angle = 90.0;  //arm1 当前角度
var g_jointAngle = 0.0;  //joint1 当前角度

function keydown(ev, gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix){
    switch (ev.keyCode){
        case 38:
            if(g_jointAngle < 135.0) g_jointAngle += ANGLE_STEP;
            break;
        case 40:
            if(g_jointAngle > -135.0) g_jointAngle -= ANGLE_STEP;
            break;
        case 39:
            g_arm1Angle = (g_arm1Angle + ANGLE_STEP) % 360;
            break;
        case 37:
            g_arm1Angle = (g_arm1Angle - ANGLE_STEP) % 360;
            break;
        default:
            return;
    }

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

function initVertexBuffers(gl) {
    //    v6----- v5
    //   /|      /|
    //  v1------v0|
    //  | |     | |
    //  | |     | |
    //  | |     | |
    //  | |v7---|-|v4
    //  |/      |/
    //  v2------v3

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

    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,
        1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,
        0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,
        -1.0, 0.0, 0.0,  -1.0, 0.0, 0.0,  -1.0, 0.0, 0.0,  -1.0, 0.0, 0.0,
        0.0,-1.0, 0.0,   0.0,-1.0, 0.0,   0.0,-1.0, 0.0,   0.0,-1.0, 0.0,
        0.0, 0.0,-1.0,   0.0, 0.0,-1.0,   0.0, 0.0,-1.0,   0.0, 0.0,-1.0
    ]);

    var indices = new Uint8Array([       // 顶点索引
        0, 1, 2,   0, 2, 3,
        4, 5, 6,   4, 6, 7,
        8, 9,10,   8,10,11,
        12,13,14,  12,14,15,
        16,17,18,  16,18,19,
        20,21,22,  20,22,23
    ]);

    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();
    if(!buffer){
        console.log("Failed to create thie buffer object");
        return -1;
    }

    //将缓冲区对象保存到目标上
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    //向缓存对象写入数据
    gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

    var a_attribute = gl.getAttribLocation(gl.program,attribute);
    if(a_attribute < 0){
        console.log("Failed to get the storage location of " + attribute);
        return -1;
    }

    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;
    g_modelMatrix.setTranslate(0.0, -12.0, 0.0);
    g_modelMatrix.rotate(g_arm1Angle, 0.0, 1.0, 0.0);
    drawBox(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix);

    //arm2
    g_modelMatrix.translate(0.0, arm1Length, 0.0);
    g_modelMatrix.rotate(g_jointAngle, 0.0, 0.0, 1.0);
    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) {
    g_mvpMatrix.set(viewProjMatrix);
    g_mvpMatrix.multiply(g_modelMatrix);
    gl.uniformMatrix4fv(u_MvpMatrix, false, g_mvpMatrix.elements);

    //计算法线变化矩阵
    g_normalMatrix.setInverseOf(g_modelMatrix);
    g_normalMatrix.transpose();
    gl.uniformMatrix4fv(u_NormalMatrix, false, g_normalMatrix.elements);

    //绘制
    gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
}

和以前的程序相比,main()函数基本没有变化,主要的变化发生在 initVertexBuffers()函数中,它将 arm1 和 arm2 的数据写入了相应的缓冲区。以前程序中的立方体都是以原点为中心,且边长为2.0;本例为了更好地模拟机器人手臂,使用如下图所示的立方体,原点位于底面中心,底面是边长为3.0的正方向,高度为10.0。将原点置于立方体的底面中心,是为了便于时立方体绕关节转动。arm1 和 arm2 都是用这个立方体。

这里写图片描述

main()函数首先根据可视空间,视点和视线方向计算出了视图投影矩阵 viewProjMatrix。

然后在键盘事件响应函数中调用 keydown()函数,通过方向键控制机器人的手臂运动。

 document.onkeydown = function(ev){ keydown(ev, gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix); };

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

接着定义 keydown()函数本身,以及若干该函数需要用到的全局变量。

var ANGLE_STEP = 3.0;  //每次按键转动的角度
var g_arm1Angle = 90.0;  //arm1 当前角度
var g_jointAngle = 0.0;  //joint1 当前角度

function keydown(ev, gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix){
    switch (ev.keyCode){
        case 38:
            if(g_jointAngle < 135.0) g_jointAngle += ANGLE_STEP;
            break;
        case 40:
            if(g_jointAngle > -135.0) g_jointAngle -= ANGLE_STEP;
            break;
        case 39:
            g_arm1Angle = (g_arm1Angle + ANGLE_STEP) % 360;
            break;
        case 37:
            g_arm1Angle = (g_arm1Angle - ANGLE_STEP) % 360;
            break;
        default:
            return;
    }

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

ANGLE_STEP 常量表示每一次按下按键,arm1 或 joint1 转动的角度,它的值是3/0。g_arm1Angle 变量表示 arm1 的当前角度,g_joint1Angle 变量表示 joint1 的当前角度。

这里写图片描述

keydown()函数的任务是,根据按下的是哪个案件,对 g_joint1Angle 或 g_arm1Angle 变量加上或减去常量 ANGLE_STEP 的值。注意,joint1 的转动角度只能在 -135度到 135度之间,这是为了不与 arm1 冲突。最后,draw()函数将整个机器人手臂绘制出来。

绘制层次模型(draw())

draw()函数的任务是绘制机器人手臂。注意,draw()函数和 drawBox()函数用到了全局变量 g_modelMatrix 和 g_mvpMatrix。

//坐标变换矩阵
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;
    g_modelMatrix.setTranslate(0.0, -12.0, 0.0);
    g_modelMatrix.rotate(g_arm1Angle, 0.0, 1.0, 0.0);
    drawBox(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix);

    //arm2
    g_modelMatrix.translate(0.0, arm1Length, 0.0);
    g_modelMatrix.rotate(g_jointAngle, 0.0, 0.0, 1.0);
    g_modelMatrix.scale(1.3, 1.0, 1.3);
    drawBox(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix);
}

如你所见,draw()函数内部调用了 drawBox()函数,每调用一次绘制一个部件,先绘制下方较细 arm1,再绘制上方较粗 arm2。

绘制单个部件的步骤是:

  1. 调用 setTranslate()或 translate()进行平移;
  2. 调用 rotate()进行旋转;
  3. 调用 drawBox()进行绘制。

绘制整个模型时,需要按照各部件的层次顺序,先 arm1 后 arm2,再执行第一步平移,第二步旋转,第三步绘制。

绘制 arm1 的步骤如下:首先在模型矩阵 g_modelMatrix 上调用 setTranslate()函数,使之平移(0.0, -12.0, 0.0)到稍下方位置;然后调用 rotate()函数,绕 y 轴旋转 g_arm1Angle 角度;最后调用 drawBox()函数绘制 arm1。

接着来绘制 arm2,它与 arm1 在 joint1 处链接。我们应当从该处上开始绘制 arm2。但是此时,模型矩阵还是处于绘制 arm1 的状态,所以得先调用 translate()函数沿 y 轴向上平移 arm1 的高度 arm1Length。注意这里调用的是 translate()而不是 setTranslate(),因为这次平移是在之前的基础上进行的。

    g_modelMatrix.translate(0.0, arm1Length, 0.0);
    g_modelMatrix.rotate(g_jointAngle, 0.0, 0.0, 1.0);
    g_modelMatrix.scale(1.3, 1.0, 1.3);
    drawBox(gl, n, viewProjMatrix, u_MvpMatrix, u_NormalMatrix);

然后,使用 g_joint1Angle 进行肘关节处的转动,并在 x 和 z 轴稍作拉伸,使前臂看上去粗一些,以便于上臂区分开。

这样以来,每当 keydown()函数更新了 g_joint1Angle 变量和 g_arm1Angle 变量的值,然后调用 draw()函数进行绘制时,就能绘制处最新状态的机器人手臂,arm1 的位置取决于 g_arm1Angle 变量,而 arm2 的位置取决于 g_joint1Angle 变量。

drawBox()函数的任务是绘制机器人手臂的某个立方体部件,如上臂或前臂。它首先计算模型视图投影矩阵,传递给 u_MvpMatrix 变量。然后根据模型矩阵计算法向量变换矩阵,传递给 u_NormalMatrix 变量,最后绘制立方体。

绘制层次模型的基本流程就是这样了。虽然本例只有两个立方体和一个链接关节,但是绘制更加复杂的模型,其原理与本节是一直的,要做的只是重复上述步骤而已。

运行:

这里写图片描述
这里写图片描述


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值