游戏开发客户端

《游戏开发》之客户端笔记

第一章:3D数学基础


1、向量

向量可以简单定义为\(n\)个实数的元组\(V = <v_1,v_2,...,v_n>\)

1.1、点积:


首先,向量的点积是个标量。
两向量点积的代数定义为:

\[\vec{a} \cdot \vec{b} = \sum_{i=1}^{n}a_ib_i \]

在二维和三位空间中,其具有几何意义。其中\(\theta\)为两向量夹角,则点积定义为以下实数:

\[\vec{a} \cdot \vec{b} =|\vec{a}||\vec{b}|cos\theta \quad (0 \le \theta \le \pi) \]

1.2、叉积:


叉积的结果是一个向量。
\(\vec{a}\)\(\vec{b}\)的叉积得到的向量同时垂直于\(\vec{a}\)\(\vec{b}\),其方向在左手坐标系中遵循左手定则。其不支持交换律。
对于三维向量其计算公式为:

\[\vec{a} \times \vec{b} = <a_yb_z-a_zb_y, a_zb_x-a_xb_z, a_xb_y-a_yb_x> \]

亦可通过三阶行列式计算。
其模的几何意义为向量组成的平行四边形面积:

\[|\vec{a} \times \vec{b}| =|\vec{a}||\vec{b}|sin\theta \quad (0 \le \theta \le \pi) \]

1.3、向量空间:


向量的集合称为向量空间\(\bar{V}\)
\(n\)维向量空间\(\bar{V}^n\)中的任意向量都可以由一组基向量得到。基向量满足:

\[a_1e_1 + a_2e_2 + ... + a_ne_n = 0 \]

若当且仅当\(a_1=a_2=...=a_n=0\)时上式成立,则称向量相互之间线性相关。基向量必是线性无关的。
当单位向量满足以下公式时,我们称其为正交向量:

\[e_i \cdot e_j = \sigma_{ij}\\ \sigma_{ij}= \left\{ \begin{aligned} & 1, \quad i=j \\ & 0, \quad i\neq j \end{aligned} \right. \]

当基向量满足上式时,我们称之为标准正交基。
\(3D\)游戏中,常用的向量空间为\(\bar{V}^3\),定义其基向量为\(x=\{1,0,0\},y=\{0,1,0\},z=\{0,0,1\}\),称该空间为空间直角坐标系。
此时,我们可以用一个向量表示\(3D\)空间中点的位置,称之为位置向量。使用位置向量,可以很方便的替代点之间的操作,比如两点之差可以通过向量之差获得。

2、矩阵代数


对于矩阵,我们可以把每一行元素看成是一个向量,或者把每一列元素看成是一个向量,那么就可以通过矩阵运算进行向量运算。

3、变换


本节将讲述不同笛卡尔坐标系中进行几何变换的方法。

3.1、线性变换


对于一个\(3D\)坐标系\(C\),记此坐标系下一点\(P\)\(<x,y,z>\)。再定义一个坐标系\(C^\prime\),其中的点\(<x^\prime,y^\prime,z^\prime>\)可以由\(C\)中的坐标进行线性变换得到:

\[x^\prime = U_1x + V_1y + W_1z + T_1 \\ y^\prime = U_2x + V_2y + W_2z + T_2 \\ z^\prime = U_3x + V_3y + W_3z + T_3 \\ \]

其矩阵形式为:

\[\left[ \begin{array}{ccc} x^\prime\\ y^\prime\\ z^\prime\\ \end{array} \right]= \left[ \begin{array}{ccc} U_1 & V_1 & W_1\\ U_2 & V_2 & W_2\\ U_3 & V_3 & W_3\\ \end{array} \right] \left[ \begin{array}{ccc} x\\ y\\ z\\ \end{array} \right]+ \left[ \begin{array}{ccc} T_1\\ T_2\\ T_3\\ \end{array} \right] \]

其中,\(T\)可以理解为坐标系\(C\)的原点到\(C^\prime\)的原点的平移向量,\(U,V,W\)为坐标轴\(C\)到坐标轴\(C^\prime\)朝向变换向量的矩阵,可以自己画个图感受一下。


缩放:
对于缩放,我们能很轻易地得到以系数\(a\)\(P\)进行缩放的变换公式\(P^\prime = aP\),其矩阵乘法形式:

\[P^\prime = \left[ \begin{array}{ccc} a & 0 & 0\\ 0 & a & 0\\ 0 & 0 & a\\ \end{array} \right] \left[ \begin{array}{ccc} P_x\\ P_y\\ P_z\\ \end{array} \right] \]

当三个坐标轴缩放比例相同时,我们称之为等比缩放;不同时,称之为非等比缩放。

\[P^\prime = \left[ \begin{array}{ccc} a & 0 & 0\\ 0 & b & 0\\ 0 & 0 & c\\ \end{array} \right] \left[ \begin{array}{ccc} P_x\\ P_y\\ P_z\\ \end{array} \right] \]

通过逆矩阵还原\(P^\prime\)\(P\)

\[P = \left[ \begin{array}{ccc} \frac{1}{a} & 0 & 0\\ 0 & \frac{1}{b} & 0\\ 0 & 0 & \frac{1}{c}\\ \end{array} \right] \left[ \begin{array}{ccc} P_x^\prime\\ P_y^\prime\\ P_z^\prime\\ \end{array} \right] \]


旋转:
本节将求向量\(P\)绕轴\(A\)旋转\(\theta\)角度,这里假设\(|A| = 1\)
首先将向量\(P\)进行分解,设\(P\)\(A\)上的投影为\(P_1\),在垂直\(A\)上的投影为\(P_2\),即分解为平行和垂直\(A\)的两个分量\(P=P_1+P_2\)。因为\(A\)上的投影\(P_1\)不随轴旋转,故只需旋转\(P_2\)后再加上\(P_1\)即可,
现将\(P_2\)绕轴\(A\)旋转\(\theta\)角度,我们设旋转后的垂直投影为\(P_2^\prime\)
已知旋转平面是一个二维平面,我们设置一个平面的向量基。将\(P_2\)作为其中一个基,已知\(A\times P\)也在旋转平面内且垂直\(P_2\),则作为另一个向量基。此时已知\(\theta\),那么最终\(P_2^\prime\)\(P_2\)上的投影为\(P_2cos\theta\),在\(A\times P\)上的投影为\((A\times P)sin\theta\)。即\(P_2^\prime=P_2cos\theta + (A\times P)sin\theta\)
易得,\(P_1=(A\cdot P)A\),则\(P_2=P-(A\cdot P)A\)
最终得旋转后向量\(P^\prime\)(简写\(sin\theta=s,cos\theta=c\)):

\[\begin{aligned} P^\prime & = [P-(A\cdot P)A]cos\theta + (A\times P)sin\theta+(A\cdot P)A\\ & = Pcos\theta +(A\times P)sin\theta+(A\cdot P)A(1-cos\theta)\\ & = \left[ \begin{array}{ccc} c+(1-c)A_x^2 & (1-c)A_xA_y-sA_z & (1-c)A_xA_z+sA_y\\ (1-c)A_xA_y+sA_z & c+(1-c)A_y^2 & (1-c)A_yA_z+sA_x\\ (1-c)A_xA_z+sA_y & (1-c)A_yA_z+sA_x & c+(1-c)A_z^2\\ \end{array} \right]P \end{aligned} \]

3.2、齐次坐标系统


由上缩放、旋转、平移公式可得任意进行三个操作为:

\[P^\prime=MP+T \]

\(M\)\(3\times3\)矩阵,\(T\)是三维平移向量。但是通过这个公式进行多次操作公式会变得过于冗长。现定义新的\(P^*=<x,y,z,w>\),其中\(w=1\)
那么上式可以改写为矩阵形式:

\[\begin{aligned} P^\prime&= \left[ \begin{array}{c} M & T\\ 0 & 1\\ \end{array} \right] P^*\\ &= \left[ \begin{array}{c} M & T\\ 0 & 1\\ \end{array} \right] \left[ \begin{array}{c} P\\ w\\ \end{array} \right]\\ & = \left[ \begin{array}{c} MP+T\\ w\\ \end{array} \right] \end{aligned} \]

这样看就简洁多了。

3.3、欧拉角


使用矩阵旋转具有很多缺点:比较占储存空间,不支持直接插值,阅读不直观。此节介绍一种表示旋转的新方式:欧拉角。
欧拉角就是记录模型绕三个坐标轴旋转的角度,任意旋转都可以分解为欧拉角形式,并且可能不唯一。对于笛卡尔坐标系,称为“\(Pitch-yaw-roll\)欧拉角系统”,\(Pitch\)为俯仰角绕\(x\)轴旋转,\(Yaw\)为偏航角绕\(y\)轴旋转,\(Roll\)为翻滚角绕\(z\)轴旋转。
但是欧拉角会出现万向节死锁的问题,插值速度不均匀,顺序不确定等缺点。

3.4、四元数


四元数是超复数,其所在空间时一个四维空间(相对于复数的二维空间)。四元数的虚部包含三个复数\(<x,y,z>\)。一个四元数可以表示为:\(q=<w,x,y,z>=w+xi+yj+zk\)。也可以写为\(q=s+v\)\(s\)是四元数的表两部分,\(v\)是向量部分。

看到这里已经懵逼。可能看不懂为什么要用复数去标识。所以这里对书里的内容扩展一下。


参考Understanding Quaternions

对于复数我们已经比较熟悉,常表示为\(a+bi\)。对于\(i\)的幂次我们可以发现以下现象:

\[\begin{aligned} i^0&=1\\ i^1&=i\\ i^2&=-1\\ i^3&=-i\\ i^4&=1\\ i^5&=i\\ &...\\ \end{aligned} \]

我们把复数映射到二维的复数平面,如下图所示。

我们可以猜想,对一个复数乘以\(i\)就是使其在复数平面逆时针旋转了\(90^\circ\)。验证可得猜想正确。
以此,我们可以得到一个在复数平面上能任意旋转的角度的方法:

\[q = cos\theta +\mathbf{i}sin\theta \]

任意复数\(p=a+b\mathbf{i}\)乘以\(q\),得到:

\[pq=(a+b\mathbf{i})(cos\theta +\mathbf{i}sin\theta )\\ a^\prime+b^\prime\mathbf{i}=acos\theta - bsin\theta + (asin\theta +bcos\theta)\mathbf{i} \]

此时,可以拓展到四元数。
四元数由Hamilton发明,其一般形式为:\(q=s+x\mathbf{i}+y\mathbf{j}+z\mathbf{k}\)
有如下表达式:

\[\begin{aligned} \mathbf{i^2}=\mathbf{j^2}=\mathbf{k^2}=\mathbf{i}\mathbf{j}\mathbf{k}=-1\\ \mathbf{i}\mathbf{j}=\mathbf{k} \quad \mathbf{j}\mathbf{k}=\mathbf{i} \quad \mathbf{k}\mathbf{i}=\mathbf{j}\\ \mathbf{j}\mathbf{i}=\mathbf{-k} \quad \mathbf{k}\mathbf{j}=\mathbf{-i} \quad \mathbf{i}\mathbf{k}=\mathbf{-j}\\ \end{aligned} \]

根据上面的表达式,\(i,j,k\)的关系很像笛卡尔坐标系的基向量的叉积规则:

\[x\times y=z \quad y\times z = x \quad z\times x = y\\ y\times x=-z \quad z\times y = -x \quad x\times z = -y \]

Hamilton以此使用超复数表达笛卡尔坐标系的三个单位向量。
我们用有序对表示四元数\(q=[s,\mathbf{v}]=[s,x\mathbf{i}+y\mathbf{j}+z\mathbf{k}]\)
四元数的加法懂的都懂,而其乘法为:

\[q_a = [s_a,\mathbf{a}]\\ q_b = [s_b,\mathbf{b}]\\ q_aq_b=[s_as_b−\mathbf{a}\cdot \mathbf{b},s_a\mathbf{b}+s_b\mathbf{a}+\mathbf{a}\times \mathbf{b}] \]

实四元数是虚部为\(0\)的四元数,纯四元数是实部为\(0\)的四元数。
我们定义单位四元数\(\mathbf{\hat{q}}=[0,\mathbf{\hat{v}}],|\mathbf{\hat{v}}|=1\),对于任意\(\mathbf{v}\)\(\mathbf{v}=v\mathbf{\hat{v}}\)
然后由上面所给出的内容,我们得到了新的表示四元数的方法,和复数表示非常像:

\[\begin{aligned} p&=[s,\mathbf{v}]\\ &=[s,\mathbf{0}]+[0,\mathbf{v}]\\ &=s+v\mathbf{\hat{q}}\\ &像复数 (a+b\mathbf{i}) \end{aligned} \]

对于四元数的点积,只需相乘求和即可:

\[\mathbf{q_1}\cdot \mathbf{q_2}=s_1s_2+x_1x_2+y_1y_2+z_1z_2 \]

通过四元数点积可以得到角度差:

\[cos\theta=\frac{s_1s_2+x_1x_2+y_1y_2+z_1z_2}{|\mathbf{q_1}| |\mathbf{q_2}|} \]

共轭四元数表示为:

\[\mathbf{\bar{q}}=[s,\mathbf{-v}] \]

四元数的长度,用\(|q|\)表示,通过共轭四元数或者四元数的点积得到其模的平方:

\[\mathbf{\bar{q}}\mathbf{q}=\mathbf{q}\mathbf{\bar{q}}=\mathbf{q}\cdot \mathbf{q}=|\mathbf{q}|^2=q^2 \]

通过共轭和模能求逆:

\[\mathbf{q}^{-1}=\frac{\mathbf{\bar{q}}}{q^2} \]

旋转:
在复数平面已经得到了一个旋转的公式,同样也存在这么一个旋转四元数:

\[q = [cos\frac{\theta}{2},sin\frac{\theta}{2}\mathbf{\hat{v}}] \]

如果要用对向量\(\mathbf{v}\)旋转,首先将\(\mathbf{v}\)调整为其次坐标形式\(<w,(x,y,z)>\),则左乘\(\mathbf{q}\)并右乘\(\mathbf{q^{-1}}\)

\[\mathbf{v^\prime}=\mathbf{q}\mathbf{v}\mathbf{q^{-1}} \]

若以\(\mathbf{q_1},\mathbf{q_2},\mathbf{q_3}\)依次对向量\(\mathbf{v}\)进行旋转变换,变换公式如下所示:

\[\mathbf{v^\prime}=\mathbf{q_3}\mathbf{q_2}\mathbf{q_1}\mathbf{v}\mathbf{q_1^{-1}}\mathbf{q_2^{-1}}\mathbf{q_3^{-1}} \]


现在继续书里。
能够进行插值运算是四元数相较于旋转矩阵和欧拉角最大的优点。
给定四元数\(\mathbf{q_1},\mathbf{q_2}\),其线性插值结果\(\mathbf{q_t}\)表示为:

\[\mathbf{q_t}=lerp(\mathbf{q_1},\mathbf{q_2},t)=(1-t)\mathbf{q_1} +t\mathbf{q_2}, \quad t\in [0,1] \]

改变\(t\)时,\(\mathbf{q_t}\)沿着弦变化,因此旋转时变化非恒定。
四元数球面线性插值:

\[\mathbf{q_t}=slerp(\mathbf{q_1},\mathbf{q_2},t)=\frac{sin(1-t)\theta}{sin\theta}\mathbf{q_1}+\frac{sint\theta}{sin\theta}\mathbf{q_2} \]

球面插值能够保证\(t\)均匀变化时,插值角度变化恒定。

4、几何对象


4.1、线


设有两点\(\mathbf{P_1},\mathbf{P_2}\),可定义如下:

\[\mathbf{P}(t)=(1-t)\mathbf{P_1}+t\mathbf{P_2} \]

\(t\in(0,1)\)时,表示一条线段;当\(t\in(-\infty,+\infty)\),表示一条直线。
通常使用原点\(\mathbf{O}\)和单位向量\(\mathbf{V}\)表示一条射线:

\[\mathbf{P}(t)=\mathbf{O}+t\mathbf{V}, \quad t\in[0,+\infty) \]

\(t\in(-\infty,+\infty)\)时,仍可表示直线。

4.2、平面


给定\(3D\)空间中的一个点\(\mathbf{P}\)和平面法向量\(\mathbf{N}\),这样一个平面可定义为满足一下公式的点集\(\mathbf{Q}\)

\[\mathbf{N}\cdot(\mathbf{Q}-\mathbf{P})=0 \]

上式展开,即为平面的点法式方程表示一个平面:

\[Ax+By+Cz+D=0 \]

其中\(D=-\mathbf{N}\cdot \mathbf{P}\)
\(D/|N|\)等于平面到坐标系原点的距离。

求直线和平面交点是常用计算。
将过\(\mathbf{Q}\)直线公式\(\mathbf{P}(t)=\mathbf{O}+t\mathbf{V}\)代入:

\[\begin{aligned} & \quad\ \ \mathbf{N}\cdot \mathbf{P}(t) +D = 0\\ &\Rightarrow \mathbf{N}\cdot \mathbf{O} +(\mathbf{N}\cdot \mathbf{V})t + D = 0\\ &\Rightarrow t = \frac{-(\mathbf{N}\cdot\mathbf{O} +D)}{\mathbf{N}\cdot \mathbf{V}} \end{aligned} \]

\(\mathbf{N}\cdot \mathbf{P}=0\)时,直线和平面平行,并且同时\(\mathbf{N}\cdot \mathbf{O}+D=0\),则直线在平面上;\(\mathbf{N}\cdot \mathbf{O}+D\ne0\)此时不存在交点,这都很好理解。\(t\)有解时代回\(\mathbf{Q}\)射线方程得出实际交点。

4.3、视域体


\(3D\)游戏引擎的可视区域通过摄像机绑定到几何体控制,称之为视域体。视域体分为透视投影视域体和正交投影视域体。
透视投影就是近大远小的效果,正交投影就是平行着投影到屏幕的效果,没有近大远小效果。

透视投影:
此时视域体类似一个以摄像机为原点的金字塔,和摄像机近的叫近裁剪平面,远的叫远裁剪平面,其定义了摄像机能观察到的最近和最远距离。构成视域体的另外四个平面控制了摄像机的横向和纵向视野。
其中,最重要的两个参数:视场角,屏幕宽高比。
横向视场角\(\alpha\)定义了视域体的左右两个平面,横向视角左右各可看到\(\alpha/2\)的角度。为了保持渲染图像和屏幕分辨率的匹配,使用屏幕宽高比\(v\)求纵向视场角\(\beta\)

\[\beta=2*tan^{-1}(v*tan\frac{\alpha}{2}) \]

正交投影:
正交投影的视域体是一个长方体,有六个参数定义。
近裁剪平面到摄像机距离\(n\),远裁剪平面到摄像机距离\(f\),左裁剪平面到摄像机距离\(l\),右裁剪平面到摄像机距离\(r\),底裁剪平面到摄像机距离\(b\),顶裁剪平面到摄像机距离\(t\)

5、光照&绘制


5.1、颜色


通常使用\(RGB\)的三元组表示。每个分量取值范围为\([0,1]\)

\[C = (C_r,C_g,C_b) \]

其四则运算遵照每位分别运算。

5.2、光照模型&材质


光照模型分为两类:局部光照模型和全局光照模型。
局部光照模型中,每个物体的光照只由光源直接照射;全局光照模型中,要同时考虑光源照射和其他物体反射的光线。全局光照模型效果比局部好,但是计算量大。通常会使用其他简化方案,限制光源和物体静止,离线计算出物体表面的全局光照结果(烘焙),使用光照贴图存储光照数据。

光源分类三类:
方向光,即平行光,发射光线不会衰减,具有恒定强度。
点光,点光源从一个点向四面八方均匀发射光线,点光源发射的光线因位置的变化其强度会衰减。\(Q\)为照射的位置,使用\(P\)表示光源位置,半径\(R\)表示最大衰减距离,\(C\)为点光源强度,\(d=|Q-P|\)表示表面到点光源距离,\(a\)表示常量衰减系数,\(b\)表示线性衰减系数,\(c\)表示二次衰减系数。

\[C_Q=\frac{C}{a+bd+cd^2} \]

聚光灯,发出的光线像聚光灯一样分布在一个锥形的区域。我们可以用\(P\)表示光源位置,\(d\)表示光源方向向量,\(C\)为光强,投射到空间的\(Q\)处。\(a\)表示常量衰减系数,\(b\)表示线性衰减系数,\(c\)表示二次衰减系数。\(L=(Q-P)/|Q-P|\)表示\(P\)\(Q\)的单位方向向量,点积上指数\(q\)控制聚光灯的内聚程度,\(q\)越大内聚程度越高。

\[C_Q=\frac{\max(-L\cdot d,0)^q}{a+bd+cd^2}C \]

5.3、法线向量


法线向量是空间中平面的方向向量。在定义某个顶点的法线时,通常定义为其相邻平面的法线加权平均。
我们可以通过变换矩阵\(M\)对点和向量进行变换,但是在对法向量进行变换时,变换后的法向量可能和平面不垂直。
当变换矩阵不包含非等比缩放,我们仍可使用原矩阵进行变换法向量;否则我们可以使用矩阵逆的转置\(M^\prime=(M^{-1})^T\)得到正确的法向量结果,但需重新归一化。

5.4、兰伯特余弦定理


根据经验可得,一束光垂直照射物体表面比侧面照射看起来亮,兰伯特余弦定理给出了上述规律的函数定义:

\[f(\theta)=\max(L\cdot n, 0) \]

\(L\)为光照向量,\(n\)为平面法向量。

5.5、漫反射&镜面反射


反射分为漫反射和镜面反射。反射光线的分布可以看做是以入射点位置和法线所定义的半球体。

当物体表面粗糙时,反射光线会在随机方向分布,假设光的颜色和强度为\(C_l\),物体表面反射率\(C_m\)\(C_lC_m\)表示漫反射光的颜色强度。漫反射光照模型的计算公式:

\[C_0=\max(L\cdot n,0)C_lC_m \]

当光线照射到光滑平面时,反射光线会分布在一个锥形区域,当观察点位置变化时,我们看到的高光也可能随着变化。\(n\)为法向量,\(L\)为入射光单位方向向量,\(R\)为单位反射向量,\(V\)为视线范围方向向量,其中\(L,R\)和法向量夹角相等。

\[\begin{aligned} &C_0=C_lC_mK\\ &K= \left\{ \begin{aligned} &\max(V\cdot R, 0)^S, &L\cdot n > 0\\ &0, &L\cdot n \le0 \end{aligned} \right. \end{aligned} \]

\(K\)是高光因子,\(S\)是高光锐利程度(内聚程度)。

第二章:图形渲染


1、渲染管线

1.1、渲染管线概述

1.2、顶点处理


传统的顶点动画被称为变形动画,后面又有骨骼动画。还存在一类非美术编辑的动画,依靠程序在\(Shader\)中利用余弦函数等周期函数定义的计算机动画,诸如海浪起伏、植被摆动。

和顶点位置无关的计算理论上都可以在\(Pixel Shader\)中进行,但是实际上为了提高效率大都在\(Vertex Shader\)中进行,因为通常顶点数远小于像素数。顶点到像素的插值是线性的,但是计算是非线性的,所以在顶点密度不够的时候,可能会带来差异。

1.3、像素处理


像素处理中大量运用了贴图。
为了将纹理映射到三角形网络中,定义了一个归一化坐标UV坐标系,其左下角为\((0,0)\),右上角为\((1,1)\)。使用归一化坐标,不论贴图精度如何,都可以找到相同的纹理位置。
在实际使用中,往往需要UV延伸到\([0,1]\)以外,GPU提供了几种贴图寻址方式:Wrap环绕模式,即贴图平铺;Mirror镜像模式;Clamp简单地取边缘部分像素;Border指定一种颜色在超出范围时使用。
为了避免贴图放大的马赛克情况,可以使用贴图过滤。常用的贴图过滤方式有:Nearest点采样,使用最近像素中心的纹理;Linear线性采样,由最近的纹理插值形成;Anisotropic各向异性采样,当三角形表面倾斜于相机时使用以提高品质。
当平面离相机太远时,贴图采样会严重闪烁,为解决此问题,采用Mipmap方式,依次把贴图的\(1/2,1/4,1/8,...\)知道只有一个像素的贴图序列记录在纹理当中,通过计算像素的贴图纹理密度采用相应的Mipmap。
序列贴图常用于各种特效,将序列贴图的多个帧放在一起,引擎中通过UV变换,每帧变换当前贴图使用的子贴图。

1.4、渲染状态


Alpha Test和Alpha Blend是两种常用的处理透明的方法。Alpha Test中,图元在写入帧缓冲前,会将alpha和预设的AlphaRef比较,比较通过才输出,否则丢弃着色,常用于镂空。Alpha Blend中,图元写入帧缓冲之前会把alpha和之前写入帧缓冲的颜色混合,常用于半透明物体。但是尽量不要用这两个玩意,都不好用。

为了在渲染中表现深度,通常使用画家算法按照先远后进覆盖绘制,但对于交错重叠的情况无法绘制。提出深度缓冲算法:

  1. 对深度缓存器和颜色缓存器初始化,深度缓存器均设为一个最大深度
  2. 当一个图元写入像素时,其深度也会写入深度缓存
  3. 当一个图元要写到像素时,渲染管线比较新图元深度和深度缓存中记载深度
  4. 新图元更接近相机则写入像素中,佛足额抛弃

深度缓冲是可配置的,其深度比较函数是可自定义的。

2、光照

2.1、光照管线


Forward光照在shader中,循环计算每个灯对物体的影响,有大量空跑。

Deferred延迟光照,会先遍历所有物体,将其光照的几何信息(位置、法线、粗糙度等)记录到RenderTarget中,然后执行一次屏幕空间光照计算,每盏灯按影响范围计算并汇总光照结果。

Lightmap烘焙,异于上面两种实时计算,Lightmap是离线形式的,更适合移动端。Lightmap会将静态物体表面的光照情况(离线可以使用更精确消耗更大的计算算法)计算出来,记录到贴图中,最终渲染时,使用Lightmap采样结果作为光照结果,和物体自身diffuse贴图运算,得到最终光照结果。

Lightmap虽然很好,但是他有一个大前提,就是只针对静态物体,对于动态物体无法Lightmap。可以使用点云烘焙,将光照信息烘焙到空间虚拟的受光点(点云)上,运动物体去采集点云的信息,插值得到受光情况。

还有一种Imaged-based lighting(IBL)。

2.2、影子


静态物体可以通过Lightmap获得影子效果。

其他可以使用ShadowMap。

  1. 从投影灯角度渲染场景,获得遮挡体到灯的距离
  2. 正常渲染每个物体,从灯光角度看,对比像素和遮挡体谁更靠近灯。遮挡体靠近投影灯,则像素处于阴影中

但是资源消耗比较大,锯齿严重(本质由于精度不足)。对于ShadowMap的反走样,可以通过软阴影缓解。
Percentage Closer Filter(PCF)是原理最简单的一种方法,绘制阴影时对周围阴影情况进行采样,确定百分比以此柔化。
另一种Cascaded Shadow Maps(CSM)是减少锯齿的另一种思路,阴影离镜头越近,使用ShadowMap精度越高;反之越低。

3、次时代渲染基础

3.1、NormalMap


使用一张纹理存储法线信息,这样Shader就可以得到每个像素的法线信息,通过光照计算产生凹凸不平的效果。但是会多采样一张法线贴图,增加带宽。

3.2、Pysically-Based Rendering(PBR)


PBR渲染中有三个重要参数:Albedo,Roughness,Metallic。
Albedo是指拥有颜色信息的贴图,不带有AO、Shadow等光影信息。
Roughness描述物体的光滑程度。
Metallic。物体分为金属、绝缘体、非金属,可以简单分为金属和非金属。金属的反射率会更高,且部分金属会带颜色。

第三章:物理

主流3D物理引擎有Havok、PhysX、Bullet,物理引擎主要作用是计算物体受到的外力,计算加速度、速度,计算物体的下一帧位置。

1、碰撞检测

碰撞检测主要分为三个部分:碰撞过滤、膨胀回调、碰撞查询。


1.1、碰撞过滤

物理引擎的碰撞过滤系统,能让用户自定义刚体在运动时是否发生碰撞。其通常通过用户自定义过滤函数FilterShaderFunction实现。PhysX提供的默认方法,会将传入的参数定义为一个编号,通过查询其内部的表判断对应的编号之间是否能碰撞;Havok的Layer-System-SubSytem系统,传入的参数记录Layer,System,SubSystem参数,经过分层判断查看是否能碰撞。


1.2、碰撞回调

碰撞回调在两个刚体发生碰撞时,提供一个通知功能。用户可以向物理引擎提供一个处理碰撞回调的对象,发生碰撞时,物理引擎会调用这个对象的接口。


1.3、碰撞查询

碰撞查询提供一个功能,使得用户可以查询场景里的刚体。一般有三种查询类型:射线查询,形状求交查询,扫描查询。

射线查询,由用户提供线段的起点和终点,物理引擎会找到所有和线段相交的刚体,交点以及交点处法向量。
形状求交查询,由用户提供一个形状,并提供形状的位置和朝向,物理引擎会找到和这个形状相交的所有刚体。
扫描查询,由用户提供一个形状、初始位置和朝向,然后提供平移方向和距离,物理引擎会找到所有在移动过程中碰撞的刚体。
物理引擎每收集到一个碰撞信息就是调用PxHitCallback的processTouches,通过这个函数的返回值,能告诉物理引擎继续查询还是马上结束返回。

2、碰撞系统架构


碰撞检测系统一般采用分层架构,以此优化效率。一般包括粗检测阶段和细检测阶段,有些物理引擎还会继续分层。在粗检测阶段会使用小的运算把不可能发生碰撞的物体对排除,细检测阶段会用精确的算法计算是否碰撞。所以粗检测阶段算法是碰撞系统中最重要的算法,其效率决定了碰撞系统的效率。

粗检测阶段,使用AABB(轴对齐包围盒)描述物体。介绍两种算法:Bounding Volume Hierarchies和Sweep-and-pure。
Bounding Volume Hierarchies通过对AABB做多层结构加速。其建立的一棵树中,父节点的AABB完全包含所有子节点的AABB。那么查询时,当两个父节点的AABB不相交时,其子节点必不相交;否则检测其子节点。Bounding Volume Hierarchies通常用于静态物体,显然这是由于其判断方式决定的。

Sweep-and-pure将AABB按照某个坐标轴(比如x轴)进行投影,记录每个AABB在轴上的最大值最小值。如果\(Max_A<Min_B\)那么AB一定不相交。Sweep-and-pure会用一个有序队列保存这些AABB的最大值最小值。
此外,还会保存当前帧的AABB相交情况。每一帧开始,依次检测每个AABB位置变化,然后通过交换\(Min,Max\)位置就能得到交叉状态的转换,提高效率。

细检测阶段,往往针对不同形状使用不同的算法。比较著名的如GJK算法。


3、物理引擎动力学模拟


物理引擎中,\(\vec{p}=(x,y,z)\)表示质心位置,\(\vec{\mathbf{q}}=x+iy+jz+kw\)表示刚体朝向。对于刚体上任意一点\(b\)的位置都可以通过质心的偏移得到:\(\vec{b}=\vec{p}+\vec{r}\)。把\(\vec{b}\)对时间求导可得其线速度,把\(\vec{b}\)对时间求二次导数可得其线性加速度。当然其角速度和角加速度亦可求解。根据力学定理,可得外力之和\(\vec{F}=m\vec{a}\)和动量。

在无约束力的情况下,仅重力对其做功,故只需考虑重力即可计算出加速度、速度、下一帧位置等等信息。

在有约束力的情况下,先按照无约束考虑可得其速度。然后代入约束方程,让刚体满足约束,调整其速度,计算其位置。
发生碰撞时,把碰撞点作为约束。

4、其他实现


4.1、布娃娃系统

布娃娃系统基于刚体模拟用来给人体建模,使物理引擎对刚体的模拟和动画系统的骨架姿态联系在一起,实现模拟动画,增强任务运动时(跌倒、击飞、死亡等)的动画表现。
因为人体肢体时有约束的,所以模拟相连的身体部位的刚体之间,他们局部的点不能发生分离,有一个点约束作为关节约束。且关节不能随意旋转,故每个方向的旋转上也有限制。由于刚体之间存在连接且不可能成环,所以布娃娃系统在逻辑上是一个树状结构。由于布娃娃系统的结构和渲染的动画骨架相似,所以能比较容易的在物理骨架和动画骨架之间做出映射,将物理引擎通过布娃娃获得的变换矩阵赋给映射的动画骨架;反之亦然。

4.2、车辆系统

对于车辆的模拟,仅通过一个车身参与建模,其他车轮等等仅作为阻挡其他物体的刚体,不参与模拟计算。
由引擎动力曲线(引擎角速度为\(\omega\)时的力矩),通过玩家输入的油门开合程度,即可得到动力力矩。
由变速器力矩、刹车力、地面摩擦阻力可得下一帧引擎的角速度。通过一系列计算还能得到车轮的角速度。针对车轮分析,亦可得到其地面摩擦力。
由于悬挂的存在,车辆在作出某些机动操作时,车身会有俯仰等等动作,车辆系统通过射线检测模拟悬挂。沿着悬挂运动方向做射线检测,对检测长度归一化到悬挂行程内。车辆系统会给出弹回系数和阻力系数描述悬挂特性。上述计算可得悬挂给轮胎的压力。当模拟完车辆的状态后,可以通过计算出来的角速度和悬挂行程,计算得到车辆的姿态。

5、破碎系统


当撞击发生时,物理引擎计算出撞击点和冲量并交给破碎系统,由破碎系统判断是否破碎,如果破碎,则将原刚体替换为碎片,由物理引擎继续模拟撞击。
为了节省计算,碎片通常都是离线分割好的。在撞击时,破碎系统会判定哪些碎片会分割哪些不会分割,并且切割渲染形状的面,使得破碎更加真实。
为了获得比较真实多变的破碎效果,可以按照破碎的疏密程度建立破碎层级的树状结构,当触发某些条件时,父节点破碎成若干子节点。

6、布料系统


布料系统模拟衣服随人运动而飘动的效果。
布料系统使用弹簧质点模型描述布料,弹簧质点模型将布料分为网格,每个点就是一个质点,其质量为布料质量随质点数的均分。为了布料不散开,质点间使用弹簧约束连接,质点间距离过远会施力拉近;反之弹远。
弹簧约束分为三类:结构约束是质点间的约束,为了保证质点的距离在一个范围,控制布料拉伸。切变约束是连接网格对角线的约束,目的是保持网格之间的平行和垂直,模拟布料拉扯时的切变程度。弯曲约束是连接相隔一个质点的质点对之间的约束,模拟布料的弯曲难度。
布料和人体的碰撞,通常都是把人的形状简单为一些几何体,发生形状交叉即纠正质点的外力。
需要布料随人体运动,通常都是把一部分接触的质点和人体骨骼绑定。

第四章:动画

1、骨骼动画


骨骼动画通过定义一套骨骼,用蒙皮把模型顶点附在骨骼上,以骨骼运动带动顶点网格运动。骨骼动画的数量通常比顶点数少,且每个骨骼只记录和父节点的信息,所以相对来说存储更小。

1.1、定义

骨架由骨骼以树状结构组织在一起,骨骼是一个个的点坐标并不是一块骨头。由于其树状结构的特性,父节点的变化会直接导致所有子节点一起变化,这使得骨骼动画具有一定的方便性。
骨骼中通常存储着当前节点和父节点的相对变换关系,比如相对父骨骼的变换矩阵\(M_i\)(缩放S旋转R平移T的整合的矩阵),由此我们可以推出任意骨骼在模型空间的点变换矩阵\(M_i\cdot M_{i-1}\cdot ...\cdot M_0\)。反之可通过求逆从父节点得到骨骼空间的点。

1.2、动画

手动编辑骨骼动画难度大,通常使用插件自动计算或者采用动作捕捉。
动画数据采样时,根据采样频率获得每个骨骼的数据,在游戏中再通过插值还原原来的运动曲线。
因为变换矩阵无法直接进行插值,所以通常将其分解为缩放S、旋转R、平移T分量,存储在每个骨骼的RTS通道,这三者都可分别进行插值运算。而且,在骨骼动画中,这三个分量在分解后通常会存在很多冗余,这对于数据压缩也有很多好处。
缩放和平移可以直接使用线性插值,而旋转(四元数表示)则使用球面插值。
在还原骨骼动画的姿态时,对每根骨骼进行插值,得到插值后的RTS数据,然后得到变换矩阵。由于其树状结构,父节点此时的模型空间变换矩阵可以通过祖父节点这样依次递归求得,那么很轻易将变换矩阵变到模型空间的变换矩阵。即可绘制出骨架姿态。

1.3、蒙皮

蒙皮即是确定网格模型顶点和骨骼之间关系的映射。
在我们确定好映射关系后,对于任意的网格顶点\(P\),都可以由一个固定的变换矩阵\(M\)从骨骼模型位置转换到网格顶点的位置。但是一个顶点只绑定一个骨骼,在动画时会显得过于生硬,所以通常为了使得动作平滑并且运行效率,每个网格顶点受4根骨骼影响,每个顶点记录7个信息:骨骼索引\(b_0,b_1,b_2,b_3\),分别的权重\(W_0,W_1,W_2\)。最终的位置由这几个点加权插值得到。
蒙皮计算的顶点较多,通常使用GPU并行架构计算。

对于一个人形角色,通常有换装需求。在处理这种情况时,有两种方案。一是使用一个大而全的骨架,所有装备的骨骼应有尽有。还有一种是使用子骨架动态拼接到主骨架上,需要管理子骨骼动画。

1.4、动作融合和叠加

角色很多时候会遇到动作融合的情况,比如一边跑一边开枪,如果所有的动作都重新设计显然开发效率非常低,所以需要动作融合。

已知变换矩阵由三部分组成:缩放、旋转、平移(SRT),所以动作融合的原理就是对着三个分量进行加权。

\[\begin{aligned} &S = S_1\cdot r_1+S_2\cdot r_2 + ...+S_n\cdot r_n\\ &R = R_1\cdot r_1+R_2\cdot r_2 + ...+R_n\cdot r_n\\ &T = T_1\cdot r_1+T_2\cdot r_2 + ...+T_n\cdot r_n \end{aligned} \]

同时,为了方便对动画进行管理,定义了动作层。动作层之间相互独立,层与层之间具有优先级,层越高其权重越大。

上下半身融合。
在骨骼空间进行上下半身融合的时候,由于骨骼的树状结构,下半身的运动很容易影响上半身的旋转,比如腰部骨骼在盆骨骨骼的子节点。所以,记录下根骨到腰部骨骼的骨链信息,然后在模型空间进行纠正。
但是这样这动作仍然较为生硬,因为根骨骼完全和上半身没有影响。所以提出分量动作融合。分量动作融合是将骨骼在6个自由度(XYZ轴的平移和旋转)分别进行动作融合,每个自由度的融合比例不同。
为了使得动作切换顺畅并且延迟小,需要考虑动作之间的同步问题。

动作叠加是在当前骨骼状态下,叠加平移、旋转和缩放量。和动作融合不同,动作融合是进行SRT插值,动作叠加是两者相加。因为动作融合是中间态,无法达到最大角度,所以动作叠加可以实现很多其他效果。

1.5、动作编辑与管理

商业引擎提供了简单的可视化编辑器可以对动画进行编辑和管理。
状态机用来表示动作状态之间的转换,每个节点也可以是状态机。当状态机很复杂的时候,很难管理和维护,可以通过多层嵌套和动态跳转简化。


2、程序动画


程序动画是在运行时对骨骼的变换矩阵进行修改,以改变骨架姿势。
程序动画的实现分为正向运动学FK和反向运动学IK。FK是通过父骨骼的修改影响子骨骼姿态,IK是先确定子骨骼姿态,反推出父骨骼姿态。
程序动画通常和动化融合配合使用,比如已经有开门动作,通过IK让手握住门把手,调整手臂其他骨骼。

2.1、正向运动学

正向运动学即是使用变换矩阵对父骨骼进行操作。由于矩阵不满足交换律,所以要小心处理矩阵之间的乘法顺序。以旋转为例,如果是绕自身坐标系旋转的,则乘在已有结果之前;绕父坐标系,乘在已有结果之后。

2.2、反向运动学

反向运动学求解通常有以下两种方法:

三角解析法:对于2根骨骼的情况,子骨骼\(P\),父骨骼\(C\)和旋转后的子骨骼\(P^\prime\),三者成平面,叉乘可得向量,然后可计算出旋转矩阵。
对于3根骨骼,祖父骨骼\(B\),父骨骼\(C\),子骨骼\(P\),当\(P\)旋转到\(P^\prime\)时,\(C^\prime\)应该在\(BP^\prime\)连线中,以某个点\(K\)半径为\(r\)的圆上,要找到这个\(C^\prime\)就需要对运动程度加以一定的约束,比如手肘抬起角度等等。找到后就可以求出旋转矩阵。
三角解析法求解精确,速度快,但是只能求解3根骨骼以下。但是在某些情况,我们可以简化为3根骨骼,进行粗调,然后再精细化操作。

循环坐标下降法:简称CCD。从末端骨骼开始遍历到根骨骼,每根骨骼依次旋转,没迭代一次判断末端骨骼是否到达预期位置的误差范围内;没有则继续迭代。通常遍历3~5次可以达到较小误差。需要控制迭代次数。

2.3、性能分析及优化

  1. 排除不在视野范围内的骨骼运算。由于相机内角色之占少数,对于相机外的角色,只更新动作播放逻辑。
  2. 调整更新频率。比如游戏帧率从60帧降到30帧。
  3. 骨架姿态结果缓存。游戏中角色大多数时候都放播放有限几个动画,那么对这几个动画每帧的结果缓存,就能够节约很多运算时间。
  4. 骨骼LOD。当角色距离较远时,使用低精度骨骼即可。
  5. GPU计算人群动画。使用Instance技术批量渲染,预先将计算好的骨骼播放动画后的姿态,存储在贴图数据中,在GPU中完成骨骼动画计算。使用GPU的并行能力加速性能。

第五章:特效

特效是一类拥有动态行为、动态效果,用来传达一种特定的美术表现所使用的计算机绘制技术。


1、粒子系统

粒子系统会随机产生大量的粒子,粒子会按照一定的运动规律进行逻辑变化。
粒子系统分为发射器、驱动器和渲染器。

1.1、发射器

发射器会按照一定的规则行为来发射粒子,粒子可以是多种定义的形状。发射器会被挂接到空间中某个位置,可以是任意位置。每个粒子都具有随机性,用户可以指定一个范围,进行伪随机生成(比如粒子颜色)。
对于发射器,我们应该指定粒子的发射空间,确定粒子的空间属性是局部空间还是世界空间。比如移动中火炬的粒子特效,其粒子应该指定在世界空间中,否则脱离发射器后,仍会在局部空间中漂浮,很不符合逻辑。
粒子的生命周期指定了粒子发射后存活的时间,在结束生命周期后粒子将被回收。在生命周期的不同阶段,可以按照生命时间对粒子的参数进行变化。

1.2、驱动器

驱动器会按照给定的要求在每一帧更新发射出来的粒子。
驱动器的实现是一个链式结构,在每一帧,所有粒子会遍历驱动器链,依次进行属性更新。

1.3、渲染器

粒子系统中的粒子在经过驱动器后得到数据,经由渲染器转化为渲染图元。


2、粒子特效类型

2.1、Sprite Particle

2D图形的动画元素。Sprite本身是由两个三角形面片和其顶点的UV坐标所构成的绘制单元,UV坐标将Sprite的像素着色映射到Sprite Particle对应贴图的一个区域内。如果是一个静态的,那么就把粒子贴图分布到(0,0)到(1,1)的整个贴图中;如果是动态的,就按照帧率分割成N份,排列在UV坐标系中。因此,帧率和分辨率此消彼长。

2.2、Mesh Particle

Mesh Particle就是将Sprite变成Mesh来制作。引擎通常会使用Hardware Instancing来优化渲染性能。

2.3、Model Particle

就是把一个模型当成粒子发射出去,其本身渲染由引擎本身的模型渲染系统进行,所以可以支持蒙皮动画、挂接等高阶功能。但是其开销也十分可观。

2.4、Trail Particle

posted @ 2021-01-15 17:13  KirinSB  阅读(195)  评论(0编辑  收藏  举报