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

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

这里写图片描述

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

这里写图片描述


示例程序(MultiJointModel.js)

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

MultiJointModel.js(按键相应部分)

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:
            if(g_joint1Angle < 135.0) g_joint1Angle += ANGLE_STEP;
            break;
        case 38:
            if(g_joint1Angle > -135.0) g_joint1Angle -= ANGLE_STEP;
            break;
        case 39:
            g_arm1Angle = (g_arm1Angle + ANGLE_STEP) % 360;
            break;
        case 37:
            g_arm1Angle = (g_arm1Angle - ANGLE_STEP) % 360;
            break;
        case 90: // 'z'
            g_joint2Angle = (g_joint2Angle + ANGLE_STEP) % 360;
            break;
        case 88: // 'x'
            g_joint2Angle = (g_joint2Angle - ANGLE_STEP) % 360;
            break;
        case 86: // 'v'
            if (g_joint3Angle < 60.0)  g_joint3Angle = (g_joint3Angle + ANGLE_STEP) % 360;
            break;
        case 67: // 'c'
            if (g_joint3Angle > -60.0) g_joint3Angle = (g_joint3Angle - ANGLE_STEP) % 360;
            break;
        default: return;
    }

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

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

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

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

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

MultiJointModel.js(绘制模型部分)

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

    //arm2
    var arm2Length = 10.0;
    g_modelMatrix.translate(0.0, arm1Length, 0.0);
    g_modelMatrix.rotate(g_joint1Angle, 0.0, 0.0, 1.0);
    drawBox(gl, n, 4.0, arm2Length, 4.0, viewProjMatrix, u_MvpMatrix, u_NormalMatrix);

    //palm
    var palmLength = 2.0;
    g_modelMatrix.translate(0.0, arm2Length, 0.0);
    g_modelMatrix.rotate(g_joint2Angle, 0.0, 0.0, 1.0);
    drawBox(gl, n, 2.0, palmLength, 6.0, viewProjMatrix, u_MvpMatrix, u_NormalMatrix);

    //移动到palm一段的中点
    g_modelMatrix.translate(0.0, palmLength, 0.0);

    //finger1
    pushMatrix(g_modelMatrix);
    g_modelMatrix.translate(0.0, 0.0, 2.0);
    g_modelMatrix.rotate(g_joint3Angle, 1.0, 0.0, 0.0);
    drawBox(gl, n, 1.0, 2.0, 1.0, viewProjMatrix, 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);
    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();
}

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

  1. 平移
  2. 旋转
  3. 绘制

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

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

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

g_modelMatrix.translate(0.0, 0.0, 2.0);
    g_modelMatrix.rotate(g_joint3Angle, 1.0, 0.0, 0.0);
    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 来存储矩阵,前者向栈中压入一个矩阵,而后者从栈中取出一个。

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

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

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


绘制部件(drawBox())

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

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

参数 width、height 和 depth 分别表示待绘制部件的宽度、高度和深度。其他的参数与 JointMode.js 中无异:参数 viewMatrix 表示视图矩阵,u_MvpMatrix 表示模型视图投影矩阵,u_NormalMatrix 表示用来计算变换后的法向量矩阵,后两者被传给顶点着色器中相应的同名 uniform 变量。

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

var g_normalMatrix = new Matrix4();

function drawBox(gl, n, width, height, depth, viewProjMatrix, u_MvpMatrix, u_NormalMatrix) {
    pushMatrix(g_modelMatrix);

    g_modelMatrix.scale(width, height, depth);
    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);
    g_modelMatrix = popMatrix();
}

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

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


绘制部件(drawSegments())

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

MultiJointModel_segment.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 、a_Position u_NormalMatrix 变量的存储位置
    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");
        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, 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;  //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, a_Position, u_MvpMatrix, u_NormalMatrix){
    switch (ev.keyCode){
        case 40:
            if(g_joint1Angle < 135.0) g_joint1Angle += ANGLE_STEP;
            break;
        case 38:
            if(g_joint1Angle > -135.0) g_joint1Angle -= ANGLE_STEP;
            break;
        case 39:
            g_arm1Angle = (g_arm1Angle + ANGLE_STEP) % 360;
            break;
        case 37:
            g_arm1Angle = (g_arm1Angle - ANGLE_STEP) % 360;
            break;
        case 90: // 'z'
            g_joint2Angle = (g_joint2Angle + ANGLE_STEP) % 360;
            break;
        case 88: // 'x'
            g_joint2Angle = (g_joint2Angle - ANGLE_STEP) % 360;
            break;
        case 86: // 'v'
            if (g_joint3Angle < 60.0)  g_joint3Angle = (g_joint3Angle + ANGLE_STEP) % 360;
            break;
        case 67: // 'c'
            if (g_joint3Angle > -60.0) g_joint3Angle = (g_joint3Angle - ANGLE_STEP) % 360;
            break;
        default: return;
    }

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

var g_baseBuffer = null;     // base 缓冲区对象
var g_arm1Buffer = null;     // arm1 缓冲区对象
var g_arm2Buffer = null;     // arm2 缓冲区对象
var g_palmBuffer = null;     // palm 缓冲区对象
var g_fingerBuffer = null;   // fingers 缓冲区对象

function initVertexBuffers(gl) {
    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
    ]);

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

    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;

    if (!initArrayBuffer(gl, 'a_Normal', normals, 3, gl.FLOAT)) return -1;

    //创建缓冲区对象
    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();
    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, a_Position, 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);
    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);
    g_modelMatrix.rotate(g_arm1Angle, 0.0, 1.0, 0.0);
    drawSegment(gl, n, g_arm1Buffer, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix);

    //arm2
    var arm2Length = 10.0;
    g_modelMatrix.translate(0.0, arm1Length, 0.0);
    g_modelMatrix.rotate(g_joint1Angle, 0.0, 0.0, 1.0);
    drawSegment(gl, n, g_arm2Buffer, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix);

    //palm
    var palmLength = 2.0;
    g_modelMatrix.translate(0.0, arm2Length, 0.0);
    g_modelMatrix.rotate(g_joint2Angle, 0.0, 0.0, 1.0);
    drawSegment(gl, n, g_palmBuffer, viewProjMatrix, a_Position, u_MvpMatrix, u_NormalMatrix);

    //移动到palm一段的中点
    g_modelMatrix.translate(0.0, palmLength, 0.0);

    //finger1
    pushMatrix(g_modelMatrix);
    g_modelMatrix.translate(0.0, 0.0, 2.0);
    g_modelMatrix.rotate(g_joint3Angle, 1.0, 0.0, 0.0);
    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);
    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);

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

示例程序的关键点是:

  1. 为每个部件单独创建一个缓冲区,在其中存储顶点的坐标数据
  2. 绘制部件之前,将相应缓冲区对象分配给 a_Position 变量
  3. 开启 a_Position 变量并绘制该部件

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

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

function initArrayBufferForLatreUse(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;
}

initArrayBufferForLaterUse()函数首先创建了缓冲区对象,然后向其中写入数据。注意,函数并没有将缓冲区对象分配给 attribute 变量(gl.vertexAtteibPointer())或开启 attribute 变量(gl.enableVertexAttribArray()),这两个步骤将留到真正进行绘制之前再完成。另外,为了便于将缓冲区分配给 attribute 变量,我们手动为其添加了两个属性 num 和 type。

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

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

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

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

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

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

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

这里写图片描述

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值