《Unity3D高级编程之进阶主程》第七章,渲染管线与图形学(一) - 图形学基础1

游戏项目编程除了业务逻辑、以及业务逻辑形成的模块、框架、架构、算法外,还有比较重要的图形学的支撑,很多人在业务层面打拼了很多年也始终无法突破的原因就是对图形学研究还不够深,这对优化游戏性能来说是一块比较大的内容,CPU和GPU两边的知识面都需要我们扩充和深入。本质上来说我们面对的是图形绘制和计算,业务逻辑虽然支撑起了图形绘制的骨架,底层知识仍然需要我们有足够多了了解,只是在使用了现代图形引擎之后,特别是使用了Unity3D之后,它为我们包装了很多工具和接口,用起来非常方便,初期我们可以不顾图形原理,但越到后期越不得不关注图形绘制原理。

===

我们也不能有偏见,其实CPU与GPU的知识面缺哪块都不行,没有逻辑不懂得各个模块的编写手法,或者不懂得框架的搭建,或者不懂架构,或者不懂算法,或者不懂图形绘制原理,都是无法完整的体现一个优秀程序员的知识面宽度,特别是对于需要在一个项目中担当起顶梁柱的主程同学,更是需要对于每个部分的都有深刻的了解。特别是图形绘制原理,它在初期编程中比较容易被忽视,也特别需要我们静下心来学习、理解和掌握。本非常想从最基本的讲起将基础部分写的详尽些,但篇幅有限只能挑些重点的说,并以言简意赅的形式说明部分细节。

特别挑出了图形学比较重要部分与Unity3D结合的来细致讲解一下。

Vector3 的意义

Vector3 有x,y,z三个变量,通常来说它是坐标的数据。不仅如此,它还可以代表距离、速度、位移、加速度、以及方向。

两个Vector3点坐标变量别分为a和b,相减后就能得到一个从b点到a点的向量c。向量c可以认为是一个长度为a点到b点的长度并且拥有b点到a点方向的向量,向量c与a和b同样是Vector3类型。

那么为什么a和b是坐标,而c则是向量呢。其实任何一个Vector3变量,确定它代表的是坐标还是向量,全靠我们如何定义它。我们可以定义a是坐标,也可以定义a是速度向量,至于如何定义以及怎么计算,全靠我们在公式中或者说在程序中怎么对待它,速度、位移、加速度都是一个道理,我们举个例子。

还是a和b定义为Vector3的坐标点,a减去b,除以常量1,可以认为我们向量除以了一个时间单位1秒得到了我们需要的有方向的速度c,此时c被认为是速度矢量,因为距离除以时间等于速度,c就是这个有方向的速度。再比如,坐标a点减去b点得到c向量,任意坐标加上c就会得到与a和b同样的相对位置,此时c就被认为是偏移量。

现在有四个坐标a,b,c,d,当a减去b除以1秒得到了a到b的时的速度,那么b减去c除以1秒得到也是b到c时的速度,这两个速度再相减依然是Vector3类型,而此时得到的结果Vector3已经不再代表坐标和速度,而是一个由两个速度相减得到的加速度结果了。

最终我们发现,我们赋予Vector3什么样的意义依赖于我们用它们来计算的过程。

为了能更深入理解Vector3的计算意义,我们挑选了几个Vector3比较重要的几何意义。

Vector3点乘的几何意义

向量a,与,向量b点乘的计算公式为

	a·b = (x1,y1,z1)·(x2,y2,z2) = x1*x2 + y1*y2 + z1*z2

除了上述的公式外点乘又有另外一个计算公式即为

	a·b = ||a||*||b||*cos(β)

我们可以用更直观的方式来表示它们,如图所示:

	缺图1

图中a向量与b向量的夹角为β,当a向量和b向量长度不变的情况下,计算出来的值越大,cos(β)值也就越大,要让cos(β)值越大则a和b的夹角就得越小。当β大于90度时,计算出来的是一个负数,也就说明b指向的方向其实与a指向的方向是相反的。

