3D数学基础图形与游戏研发笔记

3D数学基础图形与游戏研发笔记

本文所有公式都是在右手系下的,也就是v’ = M * v。如果换成左手系,那么根据矩阵转置公式v’(T) = v(T) * M(T)可得,需要将矩阵M进行一次转置并将左乘改为右乘,正角度也需要从逆时针改为顺时针

1. 向量

1.1 向量点乘

也叫点积,数量积,结果是一个数量

几何形式:a · b = ||a|| * ||b|| * cosθ

代数形式:

代数表示和坐标表示

图形学意义:

  • 可以计算一个向量在另一个向量上的投影
  • 通过计算夹角θ可以判断两个向量是否同向,如果点乘结果<0,或者θ>90°,或者cosθ<0,那么我们说两个向量反向
  • 在聚光灯的效果计算中,可以根据点积来得到光照效果,如果点积越大,说明夹角越小,则物体离光照的轴线越近,光照越强

1.2 向量叉乘

结果是一个向量

几何形式:||a * b|| = ||a|| * ||b|| * sinθ,其模长为如下平行四边形的面积

几何形式

代数形式:

代数形式

图形学意义:

  • 对于两个向量,叉乘的结果实际上是两个向量组成的平面的法线方向,方向满足右手定则
  • 可以判断左右,对于a, b, c三个点,判断c点是否在线段ab的左边,只需要作向量ab, ac, 做叉积,右手定则结果为正(指向屏幕外面)则在左边
  • 进一步可以用来判断内外/前后,对于三角形abc三个顶点外加三角形内部一个d点,如果d点在三个顺时针向量ab, bc, ca的左边,则d在三角形abc内部
判断点在某条边的左右以及三角形的内外

2. 矩阵

矩阵相乘不满足交换律

很多时候,我们更加习惯于将向量看成是n * 1的矩阵,而且经常会将向量和矩阵混合运算(比如将矩阵和向量做一些乘法等运算)

2.1 向量点乘和叉乘的矩阵形式

如上所说,不管是为了方便混合运算也好或者是为了代码实现更加统一也好,向量点乘和叉乘也可以统一写成“矩阵乘法”形式

向量点乘和叉乘的矩阵形式

叉乘的a变成的这个矩阵称为a的对偶矩阵


3. 线性变换

使用一些矩阵左乘列向量在图形学上通常有一些特殊的意义,比如旋转,平移等变换,需要知道一些特殊的变换(以2D坐标为例)

3.1 缩放变换

缩放变换矩阵

3.2 镜像变换

镜像变换矩阵

3.3 切变变换

切变变换矩阵

3.4 旋转变换

3.5 平移变换

平移变换稍微有点不同

平移变换矩阵

3.6 使用齐次坐标巧妙统一线性变换

不难发现,前面几个变换都可以写成x’ = Mx的形式,但是平移变换只能写成x’ = x + b的形式,那么我们完成上述变换可以统一写成x’ = Mx + b的形式,如下:

后面这个项不够优雅,但是如果我们对其扩展一个维度,就可以统一写成x’ = Mx的形式了。

什么意思呢?就是说对于一个二维坐标(x, y),写成列向量的时候添加一个w维度,(x, y, 1)T,(T是转置成列向量的意思,否则编辑器不好写列向量),那么可以发现平移变换也可以写成如下x’ = Mx的形式了

平移变换的齐次坐标表示法

是不是很巧妙?这种写法称为齐次坐标,在齐次坐标表示法下,w维度为1则表示向量,w为0则表示一个点

有了齐次坐标,我们就可以把以上所有3.1-3.5的变换统一写成x’ = Mx的形式了

非常优雅(主要是方便代码实现)

这里abcd是一般化写法,前面几个特殊的变换还是跟前面是一样的,自己推一下就能写出如下几个常用的了

引入齐次坐标后的缩放、旋转、平移

3.7 *推广到3维坐标

以上,都是以2D坐标为例,3维下大致相同,其次坐标为(x, y, z, w),线性变化一般化公式为:

特殊的如下:

缩放、平移

旋转

Shear:

1
2
3
4
5
6
7
8
9
10
11
Hxy(s, t) = [1 0 s
0 1 t
0 0 1]

Hxz(s, t) = [1 s 0
0 1 0
0 t 1]

Hyz(s, t) = [1 0 0
s 1 0
t 0 1]

Hxy(s, t)表示用z切变x、y,最终x += sz,y += tz,z不变

3.8 绕任意轴的旋转

绕单位向量n(nx, ny, nz)逆时针旋转θ

注意是右手系下的

推导过程可以看这篇文章

https://zhuanlan.zhihu.com/p/56587491

再次提醒:本文所有公式都是在右手系下的,如果换成左手系,那么需要将矩阵进行一次转置并将左乘改为右乘,并注意正角度需要改为顺时针

这个公式比较复杂,对于计算机运算也比较慢,可以通过欧拉角和四元数进行绕任意轴的旋转比较简便,附录中有介绍

3.9 绕任意轴的镜像

平面的单位法向量n(nx, ny, nz)


4. 视图变换(View)

第三章说的线性变换是对物体进行变换,也就是model,加上第四第五张介绍的view和projection,合称mvp变换

用照相的例子来比喻就是:

  • 找一个好的地点并调整好被拍摄者的占位(model)
  • 调整相机角度(view)
  • 茄子(projection)

所以视图变换就是:调整相机位置、确定拍摄方向、摆正相机水平角度这个过程(当然要灵活点,所谓相机就是观测者的眼睛,不一定非得是相机)

怎么摆正相机呢?在数学上怎么表示呢?通常我们将相机位置设为原点构建坐标系,相机摆正为垂直向上的y方向,看向-Z方向,那么自然x的方向虽然没有意义,但是就可以使用(g * t)叉乘得出

5. 投影变换(Projection)

透视投影和正交投影

5.1 正交投影(Orthographic)

正交投影比较简单,投影到某个面,就把法线方向的“深度”去掉,比如投影到xy平面,就把z值置为0即可

当然,这个矩阵是默认了物体经过原点的,如果看了games101,发现正交投影并没有这么做,而只是将空间中的物体移动到以原点为中心,并缩放到 [-1, 1]³中,并且保留了z。那这完全不一样呀…

games101中的正交投影

通常情况下我们说投影,投影平面是屏幕(或者说是C++运行的那个窗口),如果物体在视野外没有被看到,那么想投影就必须先被相机看到,我们不移动相机,而是移动物体,于是才有了games101中的移动并缩放到 [-1, 1]³,而保留z是因为games101不仅仅是做投影,还要后面的深度遮挡绘制和光栅化,所以这里先保留了深度值。因此games101中的投影并没有真正的投影,而是将物体规整到 [-1, 1]³这个空间中,让相机能够“看到”

上面这种情况才是一般情况,因为通常我们的场景被称为世界坐标(观测者在原点,可能出现正负坐标),但是我们投影完成后应该输出在屏幕(左上角为原点,只有正的二维坐标),所以games101中实际上做的是世界坐标->屏幕坐标的转换

5.2 透视投影(Perspective)

上面说过了什么是透视投影,一般在透视投影时,我们需要width, height, zNear, zFar这几个参数,zNear和zFar代表了远近平面的z值,只有在这个范围内的物体才会被看到。可以认为人的眼睛就是摄像机,电脑屏幕就是近平面,视力距离就是远平面。

而对于width, height,我们通常只取一个值比如height,另一个通过屏幕的宽高比获取,但是更常用的方法是连height也不直接指出,而是通过fov,称为视角的东西给出。这是因为比如我们要做一个睁眼特效,那么就可以通过不断的改变视角大小最后才达到屏幕高度(完全睁开眼)

宽高比和fov

侧面和公式


6. 几何图元

介绍一些图元的常用表示方式

直线

1
2
3
4
class Line3D{
Vector3 origin; //起点
Vector3 end; //终点
}

射线

1
2
3
4
class Ray3D{
Vector3 origin; //起点
Vector3 delta; //单位方向向量
}

1
2
3
4
class Sphere{
Vector3 center; //求球心
float radius; //半径
}

AABB(轴对齐矩形边界框)

