WebGL实现两个模型的独立运动:旋转、前进后退、缩放

实现两个三维模型的独立运动,包括每个模型的转向、前进后退、缩放功能。此处方向均以运动模型作为参考,模型正面方向为前进,相反方向为后退,绕动物中轴线转向。

完整代码已在传送门给出,效果图如下:
效果图

html文件:

首先,在html文件中定义了顶点着色器和片元着色器:

<script id="vertex-shader" type="x-shader/x-vertex">

attribute  vec4 vPosition;
attribute  vec4 vColor;
varying vec4 fColor;

uniform mat4 viewMatrix;
uniform mat4 modelViewMatrix;

void main()
{
    fColor = vColor;
    gl_Position = viewMatrix * modelViewMatrix * vPosition;
    gl_Position.z = -gl_Position.z;
}
</script>

<script id="fragment-shader" type="x-shader/x-fragment">

precision mediump float;

varying vec4 fColor;

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

在顶点着色器中,viewMatrix矩阵为视图矩阵,用于调整视线的方向;modelViewMatrix矩阵为模型视图矩阵,用于控制模型的方向。

js文件

在js文件中,首先做好准备工作,声明了一系列全局变量,以供后续的使用。此处给出了海绵宝宝身体各部位(立方体)的长宽高大小。值得一提的是,这里我们可以看到两个变量,direct和direct2,它们用于记录两个模型的正面方向,用于模型执行前进后退操作时确定方向。

var canvas;
var gl;

var ms = 180; // 画圆的面数

// 海绵宝宝
var points = []; // 顶点容器
var colors = []; // 颜色容器
var vColor, vPosition;
var cBuffer, vBuffer; // 海绵宝宝的buffer
var numVertices = 36*9 + ms*3*2*3 + 12; // 海绵宝宝顶点个数
var modelViewMatrix = mat4(); // 当前变换矩阵
var modelViewMatrixLoc; // shader变量
var CubeTx = 0, CubeTy = 0, CubeTz = 0; //海绵宝宝平移量
var CubeRotateAngle = 0; //海绵宝宝旋转角度
var scalePercent = 0.5; // 缩放比例
var direct = vec4( 0.0, 0.0, 1.0, 1.0 ); // 当前正面方向

// 粉色海绵宝宝
var points2 = []; // 顶点容器
var colors2 = []; // 颜色容器
var vColor2, vPosition2;
var cBuffer2, vBuffer2; // 粉色海绵宝宝的buffer
var numVertices2 = 36*9 + ms*3*2*3 + 12; // 粉色海绵宝宝顶点个数
var CubeTx2 = 0, CubeTy2 = 0, CubeTz2 = 0; // 粉色海绵宝宝平移量
var CubeRotateAngle2 = 0; // 粉色海绵宝宝旋转角度
var scalePercent2 = 0.5; // 缩放比例
var direct2 = vec4( 0.0, 0.0, 1.0, 1.0 ); // 当前正面方向

var viewMatrixLoc; // 视图矩阵的存储地址
var viewMatrix; // 当前视图矩阵
var viewIndex = 0; // 视图编号

var body = vec3( 0.4, 0.45, 0.2 );
var cloth = vec3( 0.4, 0.05, 0.2 );
var pants = vec3( 0.4, 0.1, 0.2 );
var leg = vec3( 0.06, 0.25, 0.05 );
var shoe = vec3( 0.12, 0.05, 0.05 );

// 所有的备选颜色
var chooseColors = [
    vec4(1.0, 0.96, 0.30, 1.0), // 黄色
    vec4(1.0, 1.0, 1.0, 1.0), // 白色
    vec4(0.51, 0.33, 0.24, 1.0), // 褐色
    vec4(0.0, 0.0, 0.0, 1.0), // 黑色
    vec4(0.96, 0.64, 0.66, 1.0) // 粉色
];