我们用这种方式可以得到一个判断依据,即当a和b两个向量点乘得到的数为正数时,两者的方向比较一致,并且这个结果越大a和b两个向量的方向越一致,得到最大正数时两者的方向完全相同。当点乘结果为负数时,a和b两个向量的方向则比较相反,得到结果的数值负得越厉害,a和b的方向相反的程度越厉害,最大负数时两者方向完全相反。

点乘除了可以判断两个向量的方向外,我们还可以用它来计算出β的角度,即:

	β = arcos( (a·b) / (|a|*|b|) )

现在我们用程序来表达计算a和b夹角的计算过程,其中Vector3.Dot为Unity3D中点乘的计算接口,Vector3.magnitude为矢量距离值,如下:

public static float Dot(Vector3 lhs, Vector3 rhs)
{
	return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;
}

public float magnitude { get { return Mathf.Sqrt(x * x + y * y + z * z); } }

我们用Mathf.Acos(float f)来获取反三角函数值,于是就有了:

float β = Mathf.Acos( Vector3.Dot(a,b) / (a.magnitude * b.magnitude) )

Vector3叉乘的几何意义

与Vector3点乘一样Vector3的叉乘也是向量与向量之间的计算公式,不同的是叉乘的结果不再是一个数值,而是一个同样维度的向量,即:

	a x b = (a1, a2, a3) x (b1, b2, b3) = (a2*b3 - a3*b3, a3*b1 - a1*b3, a1*b2 - a2*b1) = c

当两个向量a,b叉乘后,得到的是与a,b向量形成的平面垂直的向量c。其中c的长度又可以用另一个公式来表示,我们可以说 a x b的长度等于向量的大小与向量的夹角sin值的积,即:

	|c| = |a x b| = |a|*|b|*sin(β)

公式中c的长度大小与a和b向量的夹角有关。这样看来,我们可以反推为当c的长度为0时,a和b是相互平行的两个矢量,以及,当a和b的叉乘的模等于a的模乘以b的模时,a和b就为互相垂直。

此外a向量和b向量叉乘的模就是,a向量和b向量形成的四边形的面积,即a*b*sin(β) 就是a、b两条矢量形成的四边形的面积值,如图所示
	缺少图片
我们说四边形的面积公式为一条边乘以它的垂直高度即b* h,在四边形中垂直高度h怎么得到呢,我们可以用a* sin(β) 得到,因此就有了这个公式的演变,即:
	四边形面积 = |b| * h

	由于 h = |a| * sin(β) 代入后 => 四边形面积 = |b| * |a| * sin(β)

	=> 四边形面积 = |b| * |a| * sin(β) = |a x b|

经过几个公式的转换,我们得到向量叉乘后的模就是四边形的面积。这在3D中也是同样适用,因为两个向量确定一个平面,所以两个向量可以确定给一个对等边四边形的平面,从而可以用叉乘可以算出他们的面积。

在Unity3D中叉乘的函数就是Vector3.Cross,如下代码所示:

// Cross Product of two vectors.
public static Vector3 Cross(Vector3 lhs, Vector3 rhs)
{
    return new Vector3(
        lhs.y * rhs.z - lhs.z * rhs.y,
        lhs.z * rhs.x - lhs.x * rhs.z,
        lhs.x * rhs.y - lhs.y * rhs.x);
}

向量之间的投影

在几何计算过程中,我们经常用到‘投影’这种方式,在向量中投影也是常用的技巧。如图:

	缺少图片

图中向量b往向量a投影得到向量c,其实表象上来看c就是向量a乘以某个系数得到的。这个系数可以认为是c的模除以a的模得到的。即

	c = a * (|c|/|a|)
我们不知道c的值,但是c和b的夹角β又能得到计算cos的公式即:
	|c| = |b| * cos(β)