也就是将某个不规则物体框起来的长方体box,记录两个对角顶点的向量

1
2
3
4
class AABB{
Vector3 min; //xyz最小顶点
Vector3 max; //xyz最大顶点
}

顶点顺序如下图

AABB顶点顺序

这个顺序的好处是当我们求某个顶点坐标时

1
2
3
4
5
6
Vector3 AABB::corner(int i){
assert(i >= 0);
assert(i <= 7);

return Vector3((i & 1) ? max.x : mix.x, (i & 2) ? max.y : mix.y, (i & 4) ? max.z : mix.z);
}

因为从0~7,x是小大一周期,y是小小大大一周期,z是小小小小大大大大一周期,算是一个小trick

AABB对于物体旋转,物体旋转,AABB也跟着旋转,要计算的是旋转后的物体的AABB还是原来AABB的AABB

6iNoge.png

如果是前者,那么可以对旋转后的物体重新做AABB即可。如果想得到后者,需要对原AABB做旋转,然后再得出max和min的坐标,这里的计算也有个小trick,就是并不需要真正的和旋转矩阵相乘,因为我们知道旋转矩阵乘法后为

x’ = m11x + m21y + m31z

y’ = m12x + m22y + m32z

z’ = m13z + m23y + m33z

观察即可知道,想得到x’max,那么如果m11为正则取xmax,如果为-则取xmin,其他同理,因此实现时只需要对9个m不断if判断即可

1
2
3
4
5
6
7
8
9
if(m11 > 0.0f){
min.x += m11 * old.min.x;
max.x += m11 * old.max.x;
}else{
min.x += m11 * old.max.x;
max.x += m11 * old.min.x;
}

... //继续+=其他m得出x,y,z

平面

按照高中数学,平面方程有一般式(三点),点法式(平面上一点+法向量),截距式

https://zhuanlan.zhihu.com/p/102514602

对于一般式ax+by+cz+d=0,则法向量是(a,b,c)

如果点p(x0, y0, z0)是平面上一点,则pn作为向量做点乘,得到d,d是原点到这个平面的距离

3D中,有时候很多个点在一个近似平面上,求一个离所有点距离之和最小的“最佳”平面,做一个类似回归的计算

求一个离所有点距离之和最小的平面

多边形

简单多边形(没有洞)描述顶点只需要依次顺时针给出坐标,但是如果是有洞的多边形,我们可以通过切缝的方式变为简单多边形

复杂多边形->简单多边形


7. 几何检测

7.1 距离检测

一个点到xxx的最近点

2D直线上的最近点

q’ = q + (d - q·n)n 其实就是从点p做该直线的垂线,和直线相交的点

射线上的最近点

q’ = p + (d·(q - p))d 其中p为射线起点,d为射线方向单位向量,

平面上的最近点

q’ = q + (d - q·n)n 和直线是一样的,因为都是垂线

球上的最近点

q’ = q + (||c - q|| - r) * (c - q) / ||d|| 是圆心cq减去半径那段,方向是q方向

AABB上的最近点

这个在游戏开发中比较常用,比如飞机游戏,当我们瞄准一个物体的时候,我们瞄准目标物体AABB中最近的一个点即可

方法也很简单,直接判断点(x, y, z)和目标物体所在的AABB大小,比如小于AABB的minx则取minx,大于maxx则取maxx,在中间则取目标物体的x

7.2 相交检测

2D中两条直线相交性检测

3D中两条射线相交性检测

射线和平面相交性检测

三个平面间相交性检测

射线和球相交性检测

两个球/圆相交性检测

球和平面相交性检测

//TODO 这里涉及到大量的公式,后序补上,都出自《图形图像编程精粹I》和《3D数学基础:图形与游戏开发》13章


8. 三角形和网格

三维的模型一般都会分解成无数个三角形网格以方便渲染弧面和纹理映射

纹理映射需要三角形网格

关于网格,目前的图形库(比如DirectX和opengl)大都设计成以下结构