下面进入页面加载完成后的init()函数部分。按照常规,此处获取了着色器中各个变量的地址、创建绑定缓冲区,做了一些初始化工作。
第9行:setPoints()函数的内容将在后续给出,此函数就是分别设置了两个模型的所有顶点位置及颜色,写入两个模型的points[]和colors[]数组中。由于此段代码比较长,将在最后给出。
第16-21行:设置了默认的照相机方向。lookAt()三个参数分别为:视点方向,视线方向,上方向。

	canvas = document.getElementById( "gl-canvas" );

    gl = WebGLUtils.setupWebGL( canvas, null );
    if ( !gl ) { alert( "WebGL isn't available" ); }

    gl.viewport( 0, 0, canvas.width, canvas.height );
    gl.clearColor( 0.91, 0.92, 0.93, 1.0 ); // 灰色背景色

    setPoints(); // 设置所有顶点位置及颜色
    gl.enable(gl.DEPTH_TEST); // 消除隐藏面

    // 初始化着色器
    var program = initShaders( gl, "vertex-shader", "fragment-shader" );
    gl.useProgram( program );

    // 获取viewMatrix变量的存储地址
    viewMatrixLoc = gl.getUniformLocation(program, 'viewMatrix');
    // 设置视点、视线和上方向
    viewMatrix = lookAt(vec3(0, 0, 0), vec3(0, 0, 0), vec3(0, 1, 0));
    // 将视图矩阵传递给viewMatrix变量
    gl.uniformMatrix4fv(viewMatrixLoc, false, flatten(viewMatrix));

    // 创建缓冲区,并向缓冲区写入立方体每个面的颜色信息
    cBuffer = gl.createBuffer();
    gl.bindBuffer( gl.ARRAY_BUFFER, cBuffer );
    gl.bufferData( gl.ARRAY_BUFFER, flatten(colors), gl.STATIC_DRAW );
    //获取着色器中vColor变量,并向其传递数据
    vColor = gl.getAttribLocation( program, "vColor" );
    gl.enableVertexAttribArray( vColor );

    cBuffer2 = gl.createBuffer();
    gl.bindBuffer( gl.ARRAY_BUFFER, cBuffer2 );
    gl.bufferData( gl.ARRAY_BUFFER, flatten(colors2), gl.STATIC_DRAW );
    //获取着色器中vColor变量,并向其传递数据
    vColor2 = gl.getAttribLocation( program, "vColor" );
    gl.enableVertexAttribArray( vColor2 );

    // 创建缓冲区,并向缓冲区写入立方体的顶点信息
    vBuffer = gl.createBuffer();
    gl.bindBuffer( gl.ARRAY_BUFFER, vBuffer );
    gl.bufferData( gl.ARRAY_BUFFER, flatten(points), gl.STATIC_DRAW );
    // 获取着色器中vPosition变量,并向其传递数据
    vPosition = gl.getAttribLocation( program, "vPosition" );
    gl.enableVertexAttribArray( vPosition );

    vBuffer2 = gl.createBuffer();
    gl.bindBuffer( gl.ARRAY_BUFFER, vBuffer2 );
    gl.bufferData( gl.ARRAY_BUFFER, flatten(points2), gl.STATIC_DRAW );
    // 获取着色器中vPosition变量,并向其传递数据
    vPosition2 = gl.getAttribLocation( program, "vPosition" );
    gl.enableVertexAttribArray( vPosition2 );

    modelViewMatrixLoc = gl.getUniformLocation(program, 'modelViewMatrix');

init()函数中,添加对按钮点击事件的监听。
调整视图按钮中,viewIndex是一个标志位,表示当前视图的编号,由于只给出了两个视图,因此编号只有0和1。在该函数中,我们通过标志位的不同,重新设置了照相机的方向,并传递给顶点着色器。
前进、后退按钮:使用表示模型正面方向的direct向量来对模型的x、y、z坐标的值进行修改。
旋转按钮:修改了模型旋转角度的大小。
缩放按钮:修改了模型缩放比例(初始比例为0.5)。
最后调用render()函数进行模型的绘制。至此,init()函数结束。

	//event listeners for buttons
    document.getElementById("adjustView").onclick = function() {
        if (viewIndex === 0) {
            viewIndex = 1;
            // 设置视点、视线和上方向
            viewMatrix = lookAt(vec3(0.10, 0.15, 0.15), vec3(0, 0, 0), vec3(0, 1, 0));
            // 将视图矩阵传递给viewMatrix变量
            gl.uniformMatrix4fv(viewMatrixLoc, false, flatten(viewMatrix));
        } else if (viewIndex === 1) {
            viewIndex = 0;
            // 设置视点、视线和上方向
            viewMatrix = lookAt(vec3(0, 0, 0), vec3(0, 0, 0), vec3(0, 1, 0));
            // 将视图矩阵传递给viewMatrix变量
            gl.uniformMatrix4fv(viewMatrixLoc, false, flatten(viewMatrix));
        }
    };
    // 海绵宝宝
    document.getElementById("cubeForward").onclick = function() {
        CubeTx += 0.1 * direct[0];
        CubeTy += 0.1 * direct[1];
        CubeTz += 0.1 * direct[2];
    };
    document.getElementById("cubeBack").onclick = function() {
        CubeTx -= 0.1 * direct[0];
        CubeTy -= 0.1 * direct[1];
        CubeTz -= 0.1 * direct[2];
    };
    document.getElementById("cubeR1").onclick = function() {
        CubeRotateAngle -= 5;
    };
    document.getElementById("cubeR2").onclick = function() {
        CubeRotateAngle += 5;
    };
    document.getElementById("small").onclick = function() {
        scalePercent -= 0.05;
    };
    document.getElementById("big").onclick = function() {
        scalePercent += 0.05;
    };

    // 粉色海绵宝宝
    document.getElementById("cubeForward2").onclick = function() {
        CubeTx2 += 0.1 * direct2[0];
        CubeTy2 += 0.1 * direct2[1];
        CubeTz2 += 0.1 * direct2[2];
    };
    document.getElementById("cubeBack2").onclick = function() {
        CubeTx2 -= 0.1 * direct2[0];
        CubeTy2 -= 0.1 * direct2[1];
        CubeTz2 -= 0.1 * direct2[2];
    };
    document.getElementById("cubeR12").onclick = function() {
        CubeRotateAngle2 -= 5;
    };
    document.getElementById("cubeR22").onclick = function() {
        CubeRotateAngle2 += 5;
    };
    document.getElementById("small2").onclick = function() {
        scalePercent2 -= 0.05;
    };
    document.getElementById("big2").onclick = function() {
        scalePercent2 += 0.05;
    };

    render();

