实现两个三维模型的独立运动,包括每个模型的转向、前进后退、缩放功能。此处方向均以运动模型作为参考,模型正面方向为前进,相反方向为后退,绕动物中轴线转向。
完整代码已在传送门给出,效果图如下:
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;
}
至此,模型的绘制与独立运动就完成了。
该程序的完整代码已在传送门给出,需要的朋友可自行下载。