1
2
3
4
5
6
7
class Vertex{	//顶点类
Vector3 p; //一个3维向量,表示一个顶点,包含xyz坐标
float u, v; //u v坐标用于纹理映射

//其他信息
Vector3 normal; //用于Gouraud shading的顶点法向量
}
1
2
3
4
5
6
class Triangle{	//三角形类
Vertex vtx[3]; //一个三角形包含3个顶点

//其他信息
Vector3 normal; //用于Flat shading的三角形法向量
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TriMesh{	//三角形网格类
//所谓网格其实就是三角形列表
int vAlloc; //分配的顶点数量,通常会比count多一些,方便扩展,类似于capcity和count
int vCount; //需要的顶点数量
Vertex *vList; //保存顶点列表

int tAlloc; //分配的三角形数量,通常会比count多一些,方便扩展
int tCount; //真正需要的三角形数量
Tringle *triList; //保存三角形列表

int addTri(Tringle tri); //增加三角形方法,返回索引

//其他信息
}

当然,因为最终操作的其实是网格,网格中其实有顶点了,所以也有些库的三角形类并没有Vertex vtx[3];而是只存了一个index,需要的时候再统一从网格类中根据下标获取顶点,这是标准做法

8.1 三角形和顶点的法向量

关于三角形类和网格类都会有一个属性normal,这个法向量会用于shading

三角形法向量的求法:三角形两个边向量做叉乘

e1 = v3 - v2

e2 = v1 - v3

n = e1 * e2

顶点法向量的求法:一个顶点可能是n个三角形的公共顶点,将这n个三角形的法向量求和就是该顶点法向量的方向,再做归一化(很多地方说是相邻三角形的法向量的平均数,仔细看发现其实不是平均数,因为求和后不是/n而是/模长,也就是变成单位向量)

代码实现的时候,我们并不能直接知道某个顶点会连接哪几个三角形,不可能需要算某个顶点法向量的时候去临时遍历一遍所有的三角形。所以一般都计算完三角形的法向量后,做一次求所有顶点的法向量操作:遍历所有的三角形,并将其三个顶点法向量累积,最后遍历一遍所有的顶点做归一化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void Mesh::computerVertexNormals(){
int i;
computeTriNormals(); //得到所有三角形的法向量

//旧值清零
for(i = 0; i < vCount; i++){
vertex(i).mormal.zero();
}

//将每个三角形的法向量累加到各顶点
for(i = 0; i < tCount; i++){
Tri *t = &tri(i); //其实就是Tri t = triList[i],我不是很习惯用指针
for(int j = 0; j < 3; j++){ //累计到各顶点
vertex(t->v[j].index).normal += t->normal; //其实就是vList[t.v[j].index].normal += t.normal;看不懂可以看下面的解释
}
}

//所有顶点的法向量归一化
for(i = 0; i < vCount; i++){
vertex(i).normal.normalize();
}
}

再说一下,t->v[j]是三角形类里面有个顶点类的成员v,.index是因为前面说的优化,也就是说这个顶点类并不是真正的Vertex,而是一个内部类Ver(长得都差不多只是成员函数只有索引),vertex(index)就是get函数*vList[index]

8.2 网格操作

//TODO 顶点焊接、面拆分,边坍缩,网格削减等等一系列操作,实现见书本14章


9. 图形流水线(图形管道)