下面进入render()函数。
首先,给出了四个矩阵init、S、T、R,分别用于设置模型的初始位置、缩放矩阵、平移矩阵、旋转矩阵。旋转矩阵调用的是沿Y轴旋转的函数,因为此处我们做的是沿模型中轴线旋转。
接着,将四个矩阵做乘,把结果赋给modelViewMatrix变量。此处需注意四个矩阵相乘的顺序,按照代码,modelViewMatrix = init * T * R * S。越靠右边的矩阵,越先作用于模型上,因此越右边的矩阵就在嵌套mult函数的最外层。
下面,定义了一个矩阵m,该矩阵是用来作用于direct变量的,它与direct相乘后,模型正面方向做相应修改。由于init矩阵仅是用于设置模型初始位置,使得两个模型不重合在一起,因此矩阵m不需要乘上init矩阵。(此处使用的矩阵作用于向量的函数由于给定库中没有,是自己写的,将在后文给出)。
最后,给着色器传入顶点位置和颜色,使用gl.drawArrays(gl.TRIANGLES, 0, numVertices)完成绘制。
在render()函数最后,requestAnimFrame(render)表示让浏览器在合适的时候自行调用render函数,此处使用了递归,使得界面一直绘制下去。

function render()
{
    gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    // 海绵宝宝变换
    var init = translate(-0.3, 0, 0); // 初始变换矩阵,用于设置模型的初始位置
    var S = scalem(scalePercent, scalePercent, scalePercent);
    var T = translate(CubeTx, CubeTy, CubeTz);
    var R = rotateY(CubeRotateAngle);

    modelViewMatrix = mult(mult(mult(init, T), R), S);
    var m = mult(mult(T, R), S); // 用于处理正面的方向

    // 记录正面的方向
    direct = vec4( 0.0, 0.0, 1.0, 1.0 ); // 初始化初始方向
    direct = multMat4Vec4(m, direct);

    gl.uniformMatrix4fv(modelViewMatrixLoc, false, flatten(modelViewMatrix));

    // 海绵宝宝颜色
    gl.bindBuffer(gl.ARRAY_BUFFER, cBuffer);
    gl.vertexAttribPointer(vColor, 4, gl.FLOAT, false, 0, 0);
    // 海绵宝宝顶点
    gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer);
    gl.vertexAttribPointer(vPosition, 4, gl.FLOAT, false, 0, 0);

    gl.drawArrays(gl.TRIANGLES, 0, numVertices);


    // 粉色海绵宝宝变换
    init = translate(0.3, 0, 0); // 初始变换矩阵,用于设置模型的初始位置
    S = scalem(scalePercent2, scalePercent2, scalePercent2);
    T = translate(CubeTx2, CubeTy2, CubeTz2);
    R = rotateY(CubeRotateAngle2);

    modelViewMatrix = mult(mult(mult(init, T), R), S);
    m = mult(mult(T, R), S);

    // 记录正面的方向
    direct2 = vec4( 0.0, 0.0, 1.0, 1.0 ); // 初始化初始方向
    direct2 = multMat4Vec4(m, direct2);

    gl.uniformMatrix4fv(modelViewMatrixLoc, false, flatten(modelViewMatrix));

    // 粉色海绵宝宝颜色
    gl.bindBuffer(gl.ARRAY_BUFFER, cBuffer2);
    gl.vertexAttribPointer(vColor2, 4, gl.FLOAT, false, 0, 0);
    // 海绵宝宝顶点
    gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer2);
    gl.vertexAttribPointer(vPosition2, 4, gl.FLOAT, false, 0, 0);

    gl.drawArrays(gl.TRIANGLES, 0, numVertices2);

    requestAnimFrame(render);
}

