1.变换
为了深入了解变换,我们首先要在讨论矩阵之前回顾一下最基础的数学背景知识.
1.1 向量
向量有一个方向和大小.
向量标量运算
向量加减运算
对应的直观表示加法:
直观表示减法:
向量长度
使用勾股定理即可
向量点乘
两个向量的点乘也叫向量的内积、数量积,对两个向量执行点乘运算,就是对这两个向量对应位一一相乘之后求和的操作
两个向量的点乘等于它们的数乘结果乘以两个向量之间夹角的余弦值
向量叉乘
叉乘只在3D空间中有定义,它需要两个不平行向量作为输入,生成一个正交于两个输入向量的第三个向量,如果输入的两个向量也是正交的,那么叉乘之后将会产生3个互相正交的向量.下面的图片展示了3D空间中叉乘的样子:
1.2 矩阵
标量运算
数乘运算
矩阵相乘
相乘还有一些限制:
1.只有当左侧矩阵的列数与右侧矩阵的行数相等,两个矩阵才能相乘.
2.矩阵相乘不遵守交换律(Commutative),也就是说A⋅B≠B⋅A.
1.3 矩阵与向量
单位矩阵
在OpenGL中,由于某些原因我们通常使用4×4的变换矩阵,其中最重要的原因就是大部分的向量都是4分量的.我们能想到的最简单的变换矩阵就是单位矩阵(Identity Matrix).单位矩阵是一个除了对角线以外都是0的N×N矩阵.在下式中可以看到,这种变换矩阵使一个向量完全不变:
缩放矩阵
对一个向量进行缩放(Scaling)就是对向量的长度进行缩放
位移矩阵
位移是在原始向量的基础上加上另一个向量从而获得一个在不同位置的新向量的过程
旋转矩阵
利用旋转矩阵我们可以把我们的位置向量沿一个单位轴进行旋转.也可以把多个矩阵结合起来,比如先沿着x轴旋转再沿着y轴旋转.但是这会很快导致一个问题——万向节死锁.避免万向节死锁的真正解决方案是使用四元数(Quaternion).
矩阵的组合
阵乘法是不遵守交换律的,这意味着它们的顺序很重要。当矩阵相乘时,在最右边的矩阵是第一个与向量相乘的,所以你应该从右向左读这个乘法。建议您在组合矩阵时,先进行缩放操作,然后是旋转,最后才是位移,否则它们会(消极地)互相影响。比如,如果你先位移再缩放,位移的向量也会同样被缩放(译注:比如向某方向移动2米,2米也许会被缩放成1米)!
2.坐标系统
所有顶点转换为片段之前,顶点需要处于的不同的状态,将对象的坐标转换到几个过渡坐标系,对我们来说比较重要的总共有5个不同的坐标系统.
- 局部空间(Local Space,或者称为物体空间(Object Space))
- 世界空间(World Space)
- 观察空间(View Space,或者称为视觉空间(Eye Space))
- 裁剪空间(Clip Space)
- 屏幕空间(Screen Space)
局部坐标是对象相对于局部原点的坐标,也是对象开始的坐标,将局部坐标转换为世界坐标,世界坐标是作为一个更大空间范围的坐标系统.这些坐标是相对于世界的原点的.接下来我们将世界坐标转换为观察坐标,观察坐标是指以摄像机或观察者的角度观察的坐标.在将坐标处理到观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标是处理-1.0到1.0范围内并判断哪些顶点将会出现在屏幕上,最后,我们需要将裁剪坐标转换为屏幕坐标.最后转换的坐标将会送到光栅器,由光栅器将其转化为片段.
其中用到的最重要的几个分别是模型(Model)、视图(View)、投影(Projection)三个矩阵
模型矩阵(Model Matrix)
模型矩阵将对象的坐标将会从局部坐标转换到世界坐标,对象进行平移、缩放、旋转来将它置于它本应该在的位置或方向.
观察矩阵(View Matrix)
观察矩阵将场景通过一系列的平移和旋转的组合使得特定的对象被转换到摄像机前面,观察空间就是从摄像机的角度观察到的空间.
投影矩阵(Projection Matrix)
投影矩阵将顶点坐标从观察空间转换到裁剪空间,所有的坐标都能落在一个给定的范围内,且任何在这个范围之外的点都应该被裁剪掉,投影矩阵创建的观察区域(Viewing Box)被称为平截头体(Frustum)
一个顶点的坐标将会根据以下过程被转换到裁剪坐标,每个矩阵被运算的顺序是从右往左乘:
3.绘制一个正方体
着色器代码为:
//顶点着色器
attribute vec4 a_position;
uniform mat4 u_worldViewProjection;
void main() {
gl_Position = u_worldViewProjection * a_position;
}
//颜色着色器
void main() {
gl_FragColor = vec4(1,1,1,1);
}
其中u_worldViewProjection是观察矩阵和投影矩阵的矩阵相乘
首先定义好正方体的顶点
// 正方体顶点
private positions: number[] = [
-1, -1, -1,
1, -1, -1,
1, 1, -1,
-1, 1, -1,
-1, -1, 1,
1, -1, 1,
1, 1, 1,
-1, 1, 1,
];
// 指定绘制顺序的索引数组
private indices: number[] = [
0, 1,
1, 2,
2, 3,
3, 0,
4, 5,
5, 6,
6, 7,
7, 4,
0, 4,
1, 5,
2, 6,
3, 7,
];
indices保存的是顶点的索引值,好处是减少vertex数据量,提高了性能.需要使用index策略来指明有效的顶点数据,而不用添加所有顶点数据,以免重复.
// 获取在着色器中声明的变量
let positionLoc = gl.getAttribLocation(program, "a_position");
this.worldViewProjectionLoc = gl.getUniformLocation(program, "u_worldViewProjection");
// 正方体顶点
let positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.positions), gl.STATIC_DRAW);
gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLoc);
// gl.ELEMENT_ARRAY_BUFFER 表示数据是索引
let indicesBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indicesBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(this.indices), gl.STATIC_DRAW);
// 每帧刷新
window.requestAnimationFrame(this.render.bind(this));
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画.该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行.
private render(clock: number) {
// 毫秒转秒
clock = clock / 1000;
let gl = this.gl;
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 投影矩阵
let filedOfView = 45 * Math.PI / 180;
let aspect = this.canvas.clientWidth / this.canvas.clientHeight;
let projection = ThreeDMath.perspective(filedOfView, aspect, 0.01, 500);
let radius = 5;
let eye = [
Math.sin(clock) * radius,
1,
Math.cos(clock) * radius,
];
let target = [0, 0, 0];
let up = [0, 1, 0];
// 视图矩阵
let view = ThreeDMath.lookAt(eye, target, up);
let worldViewProjection = ThreeDMath.multiplyMatrix(view, projection);
gl.uniformMatrix4fv(this.worldViewProjectionLoc, false, worldViewProjection);
// gl.drawElements 表示使用索引值来画
// 第一个参数还是表示要画的线,第二参数表示要画的索引的个数
// 第三个参数表示索引值的类型,我们用了 Uint16,所以类型是 gl.UNSIGNED_SHORT
// 最后一个参数是 offset 表示起始偏移
gl.drawElements(gl.LINES, this.indices.length, gl.UNSIGNED_SHORT, 0);
requestAnimationFrame(this.render.bind(this));
}
ThreeDMath是一个数学函数工具库.可以进行方便的矩阵运算.
ThreeDMath.lookAt用来计算视图矩阵
eye 视点:观察者所在位置在欧拉坐标系下的坐标
target 观察目标点:被观察的物体所在的点
up 上方向:单一视点和视线还不能唯一确定所渲染的图形(可能上也可能下),上方向用于确定竖直方向
ThreeDMath.perspective用来计算投影矩阵
public static perspective(angle: number, aspect: number, near: number, far: number): number[] {
let f = Math.tan(Math.PI * 0.5 - 0.5 * angle);
let rangeInv = 1.0 / (near - far);
return [
f / aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, (near + far) * rangeInv, -1,
0, 0, near * far * rangeInv * 2, 0
];
}