共计 3886 个字符,预计需要花费 10 分钟才能阅读完成。
引子
在 JavaScript WebGL 矩阵之后,发现在实现三维成果之前还有一些概念须要了解,就去查了下材料,依照本人的习惯整合了一下。
- Origin
- My GitHub
齐次坐标
三维坐标实践上三个重量就够了,但在看相干程序的时候,发现会呈现 4 个重量,这种示意形式称为 齐次坐标,它将一个本来 n 维向量用一个 n+1 维向量示意。比方向量 (x, y, z) 的齐次坐标可示意为 (x, y, z, w)。这样示意有利于应用矩阵运算将一个点集从一个坐标系转换到另一个坐标系。齐次坐标 (x, y, z, w) 等价于三维坐标 (x/w, y/w, z/w)。更具体介绍见这里。
空间转换
WeGL 没有现成 API 能够间接绘制出三维成果,须要进行一系列空间转换,最终在二维空间(比方电脑屏幕)显示,从视觉上看上去是三维平面成果。上面看看几个次要转换过程。
模型空间
模型空间 是形容三维物体本身的空间,领有本人的坐标系和对应原点。这里点坐标能够依照 WebGL 中可见范畴 [-1, +1] 束缚进行形容,也能够不依照这个束缚。自定义形容规定前面须要转换一下。
世界空间
物体模型创立好后,放在具体环境当中,才会达到想要的成果,除此之外,还可能会进行位移、缩放和旋转,进行这些变动都须要一个新参考坐标系,这个所处环境就是 世界空间。有了世界坐标系,互相独立的物体才会有绝对地位的形容。
从模型空间转换到世界空间,须要用到 模型矩阵(Model Matrix)。
三维模型矩阵跟 JavaScript WebGL 矩阵中介绍的二维变换矩阵相似,次要变动是行列减少和旋转。
const m4 = {translation: (x, y, z) => {
return [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
x, y, z, 1,
];
},
// 缩放矩阵
scaling: (x, y, z) => {
return [
x, 0, 0, 0,
0, y, 0, 0,
0, 0, z, 0,
0, 0, 0, 1,
];
},
// 旋转矩阵
xRotation: (angle) => {const c = Math.cos(angle);
const s = Math.sin(angle);
return [
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1,
];
},
yRotation: (angle) => {const c = Math.cos(angle);
const s = Math.sin(angle);
return [
c, 0, s, 0,
0, 1, 0, 0,
-s, 0, c, 0,
0, 0, 0, 1,
];
},
zRotation: (angle) => {const c = Math.cos(angle);
const s = Math.sin(angle);
return [
c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
];
},
}
视图空间
人眼在察看一个立方体时,从远处看和从近处看会有大小的差异,从右边看和从左边看会看到不同的面,在 WebGL 中绘制三维物体时,须要依据观察者的地位和方向,将物体放到正确的地位,观察者所处的空间就是 视图空间。
从世界空间转换到视图空间,须要用到 视图矩阵(View Matrix)。
为了形容观察者的状态,须要上面一些信息:
- 视点:观察者所在空间的地位,从这个地位沿着察看方向的射线为 眼帘。
- 察看指标点:被察看指标所在的点,视点和察看指标点独特决定了眼帘的方向。
- 上方向:最终绘制在屏幕上影像向上的方向,因为观察者是能够围绕眼帘进行旋转,所以须要一个具体的参考方向。
下面的三种信息独特形成视图矩阵,WebGL 中观察者的默认状态为:
- 视点:位于坐标零碎原点 (0, 0, 0)
- 察看指标点:眼帘是 z 轴负方向,观察点为 (0, 0, -1)
- 上方向:y 轴正方向,(0, 1, 0)
生成视图矩阵一种办法:
function setLookAt(eye, target, up) {const [eyeX, eyeY, eyeZ] = eye;
const [targetX, targetY, targetZ] = target;
const [upX, upY, upZ] = up;
let fx, fy, fz, sx, sy, sz, ux, uy, uz;
fx = targetX - eyeX;
fy = targetY - eyeY;
fz = targetZ - eyeZ;
// 单位化
const rlf = 1 / Math.sqrt(fx * fx + fy * fy + fz * fz);
fx *= rlf;
fy *= rlf;
fz *= rlf;
// f 与上向量的叉乘
sx = fy * upZ - fz * upY;
sy = fz * upX - fx * upZ;
sz = fx * upY - fy * upX;
// 单位化
const rls = 1 / Math.sqrt(sx * sx + sy * sy + sz * sz);
sx *= rls;
sy *= rls;
sz *= rls;
// s 和 f 的叉乘
ux = sy * fz - sz * fy;
uy = sz * fx - sx * fz;
uz = sx * fy - sy * fx;
const m12 = sx * -eyeX + sy * -eyeY + sz * -eyeZ;
const m13 = ux * -eyeX + uy * -eyeY + uz * -eyeZ;
const m14 = -fx * -eyeX + -fy * -eyeY + -fz * -eyeZ;
return [
sx, ux, -fx, 0,
sy, uy, -fy, 0,
sz, uz, -fz, 0,
m12,m13, m14, 1,
];
}
这里用到了叉乘,通过两个向量的叉乘,能够生成垂直于这两个向量的法向量,从而构建一个坐标系,也就是观察者所在的空间。
上面是三维有无自定义观察者的示例:
- 有自定义观察者
- 无自定义观察者
裁剪空间
基于下面的示例,旋转一下就会发现有局部角隐没的景象,这是因为超出了 WebGL 的可视范畴。
在 WebGL 程序中,顶点着色器会将点转换到一个称为 裁剪空间 的非凡坐标系上。延展到裁剪空间之外的任何数据都会被剪裁并且不会被渲染。
从视图空间转换到裁剪空间,须要用到 投影矩阵(Projection Matrix)。
可视域
人眼的察看范畴是无限的,WebGL 相似的限度了程度视角、垂直视角和可视深度,这些独特决定了 可视域(View Volume)。
有两类常见的可视域:
- 长方体 / 盒状可视域,由 正射投影 产生。
- 四棱锥 / 金字塔可视域,由 透视投影 产生。
正射投影能够不便的比拟场景中物体的大小,因为物体看上去的大小与所在位置没有关系,在修建立体等技术绘图相干场合,该当应用这种投影。透视投影让产生的场景看上去更有深度,更加天然。
正射投影
正射投影的可视域由前后两个矩形外表确定,别离称为 近裁剪面 和远裁剪面,近裁剪面和远裁剪面之间的空间就是可视域,只有在这个空间内的物体会被显示进去。正射投影下,近裁剪面和远裁剪面的尺寸是一样的。
正射投影矩阵实现的一种形式:
function setOrthographicProjection(config) {const [left, right, bottom, top, near, far] = config;
if (left === right || bottom === top || near === far) {throw "Invalid Projection";}
const rw = 1 / (right - left);
const rh = 1 / (top - bottom);
const rd = 1 / (far - near);
const m0 = 2 * rw;
const m5 = 2 * rh;
const m10 = -2 * rd;
const m12 = -(right + left) * rw;
const m13 = -(top + bottom) * rh;
const m14 = -(far + near) * rd;
return [
m0, 0, 0, 0,
0, m5, 0, 0,
0, 0, m10, 0,
m12, m13, m14, 1,
];
}
这是示例,通过扭转各个边界感触可视范畴变动。更加具体的原理解释见这里。
Canvas 上显示的就是物体在近裁剪面上的投影。如果裁剪面的宽高比和 Canvas 的不一样,画面就会依照 Canvas 的宽高比进行压缩,物体会被扭曲。
透视投影
透视投影的可视域产生跟正射投影的相似,比拟显著的区别就是 近裁剪面 和远裁剪面 尺寸不一样。
透视投影矩阵实现的一种形式:
/**
* 透视投影
* @param {*} config 程序 fovy, aspect, near, far
* fovy - 垂直视角,可是空间顶面和底面的夹角,必须大于 0
* aspect - 近裁剪面的宽高比(宽 / 高)* near - 近裁剪面的地位,必须大于 0
* far - 远裁剪面的地位,必须大于 0
*/
function setPerspectiveProjection(config) {let [fovy, aspect, near, far] = config;
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;
const s = Math.sin(fovy);
if (s === 0) {throw "null frustum";}
const rd = 1 / (far - near);
const ct = Math.cos(fovy) / s;
const m0 = ct / aspect;
const m5 = ct;
const m10 = -(far + near) * rd;
const m14 = -2 * near * far * rd;
return [
m0, 0, 0, 0,
0, m5, 0, 0,
0, 0, m10,-1,
0, 0, m14, 0,
];
}
这是模仿街道两边视角示例,更加具体的原理解释见这里。
参考资料
- WebGL 编程指南在线版
- WebGL model view projection
- WebGL 摄像机详解之一:模型、视图和投影矩阵变换的含意
- 坐标零碎