再套入到计算投影向量c的公式上去时,就变成了:

	c = a * (|b| * cos(β)/|a|)

	进一步,在除号的两边乘以a模 => c = a * (|b| * cos(β) * |a|) / (|a| * |a|)

	由于 |b| * cos(β) * |a| 组合起来等于a向量与b向量的点乘的结果,于是就有了

	投影向量c => c = a * (a · b) / (|a|^2)

至此经历了几个公式的转换,得到了b向量向a向量投影,得到c向量的公式,下面我们给出Unity3D的Vector3中向量间的投影程序:

// Projects a vector onto another vector.
public static Vector3 Project(Vector3 vector, Vector3 onNormal)
{
    float sqrMag = Dot(onNormal, onNormal);
    if (sqrMag < Mathf.Epsilon)
        return zero;
    else
        return onNormal * Dot(vector, onNormal) / sqrMag;
}
上述代码中,Dot(onNormal,onNormal) 很巧妙的计算了矢量模的平方,即 Dot(onNormal, onNormal) 等于onNormal*onNormal* cos(β),很明显β为0,cos(β)为1,于是就有了 Dot(onNormal,onNormal) =onNormal*onNormal

矩阵的意义

矩阵看上去有点复杂,很多人看到矩阵这个词就头疼,其实耐下心来研究,会发现矩阵是很可爱。矩阵五花八门,其实大部分五花八门的矩阵使用频率非常低,我们常用矩阵就只有几种。

在图形学计算中,我们常用的矩阵大小是2x2,3x3,4x4 矩阵,这种行与列的数量相同的矩阵,我们称为‘方阵’矩阵。在众多方阵矩阵中,常用到也是这些比较特殊的矩阵,比如对角矩阵,即只有行列号相同的位置有数字,其他位置都是0的方阵矩阵,以及单位矩阵,即行列号相同的对角线上的数字都为1,其他位置都为0的方阵矩阵。这两种特殊的矩阵简单易懂,在图形学计算过程中也是非常常用的矩阵。

矩阵间的计算我们可以罗列一下其实并不多。

	转置矩阵,就是把矩阵沿着对角线翻转一下,由于我们常用的是‘方阵’矩阵,所以转置矩阵后方阵矩阵还是同样的大小,只不过对角线两侧的数字对调了一下。

	矩阵乘法,如果是数字和矩阵相乘则,直接带入矩阵中的所有变量即可,这种标量的乘法其实就是扩大矩阵中所有的数值。如果是矩阵与矩阵相乘,A矩阵 x B矩阵,则需要一些附加条件,条件是矩阵A的列数必须与矩阵B的行数相等,否则无法相乘或者说相乘无意义。

矩阵相乘后得到的矩阵,里面每个位置Cij(即C矩阵的第i行第j列)都是A矩阵的第i行向量与B矩阵的第j列向量点乘的计算结果,如下我们拿2x2方阵相乘做示意:

	A x B = [a11, a12]  x  [b11, b12]
	        [a21, a22]     [b21, b22]

	= [a11*b11 + a12*b21, a11*b12 + a12*b22]
	  [a21*b11 + a22*b21, a21*b12 + a22*b22]

上图对矩阵乘法公式做出了清晰的描述,即Cij = Ai1 * B1j + Ai2 * B2j + Ai3 * B3j … 我们可以归纳为ij位置的值等于,A的i行向量与B的j列向量点乘的值。

	逆矩阵,逆矩阵由运算矩阵相乘而来,由于矩阵与矩阵相乘也会得到标准的单位矩阵,即对角线都是1其余都是0的方阵矩阵,于是就有了当一个矩阵与某个矩阵相乘等于单位矩阵时,这‘某个’矩阵就为该矩阵的‘逆矩阵’。

不过不是每个矩阵都有逆矩阵的,一个明显的例子是若矩阵的某一行或列上的元素都是0,用任何矩阵乘以该矩阵,结果都是一个带有某行或某列全零的矩阵,这样的结果一定不是单位矩阵。因此我们通常称一个有逆矩阵的矩阵为,这个矩阵可逆,相反如果这个矩阵没有逆矩阵,那么就称这个矩阵不可逆。