  • 建立场景
  • 可见性检测
  • 设置渲染状态
  • 几何体生成与提交
  • 变换和光照
  • 背面剔除与裁剪
  • 投影到屏幕空间
  • 光栅化
  • 像素着色

其实基本上就是从第一行代码到显示在屏幕上所有过程

这几个的步骤不是固定的,有些步骤可以互换,所以如果用不同的图形库,也要遵守他们的流水线

d3d流水线

opengl流水线简化图

opengl这里是简化图


10. 光照和着色

这里说的光照只关注光的效果,不关注光造成的阴影,称为shading,而不关注shadow

10.1 光照

blinn-phong反射模型简单的认为光照由三部分组成:环境光,漫反射,高光

一张经典布林冯模型图

La,Ld,Ls的具体内容是什么呢?

10.1.1 漫反射diffuse

漫反射是说是光照在物体上然后反射到眼睛里面去

漫反射

这个公式如何理解呢?,这只是一个大胆的简化模型,但是是有一定道理的,可以看下面一步步的假设推导

我们可以从I开始,假设一开始光照强度是I,那么经过反射后先视为平行光(视为,不是真的,因为这个是简易模型),那么我们会发现如下图,跟物体平面是否正对着光有关,Lambert余弦指出平面法线和光的夹角余弦值会和光的强度成正比,这确实是有道理的,如下

6MkoIf.png

那么我们就得到了I’ = I*cosθ,cosθ可以替换成两个向量的点乘,同时根据常识可得光最小为0而不会为负数,为0就全黑了,因此

I’ = I * max(I·n)

这是我们认为是平面光源的情况(光无限远或者其他手段),但是光其实是点光源

点光源

我们认为光往每个角度平均的向外球面传播,于是我们可以认为强度以圆的半径速率减少

因此光到某个点时的强度为

I’ = (I/r²) * max(I·n)

是不是也有那么点道理??

最后我们认为可以控制强度因子,这个我认为你可以想象成雾天什么的,对吧,光还是那么强,只是经过云层等到达的强度就减少了,我们直接认为这个为一个常数因子。当然雾只是个比喻,有了这个因子之后可以方便我们操控光的亮面大小

常数因子的影响

再次提醒,这一切都是大胆的简化假设,如果以上三步你都认为确实存在一定的道理,那么你就得出上面漫反射部分的公式了

10.1.2 高光Specular

高光是一种镜面反射

在Blinn-phong中仍然是在简单的大胆简化假设的基础上进行推导

假设有一面镜子反射太阳光,我们在什么时候会感到刺眼?那就是眼睛刚好在反射角的时候,如下图:如果眼睛在黄色线上,就会感到很刺眼,偏一点点,仍然会刺眼但程度小,越远离黄色线,就越没有刺眼的感觉

高光

为了描述这个偏离的角度,这样子就比较难,这里很巧妙的引入了半程向量这种说法

引入半程向量

半程向量非常好求,I+v再归一化为单位向量即可,这个夹角α和偏离角度虽然差了2倍,但是也可以用来描述偏差嘛对吧

和Ld不同的是,我们可以看到一个次数p,这个是什么意思呢?

在之前漫反射的时候我们说了kd代表漫反射光面的大小,同样的这里的ks也代表高光光面的大小,但是对于高光来说,除了光面还有一个很重要的因素就是亮度,p则代表了光面的亮度,这仍然是有道理的

一张图道清了真相

公式中的其他部分和Ld差不多,就不解释了

10.1.3 环境光Ambient

最后是La环境光,当我们用手机夜拍的时候,没有太阳也不开闪光灯,唯一的一点光源只是转角看不到的路灯,你是否希望拍出来的东西是一片黑??

不希望吧,我们在一个很暗的环境下,我们希望物体接受到一些墙面或者任何(不光滑的平面)反射过来的那么一丢丢亮度

这很难处理,我们没有办法知道四面八方有多少物体能够给我们提供光源,那么我们干脆简单的认为四面八方都能提供一样强度的一点点光

环境光

就是这么简单大胆,于是将上述三个公式Ld, La, Ls一组合,便得到了Blinn-phong模型的完整公式

完整公式

一切都是这么的妙~啊

10.2 着色

三维物体都会表示成一堆三角形的模型,在着色时我们就可以选择对表面,还是顶点还是像素

10.2.1 flat shading

对表面求法向量

Flat shading

10.2.2 Gouraud shading

对顶点求法向量,求法面第8章也说过了,是该顶点邻接三角形的平均值

Gouraud shading

10.2.3 Phong shading

对每个像素进行着色

Phong shading

诚然,对像素着色效果最好,效率也比较低,但games101中指出,并不是说我们每个时刻都需要phong shading,因为这还取决于三角形的数量,如果三角形足够细化,那么flat shading也是可以有很好的效果的,并给出了对比图

不同三角形数量下的各种shading效果

我们可以看到,当模型三角形数量足够多的时候,phong shading带来的提升效果就不明显了,白白增加了计算量

10.3 雾化

雾化就是将3D图形和雾的颜色相混合的效果,会造成物体颜色模糊效果

雾化浓度和雾化距离相关,公式如下

雾化浓度


计算出雾的浓度后再混入颜色

颜色


11. 纹理映射

纹理映射

不管纹理图片大小多大,我们都首先缩放到u, v范围为[0, 1]

然后我们对于uv中间的小数比如0.25、0.5就用插值的方法将图贴上去

//TODO纹理映射中的双线性插值,解决插值中的模糊问题

11.3 纹理复用

很多时候纹理贴图都是可以复用的,比如墙壁的砖块,如果墙壁很大,为每一个墙壁制作纹理贴图肯定是不合理的,所以厉害的视觉设计师会将纹理设置成这样:

可复用纹理

什么意思呢?就是说我用两张这样的图片从上下左右跟另一张拼在一起,瓷砖纹理都能无缝接合另外一张,这样的纹理就可以复用而不违和


12. 双缓存

计算机输出图形时会出现闪烁(物体运动的时候突然消失,然后出现在下一个位置),使用双缓存可以解决闪烁

我们说双缓存,实际上是一个缓存…单缓存实际上是没有缓存…因为默认屏幕显示东西认为是一个缓存…闪烁的原因:原来是擦除上一帧的屏幕并重新绘制,擦除需要时间,绘制也需要时间,擦除完了我们看到原物体消失,重绘完了我们看到新位置物体出现,这就是闪烁。如果加入了缓存,可以直接将下一帧的图片存在缓冲区中,一次性输出到屏幕。

注意,擦除和重绘的时间还是一样慢,但是擦除的不是屏幕,而是缓存,也就是说上一帧还在屏幕上,重新绘制到缓存后,从缓存到屏幕的拷贝是一瞬间的事情

双缓存

关于双缓存,知道出现的原因和解决的原理即可,哪怕是自己实现也就是swapBuffer,用d3d或者opengl更简单了,一两行代码


13 可见性检测

这里不需要自己做,DirectX能自动调用硬件进行快速的可见性检测,原理部分以后再介绍

//TODO


14 抗锯齿

所谓锯齿,就是由于屏幕像素太小造成如下不平滑的边缘

锯齿

原因是对于像素的每个格子,用格子中心的坐标(0.5, 0.5)所在的颜色代表了整个像素格子的颜色

原因是格子中心的颜色代表了整个各自的颜色

解决方法

