最近学习构建三维图形的时候,深感几何功底不够,一个视图变化矩阵看了几天也没想过来,只勉强理解原理,细节部分自己还需要加强学习
1. 视图变换
在二维图形绘制的时候,不用考虑z
轴,但是绘制的三维图形处于一个立体空间,就要使用z
轴了。由于存在z
轴,那么在物体观察的时候首先就是要确定观察坐标系
1.1 视点,观察点,正方向
观察坐标系可以通过定义:视点,观察点,正方向确定
视点:是眼睛所在的位置,可以认为是观察坐标系的原点
观察点:是观察对象所在位置,虽然观察对象是一个三维物体,但是物体都是有点组成,那么可以确认任何一个点作为观察点
视线:视点和观察点之间的连线
正方向:我的理解是观察坐标系下y
轴的正方向,也就是当人观察事物的时候,头顶的朝向
当确定好以上三个要素以后,就可以确定一个观察的坐标系了,为什么?
目前使用坐标系都是直角坐标系,也就是在y
轴和视线方向确定好以后,y
轴和视线组成了一个平面,那么x
轴需要和这个平面垂直,并且经过视点(因为默认视点为原点),之后z
轴需要和y
轴还有x
轴垂直,所以也就确定下来了。
在使用WebGL绘制点的时候都是使用的WebGL的坐标系,但是现在的视点并不一定是WebGL坐标系中的原点,所以这个时候,需要把视点转换为WebGL坐标系中的原点,才能正确的利用WebGL绘制图形,将视点转换为WebGL原点的且y
轴正方向和WebGL的y
轴重合的变换矩阵就是视图矩阵。
连续看了几天也没有特别明白这个转换矩阵是如何运作的,所以这里先暂时贴出公式(代码摘抄自WebGL编程指南)
Matrix4.prototype.setLookAt = function(eyeX, eyeY, eyeZ, centerX, centerY, centerZ, upX, upY, upZ) {
var e, fx, fy, fz, rlf, sx, sy, sz, rls, ux, uy, uz;
fx = centerX - eyeX;
fy = centerY - eyeY;
fz = centerZ - eyeZ;
// Normalize f.
rlf = 1 / Math.sqrt(fx*fx + fy*fy + fz*fz);
fx *= rlf;
fy *= rlf;
fz *= rlf;
// Calculate cross product of f and up.
sx = fy * upZ - fz * upY;
sy = fz * upX - fx * upZ;
sz = fx * upY - fy * upX;
// Normalize s.
rls = 1 / Math.sqrt(sx*sx + sy*sy + sz*sz);
sx *= rls;
sy *= rls;
sz *= rls;
// Calculate cross product of s and f.
ux = sy * fz - sz * fy;
uy = sz * fx - sx * fz;
uz = sx * fy - sy * fx;
// Set to this.
e = this.elements;
e[0] = sx;
e[1] = ux;
e[2] = -fx;
e[3] = 0;
e[4] = sy;
e[5] = uy;
e[6] = -fy;
e[7] = 0;
e[8] = sz;
e[9] = uz;
e[10] = -fz;
e[11] = 0;
e[12] = 0;
e[13] = 0;
e[14] = 0;
e[15] = 1;
// Translate.
return this.translate(-eyeX, -eyeY, -eyeZ);
};
Matrix4.prototype.translate = function(x, y, z) {
var e = this.elements;
e[12] += e[0] * x + e[4] * y + e[8] * z;
e[13] += e[1] * x + e[5] * y + e[9] * z;
e[14] += e[2] * x + e[6] * y + e[10] * z;
e[15] += e[3] * x + e[7] * y + e[11] * z;
return this;
};
PS:公式主要是将点进行了反向的旋转和平移变换
2. 可视范围
我们观察时候的坐标系是我们自定定义的尺度,而WebGL坐标系中坐标范围是(-1.0,1.0),一旦超出的坐标就不会绘制了,从而导致图形缺失。如果要让视野所见的所有内容都包含在坐标系中,就可能需要对坐标进行缩放和平移,来改变可视范围,常用的方式有两种
2.1 正射投影
长方体的可视范围,是一种盒状空间,用于建筑平面设计。利用近裁剪面和远裁剪面来确定可视区域,因此使用六个参数可以确定正射投影:left,right,top,bottom,near,far,具体正射投影矩阵构建公式如下:
Matrix4.prototype.setOrtho = function(left, right, bottom, top, near, far) {
var e, rw, rh, rd;
if (left === right || bottom === top || near === far) {
throw 'null frustum';
}
rw = 1 / (right - left);
rh = 1 / (top - bottom);
rd = 1 / (far - near);
e = this.elements;
e[0] = 2 * rw;
e[1] = 0;
e[2] = 0;
e[3] = 0;
e[4] = 0;
e[5] = 2 * rh;
e[6] = 0;
e[7] = 0;
e[8] = 0;
e[9] = 0;
e[10] = -2 * rd;
e[11] = 0;
e[12] = -(right + left) * rw;
e[13] = -(top + bottom) * rh;
e[14] = -(far + near) * rd;
e[15] = 1;
return this;
};
2.2 透视矩阵
透视矩阵是四棱锥的可视空间,一般用于游戏设计,符合现实场景(近大远小的效果)。也存在近裁剪面和远裁剪面,需要4个参数来确定相关变换矩阵:fovy(可视空间顶面和底面的夹角),aspect(裁剪面高宽比),near,far
Matrix4.prototype.setPerspective = function(fovy, aspect, near, far) {
var e, rd, s, ct;
if (near === far || aspect === 0) {
throw 'null frustum';
}
if (near <= 0) {
throw 'near <= 0';
}
if (far <= 0) {
throw 'far <= 0';
}
fovy = Math.PI * fovy / 180 / 2;
s = Math.sin(fovy);
if (s === 0) {
throw 'null frustum';
}
rd = 1 / (far - near);
ct = Math.cos(fovy) / s;
e = this.elements;
e[0] = ct / aspect;
e[1] = 0;
e[2] = 0;
e[3] = 0;
e[4] = 0;
e[5] = ct;
e[6] = 0;
e[7] = 0;
e[8] = 0;
e[9] = 0;
e[10] = -(far + near) * rd;
e[11] = -1;
e[12] = 0;
e[13] = 0;
e[14] = -2 * near * far * rd;
e[15] = 0;
return this;
};
3. 深度处理
3.1 隐藏面消除
WebGL默认深度的并不会对深度进行处理,会按照我们对点/面的绘制顺序进行绘制,也就最后绘制的内容会在最前面,违背了本来的意图,不过WebGL提供了对应的方法来处理深度关系:
首先,利用
gl.enable(gl.DEPTH_TEST)
开启隐藏面消除功能
然后,利用深度清理gl.clear(gl.DEPTH_BUFFER_BIT)
,可以让WebGL自己处理好深度关系
3.2 深度冲突
由于存在两个平面处在同一个深度的情况,这个时候WebGL绘制会出现深度冲突,表现为图形绘制结果看上去表面斑驳,于是WebGL提供了多边形偏移的功能,让即使深度一致的两个表面也会发生一定深度的偏移
首先,利用
gl.enable(gl.POLYGON_OFFSET_FILL)
开启多边形偏移功能
然后,利用gl.polygonOffset(1.0, 1.0)
来指定计算偏移量的参数
4. 绘制立方体
要绘制立方体,仍然可以使用gl.drawArrays()
,利用缓冲区数据来绘制表面,除此之外,为了高效利用图形中的坐标信息,可以使用gl.drawElements()
配合将序列放到gl.ELEMENT_BUFFER_ARRAY
来绘制表面。
具体做法如下:
function initPoint(gl, a_Position, a_Color) {
let pointData = new Float32Array([
0.0, 0.5, 0.0, 1.0, 0.0, 0.0,
-0.5, -0.5, 0.5, 1.0, 0.0, 0.0,
0.5, -0.5, 0.5, 1.0, 0.0, 0.0,
0.0, 0.5, 0.0, 0.0, 1.0, 0.0,
0.5, -0.5, 0.5, 0.0, 1.0, 0.0,
0.0, -0.5, -0.5, 0.0, 1.0, 0.0,
0.0, 0.5, 0.0, 0.0, 0.0, 1.0,
0.0, -0.5, -0.5, 0.0, 0.0, 1.0,
-0.5, -0.5, 0.5 ,0.0, 0.0, 1.0,
-0.5, -0.5, 0.5, 1.0, 0.0, 1.0,
0.0, -0.5, -0.5, 1.0, 0.0, 1.0,
0.5, -0.5, 0.5, 1.0, 0.0, 1.0
]);
// 利用的坐标序列
let indexData = new Uint8Array([
0, 1, 2,
3, 4, 5,
6, 7, 8,
9, 10, 11
])
let FSIZE = pointData.BYTES_PER_ELEMENT;
let buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, pointData, gl.STATIC_DRAW);
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 6*FSIZE, 0);
gl.enableVertexAttribArray(a_Position);
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, 6*FSIZE, 3*FSIZE);
gl.enableVertexAttribArray(a_Color);
// 将坐标序列信息存储到ELEMENT_ARRAY_BUFFER
let indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexData, gl.STATIC_DRAW);
}
5. 总结
构建三维图形相比二维图形来说,需要使用视图转换和可视范围的矩阵信息,因此坐标信息就变为了:gl_Position = 可视矩阵 X 视图矩阵 X 变换矩阵 X 原始坐标
,同时要利用深度规则消除深度影响,最后可以利用gl.drawElements()
,定义序列,复用坐标信息
6. 参考
《WebGL编程指南》