最后我们了解一下齐次矩阵,齐次矩阵也并没有我们想象的神秘,它只是从我们认知的角度上划分了矩阵分量和w分量,我们来了解下。

我们听说过齐次坐标,齐次坐标就是将一个原本是n维的向量用一个n+1维向量来表示,例如我们的三维向量用四维来表示即Vector3变为Vector4,除了x、y、z又多了一个w,齐次矩阵也是同样的道理,n维表达不了的事情用n+1维来表达,3 x 3 矩阵表达不了的事情用 4 x 4 来表达,下面我们就由浅入深来理解齐次坐标与齐次矩阵。

(引用自《齐次坐标》)在欧几里得几何空间里,两条平行线永远都不会相交。但是在投影空间中,如右图中的两条铁轨在地平线处却是会相交的,因为在无限远处它们看起来相交于一点。

[缺图,两条并行轨道在图中远处相交]

在欧几里得(或称笛卡尔)空间里描述2D/3D 几何物体是很理想的,但在投影空间里面却并不见得。我们用 (x, y) 表示笛卡尔空间中的一个 2D 点,而处于无限远处的点 (∞,∞) 在笛卡尔空间里是没有意义的。投影空间里的两条平行线会在无限远处相交于一点,但笛卡尔空间里面无法搞定这个问题(因为无限远处的点在笛卡尔空间里是没有意义的),因此数学家想出齐次坐标这个点子来了。由 August Ferdinand Möbius 提出的齐次坐标(Homogeneous coordinates)让我们能够在投影空间里进行图像和几何处理,齐次坐标用 N + 1个分量来描述 N 维坐标。比如,2D 齐次坐标是在笛卡尔坐标(X, Y)的基础上增加一个新分量 w,变成(x, y, w),其中笛卡尔坐标系中的大X,Y 与齐次坐标中的小x,y有如下对应关系:

X = x/w

Y = y/w

笛卡尔坐标中的点 (1, 2) 在齐次坐标中就是 (1, 2, 1) 。如果这点移动到无限远(∞,∞)处,在齐次坐标中就是 (1, 2, 0) ,这样我们就避免了用没意义的”∞” 来描述无限远处的点。

那么为什么叫齐次坐标呢?前面提到,我们分别用齐次坐标中的 x 和 y 除以 w 就得到笛卡尔坐标中的 x 和 y,如图所示:

[缺图]

仔细观察下面的转换例子,可以发现些有趣的东西:

[缺图]

上图中,点 (1, 2, 3), (2, 4, 6) 和 (4, 8, 12) 对应笛卡尔坐标中的同一点 (1/3, 2/3)。 任意数量积的(1a, 2a, 3a) 始终对应于笛卡尔坐标中的同一点 (1/3, 2/3)。因此这些点是“齐次”的,因为他们始终对应于笛卡尔坐标中的同一点。换句话说,齐次坐标描述缩放不变性(scale invariant)。

向量在空间中的运用还不够广泛,矩阵可以表达空间中的缩放、旋转、切变,但无法表达偏移,因此我们增加一个维度的矩阵叫做齐次矩阵来表达当下维度的偏移,即齐次矩阵。

由矩阵带来的旋转,缩放,投影,镜像,和仿射。

很多人都难以理解矩阵为什么能做到旋转和缩放,或者怎么理解矩阵的旋转和缩放,这节我们就来讲讲如何理解矩阵的旋转和缩放。

我们先来了解下,向量与矩阵的乘法。

首先我们要明白,我们用的都是方阵矩阵,即2x2,3x3,4x4的矩阵。由于矩阵与矩阵相乘必须是前置的列与后置的行数要相等才有意义,向量与矩阵相乘也是一样,如果向量不是前置的那个即左乘矩阵,或者向量是以竖列表达方式右乘矩阵,对于向量与矩阵的乘法来说都是无意义的,即如下图所示:

	[缺图]

	[缺图]