  • 物理:使用分辨率更好的显示器
  • 超采样supersampling:MsAA
  • 多重采样multisampling:directx

超采样就是将每个像素细分为四个小格子,对于每个小个子都计算颜色平均值作为当前的颜色。如果分为四个小格子就叫做MsAA4*4,可以更细的距离进行采样,缺点是分的越多计算量越大

以下是games101作业2中的代码(具体逻辑不用深究,就看是不是多了4倍计算)

不带抗锯齿是这样的

带抗锯齿

多了一层for,就是细分的格子数量,所以计算量也随之增大

多重采样是directx和一些图形库的抗锯齿方法,很快,仍然会使用屏幕分辨率4倍大小的后台缓冲和深度缓冲,但是,不像超级采样那样计算每个子像素的颜色,而是只计算像素中心颜色一次,然后基于子像素的可见性(基于子像素的深度/模板测试)和范围(子像素中心在多边形之外还是之内)共享颜色信息。

多重采样直接填充Directx的结构体即可

1
2
3
4
5
typedef struct DXGI_SAMPLE_DESC
{
UINT Count; //用于指定每个像素的采样数量
UINT Quality; //用于指定希望得到的质量级别
} DXGI_SAMPLE_DESC, *LPDXGI_SAMPLE_DESC;

不同硬件的质量级别表示的含义不一定相同,质量级别越高,占用的系统资源就越多,所以我们必须在质量和速度之间权衡利弊。具体的说明在后面的d3d文章中介绍,这里只做理论了解

抗锯齿效果


附录

多坐标系

当我们进行三维场景设计时,我们想旋转单个物体而不能影响其他物体,就不可能去旋转世界坐标系。因此引入了物体坐标系,在世界坐标系和物体坐标系中间为了方便转换还引入了惯性坐标系,如下,世界坐标系和惯性坐标系的转换只需要平移,惯性坐标系和物体坐标系转换只需要旋转

多坐标系

旋转和坐标系变换是一模一样的,只是理解角度不同,一个 active 变换也可以理解为一个坐标系转换变换,反之亦然。在某些情况下,使用多个坐标系统更符合思维习惯,我们可以让物体自身保持不变,只是从一个参考系转换到另一个参考系,由于参考系发生了改变,因此物体的坐标也会随之改变。在另一些情况中,我们不想改变参考系,而只想在同一个参考系中对物体进行变换

所以旋转的作用不仅仅在某个坐标系上旋转物体,还可以用于惯性坐标系和物体坐标系之间的坐标转换,比如对于同一个“i2o转换矩阵”可以通过如下旋转公式相互转换

其实和普通的旋转没有区别,只是说一下旋转有这种作用而已

为了欧拉角的唯一性(多转360°还是同一个角度),我们通常把heading限制在-180° ~ +180°,bank限制在-180° ~ +180°,pitch限制在-90° ~ +90°

欧拉角

之前说过绕任意轴旋转公式复杂,计算机也需要保存9个计算复杂的变量,欧拉角对其进行了简化

欧拉角是用三个角度表示方位和旋转,称为角位移:

heading角(也称作yaw角):常用于描述绕y轴(垂直轴)飞机水平偏航

pitch角:常用于描述绕x轴飞机低头抬头

bank角(也称作roll角):常用于描述绕x轴飞机自身翻滚

飞机图

三个旋转角绕着三个相互垂直的坐标轴(三个轴可以是任意的),通常是笛卡尔坐标系

注意:欧拉角是基于物体坐标系来旋转的,而不是基于世界坐标系来旋转的,所以通常我们使用欧拉角旋转一个空间坐标下的物体时,记得做惯性系->物体系的旋转

缺点