上文用到的矩阵和向量乘积的multMat4Vec4()函数定义如下:

// 计算矩阵作用于向量的结果,mat4 * vec4
function multMat4Vec4(mat4, vector) {
    var newVec = [];
    for (var i = 0; i < 4; i++) {
        newVec.push(mat4[i][0] * vector[0] +
            mat4[i][1] * vector[1] +
            mat4[i][2] * vector[2] +
            mat4[i][3] * vector[3]);
    }
    return newVec;
}

最后,进入setPoints()函数,此函数对两个模型的顶点位置及颜色进行了设置。由于该部分代码过于冗长,此处只给出部分。
在模型脸部对五官进行绘制时,由于进行的是平面绘制,因此绘制的z轴值的不同决定了覆盖的不同(同一位置处,z值大的会覆盖z值小的)。

function setPoints() {
    // 画第一个海绵宝宝
    drawMouse(points, colors, 0);

    drawBody(0, 1, 2, 3, 0, points, colors); // 身体的第一个面,黄色
    drawBody(0, 3, 7, 4, 0, points, colors); // 身体的第二个面,黄色
    drawBody(4, 5, 6, 7, 0, points, colors); // 身体的第三个面,黄色
    drawBody(1, 5, 6, 2, 0, points, colors); // 身体的第四个面,黄色
    drawBody(0, 4, 5, 1, 0, points, colors); // 身体的第五个面,黄色
    drawBody(3, 7, 6, 2, 0, points, colors); // 身体的第六个面,黄色

    drawCloth(0, 1, 2, 3, 1, points, colors); // 衣服的第一个面,白色
    drawCloth(0, 3, 7, 4, 1, points, colors); // 衣服的第二个面,白色
    drawCloth(4, 5, 6, 7, 1, points, colors); // 衣服的第三个面,白色
    drawCloth(1, 5, 6, 2, 1, points, colors); // 衣服的第四个面,白色
    drawCloth(0, 4, 5, 1, 1, points, colors); // 衣服的第五个面,白色
    drawCloth(3, 7, 6, 2, 1, points, colors); // 衣服的第六个面,白色

    ……

    drawLeftEye(points, colors);
    drawRightEye(points, colors);
    drawTeeth(points, colors);
}

// 绘制身体
function drawBody(a, b, c, d, colorIndex, points, colors) {
    // 身体的八个顶点(x,y,z,a)
    var bodyVertices = [
        vec4(-body[0]/2, body[1]*2/3, body[2]/2, 1.0),
        vec4(body[0]/2, body[1]*2/3, body[2]/2, 1.0),
        vec4(body[0]/2, -body[1]/3, body[2]/2, 1.0),
        vec4(-body[0]/2, -body[1]/3, body[2]/2, 1.0),
        vec4(-body[0]/2, body[1]*2/3, -body[2]/2, 1.0),
        vec4(body[0]/2, body[1]*2/3, -body[2]/2, 1.0),
        vec4(body[0]/2, -body[1]/3, -body[2]/2, 1.0),
        vec4(-body[0]/2, -body[1]/3, -body[2]/2, 1.0)
    ];
    var indices = [ a, b, c, a, c, d ]; // 顶点索引顺序
    // 存取顶点余顶点索引信息算法
    for ( var i = 0; i < indices.length; i++ ) {
        points.push(bodyVertices[indices[i]]);
        colors.push(chooseColors[colorIndex]);
    }
}

// 绘制衣服
function drawCloth(a, b, c, d, colorIndex, points, colors) {
    // 衣服的八个顶点(x,y,z,a)
    var clothVertices = [
        vec4(-cloth[0]/2, -body[1]/3, cloth[2]/2, 1.0),
        vec4(cloth[0]/2, -body[1]/3, cloth[2]/2, 1.0),
        vec4(cloth[0]/2, -body[1]/3 - cloth[1], cloth[2]/2, 1.0),
        vec4(-cloth[0]/2, -body[1]/3 - cloth[1], cloth[2]/2, 1.0),
        vec4(-cloth[0]/2, -body[1]/3, -cloth[2]/2, 1.0),
        vec4(cloth[0]/2, -body[1]/3, -cloth[2]/2, 1.0),
        vec4(cloth[0]/2, -body[1]/3 - cloth[1], -cloth[2]/2, 1.0),
        vec4(-cloth[0]/2, -body[1]/3 - cloth[1], -cloth[2]/2, 1.0)
    ];
    var indices = [ a, b, c, a, c, d ]; // 顶点索引顺序
    // 存取顶点余顶点索引信息算法
    for ( var i = 0; i < indices.length; i++ ) {
        points.push(clothVertices[indices[i]]);
        colors.push(chooseColors[colorIndex]);
    }
}