图中,第一个例子,当向量左乘矩阵,是无定义的结果,第二个例子,向量以竖列的表达方式右乘矩阵,也是无定义的结果,只有第三个例子和第四个例子,向量横向表达右乘矩阵,或竖向表达左乘矩阵,才有意义。

我们知道向量与矩阵的乘法公式,其中我们最常用的是三维向量与三维方阵矩阵相乘,即公式为:

	(x,y,z) x 	[m11,m12,m13]
				[m21,m22,m23]
				[m31,m32,m33]

	= (x*m11 + y*m21 + z*m31, x*m12 + y*m22 + z*m32, x*m13 + y*m23 + z*m33)

至于向量的行向量的表达方式,和列向量的表达方式,其实都是可以行得通的,那么我们为什么要选择行向量呢,因为行向量表达更方便,无论是书写还是计算,行向量更加适合我们人类的习惯,因此我们选择了行向量来表达向量。

理解旋转矩阵

理解旋转矩阵我们应该从 2x2 矩阵讲起,比起3x3矩阵2x2矩阵更加容易理解。我们假设一个 2x2 的矩阵:

	[2, 1]
	[-1,2]

我们可以认为它是由两个行向量a,b构成,a为(2,1),b为(-1,2),即

	[2, 1]  等  [a]
	[-1,2]  于  [b]

在平面坐标系中,a和b的表达如图:

	缺少图片

图中a,b向量展现在2D坐标系中是两个互相垂直的向量。我们把这两个a,b向量可以看做是,从两个标准向量(1,0)和(0,1)旋转并且放大后的向量,即如图下所示:

	缺少图片

图中,(1,0)和(0,1)向量,缓缓的旋转,并且放大,逐步变成了(2,1)和(-1,2),即从原来的(1,0)和(0,1)向量上,旋转了β度并放大了2.236倍。我们可以认为矩阵由标准矩阵旋转并缩放而来,这是矩阵的几何解释。对于标准矩阵来说,旋转缩放后形成了另一个矩阵,这个结果矩阵就是我们计算的‘变换矩阵’。对于任何一个向量来说,乘以‘变换矩阵’就能得到我们所要表达的旋转和缩放值,我们举例的矩阵就表达了向量旋转β度和2.236倍的缩放,那么任何二维向量乘以这个矩阵,就会得到在标准坐标系中以标准轴为基准旋转β度,并且以标准轴为基准放大2.236倍。

这样看矩阵还是不够直观,我们来看看更直观的表达方式,如果一个矩阵要表达旋转β度,那么它的a,b的向量该是如下图:

	缺少图片

a就是 [cosβ, sinβ],b就是 [-sinβ, cosβ],它们分辨表达了标准向量(1,0)和(0,1)旋转β度后的向量。

	[cosβ,  sinβ]
	[-sinβ, cosβ]

因此任何向量乘以这个这个旋转矩阵都会在标准坐标系中以标准轴为基准旋转β度。

理解了二维空间的矩阵旋转原理,我们延伸到三维空间就容易多了。

三维空间的矩阵也可以像二维空间一样理解,用三个向量来表示空间中的矩阵,即

	[a]   [Ax, Ay, Az]
	[b] = [Bx, By, Bz]
	[c]   [Cx, Cy, Cz]

一个绕x轴旋转β度的旋转矩阵为:

	[1, 0,     0]
	[0, cosβ,  sinβ]
	[0, -sinβ, cosβ]

那么一个绕y轴旋转β度的旋转矩阵为:

	[cosβ, 0, -sinβ]
	[0, 1, 0]
	[sinβ, 0, cosβ]

于是一个绕z轴旋转β度的旋转矩阵为:

	[cosβ, sinβ, 0]
	[-sinβ, cosβ, 0]
	[0, 0, 1]