  • 表达不唯一,绕某个轴旋转360°还是一样的方位
  • 插值计算复杂
  • 万向锁问题

万向锁问题

很多博客用油管那个英文视频举例,相信大家也看过了,简单的说,本来三个角度有自己的作用,可以控制飞机的偏航、俯仰、滚转。但是转到如下图所示的时候

万向锁

本来红绿蓝三个圈应该是两两垂直的,蓝和绿至少有一个在图中的灰色圈里(准确的说应该是绿圈)用于控制俯仰操作,但是现在没有了,那么这个箭头不能做到俯仰操作了。转动绿圈的效果和转动蓝圈的效果变成一样了,绿圈就仿佛失去了他的作用。

矩阵和欧拉角的转换

这里以物体坐标系转换到惯性坐标系为例,其他也一样

欧拉角->矩阵:

其实就是分别绕轴旋转再做矩阵乘法(下面这两张图是左手坐标系所以看起来跟之前的不太一样,自行转置)

左手系

矩阵乘法结果如下:

左手系

这里以物体坐标系转换到惯性坐标系为例,如果是惯性系转换成物体坐标系,则做矩阵的逆,这里是正交矩阵,所以做转置即可

矩阵->欧拉角:

其实就是通过矩阵得到h,b,p角度大小,可以从上面矩阵

由m32得:p = asin(-m32)

由m31和m33相除得:h = atan(m31 / m33) = atan2(m31, m33)

由m12和m22相除得:b = atan(m12 / m22) = atan2(m12, m22)

同理,如果是惯性->物体则转置

欧拉角和四元数的转换

看四元数同章节



四元数

四元数类比复数,是三维下的复数

可以表示为[w (x y z)],xyz通常表示成向量,所以也可以简写成[w v]

[w (x y z)] = w + xi + yj + zk,其中i² + j² + k² = -1

四元数的一些运算

  • 负号:-q = -[w v] = [-w -v]

  • 单位四元数:[1 (0 0 0)]图形学表示没有角位移也就是没有旋转

  • 求模:||q|| = √(w² + x² + y² + z²),单位四元数模场为1

  • 共轭:q* = [w (-x -y -z)]图形学表示为沿负方向旋转

  • 逆:q^-1 = q* / ||q||