// 画左眼
function drawLeftEye(points, colors) {
    // 画眼白
    var leftEyeVertices = getCircleVertex(-0.08, 0.15, 0.103, 0.06, ms, 360, 0);
    for (var i = 0; i < leftEyeVertices.length; i++) {
        points.push(leftEyeVertices[i]);
        colors.push(chooseColors[1]); // 白色
    }
    // 画眼球
    leftEyeVertices = getCircleVertex(-0.06, 0.15, 0.104, 0.02, ms, 360, 0);
    for (var i = 0; i < leftEyeVertices.length; i++) {
        points.push(leftEyeVertices[i]);
        colors.push(chooseColors[3]); // 黑色
    }
}

// 画右眼
function drawRightEye(points, colors) {
    var rightEyeVertices = getCircleVertex(0.08, 0.15, 0.103, 0.06, ms, 360, 0);
    for (var i = 0; i < rightEyeVertices.length; i++) {
        points.push(rightEyeVertices[i]);
        colors.push(chooseColors[1]); // 白色
    }
    var rightEyeVertices = getCircleVertex(0.06, 0.15, 0.104, 0.02, ms, 360, 0);
    for (var i = 0; i < rightEyeVertices.length; i++) {
        points.push(rightEyeVertices[i]);
        colors.push(chooseColors[3]); // 黑色
    }
}

// 画嘴巴
function drawMouse(points, colors, colorIndex) {
    var mouseVertices = getCircleVertex(0.0, 0.24, 0.1019, 0.21, ms, 80, 140);
    for (var i = 0; i < mouseVertices.length; i++) {
        points.push(mouseVertices[i]);
        colors.push(chooseColors[3]); // 黑色
    }
    mouseVertices = getCircleVertex(0.0, 0.24, 0.102, 0.205, ms, 80, 140);
    for (var i = 0; i < mouseVertices.length; i++) {
        points.push(mouseVertices[i]);
        colors.push(chooseColors[colorIndex]); // 黄色
    }
}

// 画牙齿
function drawTeeth(points, colors) {
    // 左牙
    points.push(vec4(-0.05, 0.036, 0.102, 1.0));
    points.push(vec4(-0.02, 0.032, 0.102, 1.0));
    points.push(vec4(-0.05, 0.01, 0.102, 1.0));
    points.push(vec4(-0.02, 0.032, 0.102, 1.0));
    points.push(vec4(-0.05, 0.01, 0.102, 1.0));
    points.push(vec4(-0.02, 0.005, 0.102, 1.0));

    // 右牙
    points.push(vec4(0.02, 0.032, 0.102, 1.0));
    points.push(vec4(0.05, 0.036, 0.102, 1.0));
    points.push(vec4(0.02, 0.005, 0.102, 1.0));
    points.push(vec4(0.05, 0.036, 0.102, 1.0));
    points.push(vec4(0.02, 0.005, 0.102, 1.0));
    points.push(vec4(0.05, 0.01, 0.102, 1.0));

    // 设置牙齿颜色
    for (var i = 0; i < 12; i++) {
        colors.push(chooseColors[1]); // 白色
    }
}

// 画圆
// 半径r 面数m 度数c 偏移量offset
function getCircleVertex(x, y, z, r, m, c, offset) {
    var arr = [];
    var addAng = c / m;
    var angle = 0;
    for (var i = 0; i < m; i++) {
        arr.push(vec4(x + Math.sin(Math.PI / 180 * (angle+offset)) * r, y + Math.cos(Math.PI / 180 * (angle+offset)) * r, z, 1.0));
        arr.push(vec4(x, y, z, 1.0));
        angle = angle + addAng;
        arr.push(vec4(x + Math.sin(Math.PI / 180 * (angle+offset)) * r, y + Math.cos(Math.PI / 180 * (angle+offset)) * r, z, 1.0));
    }
    return arr;
}

至此,模型的绘制与独立运动就完成了。
该程序的完整代码已在传送门给出,需要的朋友可自行下载。

猜你喜欢

转载自blog.csdn.net/qq_40121502/article/details/84075560