向量和矩阵
坐标系
左手系、右手系
也就是把拇指当作
x
x
x轴,食指当作
y
y
y轴,中指当作
z
z
z轴,来考虑三维坐标。
向量常用公式
向量的模(长度)
公式:
∣
v
⃗
∣
=
v
x
∗
v
x
+
v
y
∗
v
y
+
v
z
∗
v
z
|\vec{v}| = \sqrt{v_x*v_x+v_y*v_y+v_z*v_z}
∣v∣=vx∗vx+vy∗vy+vz∗vz
代码:float length(vec3 v) { return sqrt(v.x*v.x+v.y*v.y+v.z*v.z); }
几何意义:得到
v
⃗
\vec{v}
v的长度。
标准化向量
公式:
v
⃗
n
o
r
m
=
v
⃗
∣
v
⃗
∣
\vec{v}_{norm} = \frac{ \vec{v} }{ |\vec{v}| }
vnorm=∣v∣v
代码:vec3 normal(vec3 v) { return v / length(v); }
几何意义:得到
v
⃗
\vec{v}
v的单位向量。
点积
公式 | 几何意义 |
---|---|
公式A: a ⃗ ⋅ b ⃗ = b ⃗ ⋅ a ⃗ = a x b x + a y b y + a z b z \vec{a} \cdot \vec{b} = \vec{b} \cdot \vec{a} = a_x b_x+a_yb_y+a_zb_z a⋅b=b⋅a=axbx+ayby+azbz | |
公式B1:
a
⃗
⋅
b
⃗
=
∣
a
⃗
∣
∣
b
⃗
∣
c
o
s
(
θ
)
\vec{a} \cdot \vec{b} = \vert \vec{a} \vert \vert \vec{b} \vert cos{(\theta)}
a⋅b=∣a∣∣b∣cos(θ) 公式B2: θ = arccos ( a ⃗ ⋅ b ⃗ ∣ a ⃗ ∣ ∣ b ⃗ ∣ ) \theta = \arccos{(\frac{\vec{a} \cdot \vec{b}}{\vert \vec{a} \vert \vert \vec{b} \vert})} θ=arccos(∣a∣∣b∣a⋅b) | 计算两个向量的夹角 |
公式C: v ⃗ p = n ⃗ v ⃗ ⋅ n ⃗ ∣ n ∣ 2 \vec{v}_p = \vec{n}\frac{\vec{v} \cdot \vec{n}}{{\vert n \vert}^2} vp=n∣n∣2v⋅n | v ⃗ \vec{v} v在 n ⃗ \vec{n} n上的投影 v ⃗ p \vec{v}_p vp |
公式A代码:float dot(vec3 a, vec3 b) { return a.x*b.x+a.y*b.y+a.z*b.z; }
公式B1代码:float dot(vec3 a, vec3 b) { return length(a)*length(b)*angle(a, b); }
公式B2代码:float angle(vec3 a, vec3 b) { return acos(dot(a, b)/(length(a)*length(b))); }
公式C代码:vec3 vec_projection(vec3 v, vec3 n) { return n*(dot(v, n)/pow(length(n), 2)); }
叉积
公式 | 几何意义 |
---|---|
公式A: a ⃗ × b ⃗ = [ a y b z − a z b y a z b x − a x b z a x b y − a y b x ] \vec{a}\times\vec{b} = \begin{bmatrix} a_yb_z-a_zb_y & a_zb_x-a_xb_z & a_xb_y-a_yb_x \end{bmatrix} a×b=[aybz−azbyazbx−axbzaxby−aybx] | 由 a ⃗ \vec{a} a、 b ⃗ \vec{b} b组成的面的法向量; |
公式B: S = ∣ a ⃗ × b ⃗ ∣ = ∣ a ⃗ ∣ ∣ b ⃗ ∣ sin ( θ ) S=\vert \vec{a}\times\vec{b} \vert = \vert \vec{a} \vert \vert \vec{b} \vert \sin{(\theta)} S=∣a×b∣=∣a∣∣b∣sin(θ) | 由
a
⃗
\vec{a}
a、
b
⃗
\vec{b}
b组成的平行四边形的面积; 也是由 a ⃗ \vec{a} a、 b ⃗ \vec{b} b组成的面的法向量长度; |
矩阵常用性质
矩阵能够表示线性变换,平常会用的有旋转、缩放、平移、镜像、仿射。
逆矩阵性质
公式: M ( M − 1 ) = I M(M^{-1})=I M(M−1)=I
几何解释:逆矩阵可以“撤销”原变换。 ( v ⃗ M ) M − 1 = v ⃗ ( M M − 1 ) = v ⃗ I = v ⃗ (\vec{v}M)M^{-1}=\vec{v}(MM^{-1})=\vec{v}I=\vec{v} (vM)M−1=v(MM−1)=vI=v
逆矩阵:如果矩阵是非奇异矩阵,那么该矩阵可逆。
非奇异矩阵:非奇异矩阵的行列式不为零。
可逆矩阵求逆方法:
- 求解伴随矩阵再除以行列式
- 高斯消元法
正交矩阵
定义:若方阵 M M M是正交的,则当且仅当 M M M与它的转置 M T M^{T} MT的乘积等于单位矩阵。
公式: M 正 交 ⟺ M M T = I M_{正交} \Longleftrightarrow MM^{T}=I M正交⟺MMT=I
推到公式: M 正 交 ⟺ M T = M − 1 M_{正交} \Longleftrightarrow M^{T} = M^{-1} M正交⟺MT=M−1
正交矩阵必须满足的条件:
- 矩阵的每一行都是单位向量
- 矩阵的所有行互相垂直
- 对矩阵的列也要做到类似的条件
齐次空间
齐次空间有以下意义:
- 平移:由于 M a t r i x 3 × 3 Matrix3\times3 Matrix3×3不能够表示平移,而齐次空间的 M a t r i x 4 × 4 Matrix4\times4 Matrix4×4能够表示平移。
- W分量: v ⃗ \vec{v} v在经过MVP变换后;如果是透视投影,则需要将视锥体投影到屏幕空间,也就是需要进行一次透视除法,透视除法就是用 v ⃗ \vec{v} v的w分量除以x、y、z分量。公式: v ⃗ N D C = v ⃗ x y z v ⃗ w \vec{v}_{NDC}=\frac{\vec{v}_{xyz}}{\vec{v}_w} vNDC=vwvxyz。此时 v ⃗ x y z ∈ [ − 1 , 1 ] \vec{v}_{xyz}\in[-1, 1] vxyz∈[−1,1]。
坐标空间变换推到过程
转自:知乎的一篇文章
MVP矩阵
移步:坐标系统
局部空间
局部空间是指物体所在的坐标空间,即对象最开始所在的地方。想象你在一个建模软件(比如说Blender)中创建了一个立方体。你创建的立方体的原点有可能位于(0, 0, 0),即便它有可能最后在程序中处于完全不同的位置。甚至有可能你创建的所有模型都以(0, 0, 0)为初始位置(译注:然而它们会最终出现在世界的不同位置)。所以,你的模型的所有顶点都是在局部空间中:它们相对于你的物体来说都是局部的。
我们一直使用的那个箱子的顶点是被设定在-0.5到0.5的坐标范围中,(0, 0)是它的原点。这些都是局部坐标。
从数据结构的理解角度为:每个顶点都在局部空间中。
typedef struct Vectex {
vec3 position;
vec2 uv;
vec3 normal;
};
// 记录模型中每一个点的信息
typedef struct Mesh {
Vectex *vectexs; // 顶点数组
unsigned int *indices; // 索引数组
}
世界空间
如果我们将我们所有的物体导入到程序当中,它们有可能会全挤在世界的原点(0, 0, 0)上,这并不是我们想要的结果。我们想为每一个物体定义一个位置,从而能在更大的世界当中放置它们。世界空间中的坐标正如其名:是指顶点相对于(游戏)世界的坐标。如果你希望将物体分散在世界上摆放(特别是非常真实的那样),这就是你希望物体变换到的空间。物体的坐标将会从局部空间变换到世界空间;该变换是由模型矩阵(Model Matrix)实现的。
模型矩阵
在拥有了世界空间的前提条件下,我们能够自然的求出模型矩阵。模型矩阵是一种变换矩阵,它能通过对物体进行位移、缩放、旋转来将它置于它本应该在的位置或朝向。
假设世界空间的原点为(0, 0, 0),那么从数据结构上面看:每个独立的GameObject都会有一个Transform信息。 其中modelMatrix * go.meshInfos[i].vertex => 局部空间 -> 世界空间
。
class Transform {
Matrix4x4 translation;
Matrix4x4 rotate;
Matrix4x4 scale;
}
class GameObject {
Mesh *meshInfos; // 可能有多个网格
Transform transform; // model矩阵
// model矩阵的方法
Matrix4x4 matrix() { return transform.translation * transform.rotate * transform.scale; }
}
void drawCall() {
// 假设是一个模型了
GameObject go;
// 将transform输入给顶点着色器
shader.setUniformMatrix4fv("modelMatrix", go.transform.matrix());
// 渲染
shader.drawElements(go.meshInfos);
}
观察空间
观察空间经常被人们称之OpenGL的摄像机(Camera)(所以有时也称为摄像机空间(Camera Space)或视觉空间(Eye Space))。观察空间是将世界空间坐标转化为用户视野前方的坐标而产生的结果。因此观察空间就是从摄像机的视角所观察到的空间。而这通常是由一系列的位移和旋转的组合来完成,平移/旋转场景从而使得特定的对象被变换到摄像机的前方。这些组合在一起的变换通常存储在一个观察矩阵(View Matrix)里,它被用来将世界坐标变换到观察空间。
观察矩阵
观察矩阵是一种变换矩阵,一般有2种方式构造这个矩阵。
- 已知摄像机在世界空间的transform,可以构建出从观察空间变换到世界空间的变化矩阵,即
transform.matrix()
;进而求transform.matrix()
矩阵的逆。 - Look At Camera方法,需要知道摄像机的世界空间下的位置Eye、一个Up向量、观察点At。
void drawCall() {
GameObject go;
// 观察矩阵获取方式
Matrix4x4 viewMatrix = Matrix4x4.inverse(Camera.main.matrix());
Matrix4x4 viewMatrix = Camera.lookAt(eye, up, at);
shader.setUniformMatrix4fv("viewMatrix", viewMatrix);
shader.setUniformMatrix4fv("modelMatrix", go.transform.matrix());
shader.drawElements(go.meshInfos);
}
裁剪空间
在一个顶点着色器运行的最后,OpenGL期望所有的坐标都能落在一个特定的范围内,且任何在这个范围之外的点都应该被裁剪掉(Clipped)。被裁剪掉的坐标就会被忽略,所以剩下的坐标就将变为屏幕上可见的片段。这也就是裁剪空间(Clip Space)名字的由来。为了将顶点坐标从观察变换到裁剪空间,我们需要定义一个投影矩阵(Projection Matrix)。
由投影矩阵创建的观察箱(Viewing Box)被称为平截头体(Frustum),每个出现在平截头体范围内的坐标都会最终出现在用户的屏幕上。将特定范围内的坐标转化到标准化设备坐标系NDC的过程(而且它很容易被映射到2D观察空间坐标)被称之为投影(Projection),因为使用投影矩阵能将3D坐标投影(Project)到很容易映射到2D的标准化设备坐标系中。
一旦所有顶点被变换到裁剪空间,最终的操作——透视除法(Perspective Division)将会执行,在这个过程中我们将位置向量的x,y,z分量分别除以向量的齐次w分量;透视除法是将4D裁剪空间坐标变换为3D标准化设备坐标的过程。这一步会在每一个顶点着色器运行的最后被自动执行。
透视投影
一个透视平截头体可以被看作一个不均匀形状的箱子,在这个箱子内部的每个坐标都会被映射到裁剪空间上的一个点。
API:perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
它的第一个参数定义了fov的值,它表示的是视野(Field of View),并且设置了观察空间的大小。如果想要一个真实的观察效果,它的值通常设置为45.0f,但想要一个末日风格的结果你可以将其设置一个更大的值。第二个参数设置了宽高比,由视口的宽除以高所得。第三和第四个参数设置了平截头体的近和远平面。我们通常设置近距离为0.1f,而远距离设为100.0f。所有在近平面和远平面内且处于平截头体内的顶点都会被渲染。
正交投影
正交投影矩阵定义了一个类似立方体的平截头箱,它定义了一个裁剪空间,在这空间之外的顶点都会被裁剪掉。创建一个正交投影矩阵需要指定可见平截头体的宽、高和长度。在使用正交投影矩阵变换至裁剪空间之后,任何在这个平截头体以外的坐标将会受到裁剪。
API:ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
前两个参数指定了平截头体的左右坐标,第三和第四参数指定了平截头体的底部和顶部。通过这四个参数我们定义了近平面和远平面的大小,然后第五和第六个参数则定义了近平面和远平面的距离。这个投影矩阵会将处于这些x,y,z值范围内的坐标变换为标准化设备坐标。
屏幕空间
在完成以上MVP矩阵运算后,映射到屏幕空间的事情将交给glViewport,由它来设置屏幕显示区域。
容易让人产生误会的地方
MVP后的透视除法
1、 顶点着色器乘上MVP矩阵后,输出的顶点为裁剪空间的点
并不是NDC坐标空间的点
,NDC转换是由GPU自己去做的。
2、 片段着色器的输入不是NDC坐标空间or
裁剪空间的点
,而是屏幕空间的点。