  • 对数:q = [cos θ/2 (sin θ/2)n]则log q = [0 nθ/2]

  • 差:d = a^-1*b,也就是d = b/a,需要注意的是所谓的差其实是除法,代表的是角位移了多少

  • 点乘:q1q2 = w1w2 + x1x2 + y1y2 + z1z2

  • 图形学中,将物体绕任意轴的向量n旋转,可以表示成四元数

    q = [cos θ/2 (sin θ/2)nx (sin θ/2)ny (sin θ/2)nz]

    把点p旋转q到点p‘

    旋转

  • 幂:q^t = e^(t * log q),几何意义:q = [cos θ/2 sin θ/2n]时表示绕n旋转θ°,则q^2表示旋转2θ°

矩阵和四元数的转换

下面这个矩阵是左手系下的

左手系

左手系

需要注意的是,不能直接取第一列的公式计算wxyz,因为开根号会丢失正负信息,而是算出wxyz最大的后,再通过对应右边的公式计算剩余三个

给出其中的一个w的推导,其余类似:

简便计算w的推导

欧拉角和四元数的转换

左手系

四元数 -> 欧拉角

先判断p角是否为90°(或者判断sinp == 1),是的话会出现万向锁,直接p = sinp*pi/2(注意不要直接写成pi/2,要累计误差)不是的话才是下面的公式

p角

下面h角和b角都是分了万向锁情况和普通情况

h角

b角

四元数插值

用过flash就知道,如果对于一个物体在t0、t10时刻的位置不同,中间的“关键帧”是通过插值计算出来的,并不需要我们为每个时刻都绘制一幅图像,这就是插值

球面线性插值“slerp”

对于四元数表示的原角度q0,新角度q1,插值函数为slerp函数

t的范围是[-1, 1],其实很好理解,Δq就是做差嘛,前面说过四元数的差其实是做除法,t就是描述了一个类似于时间的东西,或者理解为中间的几分之一

旋转插值

另一种更便于计算机实现的方式是使用角度ω进行旋转插值

如下图两个单位向量的位置v0v1,我们不再认为v1 = v0 + tω,而是认为v0和v1都乘一个系数后平移再做向量加法

旋转插值思路

如此,我们便只需要求出k0和k1到底是多少即可:

旋转插值求k

将k0k1代入前面的公式v1 = k0v0 + k1v1即可,这是对于两个单位向量,类比一下换成两个四元数可得

旋转插值slerp

btw:这个ω可以通过四元数的点乘求出来,因为四元数的点乘实际上跟向量的点乘差不了多少,只是多了个w,那么cosω = q0点乘q1

在实现的时候,需要注意的是前面向量的点乘我们说过cos可以用来判断两个向量的同向反向,这里表现为v0转到v1是顺时针大半圈呢还是逆时针小半圈呢?虽然这里四元数正负都是同一个方位,所以我们在钝角的情况下旋转到他的反向,也就是总是挑最小的那个方向进行,所以我们在实现时最好有如下判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//如果为钝角,那么不旋转到他那里,而是旋转到他的反向
if(cosOmega < 0.0f){
q1w = -q1w;
q1x = -q1x;
q1y = -q1y;
q1z = -q1z;
cosOmega = -cosOmega;
}

//如果夹角很小,为了减小计算量,直接认为走直线
if(cosOmega > 0.9999f){
k0 = 1.0f - t;
k1 = t;
}else{
//旋转插值公式
}

几种方法对比

任务/性质 矩阵 欧拉角 四元数
坐标系间旋转点 不能(转换成矩阵) 不能(转换成矩阵)
连接或增量旋转 能,但比四元数慢,小心矩阵蠕变情况 不能 能,比矩阵快
插值 基本上不能 能但可能遭遇万向锁 Slerp提供了平滑插值
易用程度
占用大小 9个数 3个数 4个数
是否能唯一表示给定方位 不是,同一方位有无数种表达 不是,有两种方法,他们相互为负
可能导致非法 矩阵蠕变 任意三个数都能构成合法的欧拉角 可能会出现误差积累从而非法

没有最好的方法,根据需求,并且几种方法之间可以相互转换

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×