用β度所形成的向量来表达坐标空间中的旋转矩阵,这样的表达可以帮助我们更加清晰的理解旋转矩阵的几何表达意义。上述只是对某一个轴进行旋转β度,如果我们要对某个向量做各个方向轴上的旋转,比如在x轴上旋转20度,再在y轴上旋转30度,最后在z轴上旋转15度,相当于这个向量在坐标系中旋转了20°,30°,15°,这时我们该怎么办?其实有了上述对各轴的旋转矩阵,我们就能很容易计算出各方向上的矩阵,就如上面提的问题,在各轴上都有旋转角度,我们就可以用先旋转x轴,再旋转y轴,最后旋转z轴的方式来计算最后的结果,如图:

	缺少图片

图中,c向量乘以x轴旋转矩阵,再乘以y轴旋转矩阵,最后乘以z轴旋转矩阵,即:

c * Mx * My * Mz = c`(得到旋转后的结果向量)

其中Mx为以x轴为基准旋转20度的旋转矩阵,My为以y轴为基准旋转30度的旋转矩阵,Mz为以z轴为基准旋转15度的旋转矩阵。c乘以Mx得到旋转x轴后的向量,再乘以My得到旋转y轴后的向量,最后乘以Mz得到旋转z轴后的向量,最终得到结果。矩阵乘法具有结合律,也就是说:

c * Mx * My * Mz = c * (Mx * My * Mz)

其中 Mx * My * Mz 得到的结果就是我们需要的‘x轴上旋转20度,再在y轴上旋转30度,最后在z轴上旋转15度’的旋转变化矩阵。任何一个向量乘以我们计算出来的这个变化矩阵都会同样在各轴上旋转同样的度数。

绕任意轴旋转

我们前面说的都是绕标准轴即x轴,y轴,z轴的旋转,其实这个标准空间上得到的矩阵还远远不够我们需要的计算,很多时候我们需要计算绕任意向量或者说绕任意轴的旋转。

我们来推导一下绕轴 N 旋转角度 β 的矩阵,先定义旋转矩阵为 F(N, β),v为需要旋转的向量,于是公式满足:

	v * F(N, β) = v' (v向量绕 N 轴,旋转β度后的结果v‘)

这个公式中v向量为已知,N绕轴为已知,β绕轴度数为已知,还需要知道哪些变量,如图所示:

	缺少图片

图中,v 到 N 的投影分量可以计算得到 v1,进而可以计算得到 v 到 v1 的垂直向量 v2,最后用垂直向量 v2 与角度 β 可以计算得到 v’ 与 v 到 N 的投影分量的垂直向量 v‘’,最后由 v‘’ 和 v1 计算得到 v‘。其中用到的计算方式有,向量投影计算公式,向量旋转计算公式,垂直向量计算公式,最后结果为:

	缺少图片

一个看起来很复杂的,只要一步步推导就能得到的结果矩阵,虽然推导的结果公式是一个相对复杂的公式,但其实结果并不重要,很多时候我们也记不住,只是我们用心推导的过程比较重要,也只有这样我们可以加深对旋转矩阵的理解,推导的过程也运用到了很多其他方面的知识巩固了投影等公式。’用进废退‘的我们很容易忘记这些公式,但只要印象深刻我们就知道有这些公式的存在,它们的原理是什么,在需要用到时搜索一下很快就能进入逻辑数学的心流状态。

参考文献:

《3D数学基础:图形与游戏开发》 《齐次坐标》Daisycv http://www.360doc.com/content/10/1226/10/3843418_81406265.shtml

· 书籍著作, Unity3D, 前端技术

感谢您的耐心阅读

Thanks for your reading

  • 版权申明

    本文为博主原创文章,未经允许不得转载:

    《Unity3D高级编程之进阶主程》第七章,渲染管线与图形学(一) - 图形学基础1

    Copyright attention

    Please don't reprint without authorize.

  • 微信公众号,文章同步推送,致力于分享一个资深程序员在北上广深拼搏中对世界的理解

    QQ交流群: 777859752 (高级程序书友会)