模型变换
首先我们可以看一下在Unity中如何用脚本旋转一个GameObject,其中一个可行的办法就是如下。
void Awake()
{
cube1 = GameObject.CreatePrimitive(PrimitiveType.Cube);
cube1.transform.position = new Vector3(0.75f, 0.0f, 0.0f);
cube1.transform.Rotate(90.0f, 0.0f, 0.0f, Space.Self);
cube1.GetComponent().material.color = Color.red;
cube1.name = "Self";
cube2 = GameObject.CreatePrimitive(PrimitiveType.Cube);
cube2.transform.position = new Vector3(-0.75f, 0.0f, 0.0f);
cube2.transform.Rotate(90.0f, 0.0f, 0.0f, Space.World);
cube2.GetComponent().material.color = Color.green;
cube2.name = "World";
}
void Update()
{
cube1.transform.Rotate(xAngle, yAngle, zAngle, Space.Self);
cube2.transform.Rotate(xAngle, yAngle, zAngle, Space.World);
}
其中用到的函数就是Rotate,它的原型如下:
public void Rotate(Vector3 eulers, Space relativeTo = Space.Self);
public void Rotate(float xAngle, float yAngle, float zAngle, Space relativeTo = Space.Self);
The implementation of this method applies a rotation ofzAngle
degrees around the z axis,xAngle
degrees around the x axis, andyAngle
degrees around the y axis ( in that order).
根据这个函数的描述,我们可以知道,Unity实现这个函数是通过先绕z轴旋转,然后绕x轴旋转,最后绕y轴旋转。也就是:
$$ P'= M_{rotateY}M_{rotateX}M_{rotateZ}P $$
从模型空间转换到世界空间需要考虑Transform的继承结构。若不考虑父节点,依循先缩放、在旋转、最后平移的原则,模型空间的某点应当以如下方式换算:也就是:
$$ P'= M_{translate}M_{rotateY}M_{rotateX}M_{rotateZ}M_{scale}P $$
如果Transform仍有父节点,则继续按照如上原则计算,直至抵达世界空间。
Unity 3D中的模型空间坐标系和世界空间坐标系
Unity 3D在各平台上,顶点的模型坐标系都统一使用左手坐标系。在顶点缓冲区中,把顶点坐标定义为w分量为1的四维齐次坐标。因为w分量为1,所以等同于三维的笛卡尔坐标。令在模型空间的顶点坐标为vInModelSpace = (x, y, z, 1)
,变换到世界空间的坐标vInWorldSpace = (wx, wy, wz, 1)
。通过使用内置变量unity_ObjectToWorld,可以实现这个转换,代码如下:
float4 vInWorldSpace = mul(unity_ObjectToWorld, vInModelSpace)
观察变换
观察变换需要定义摄像机。通常,摄像机有三个参数,即Eye,LookAt和Up。 Eye指摄像机在世界空间中位置的坐标;LookAt指摄像机在世界空间观察的位置的坐标;Up则指在世界空间中,近似于摄像机朝上的方向分量,通常定义为世界坐标系的y轴。给定这三个参数即可定义观察空间,观察空间的原点位于Eye处。这个空间可以用以下表示:
$$ \{U,V,N\} $$
分别对应x,y,z坐标轴。在观察空间中,摄像机位于原点位置且指向N,即摄像机的观察方向,也称为朝前方向(forward)为N。
$$ N= \frac{LookAt-Eye}{\mid\mid LookAt-Eye \mid\mid} $$
$$ U = \frac{N \times Up}{\mid\mid N \times Up\mid\mid} $$
$$ V = U \times N $$
观察矩阵及其推导过程(左手坐标系下)
理论上,把模型从世界空间变换到观察空间,应该是在给定观察坐标系的情况下,保持观察坐标系不懂;然后计算出模型的顶点相对于观察坐标系3个坐标轴平移了多少位移,旋转了多少度,计算出平移矩阵和旋转矩阵,最终组合成观察变换矩阵。但是这样虽然计算平移量比较简单,但是计算旋转角度就会比较麻烦。因此,我们可以换一个思路,我们可以想像世界空间所有的模型都和观察坐标系“锚定”在一起,然后通过平移旋转观察坐标系使之最终与世界坐标系重合,当观察坐标系平移旋转时,与之“锚定”的模型顶点也随之平移旋转,最终世界空间与观察空间重合,模型顶点变化后得到的世界坐标值,实际上就是它在观察坐标下的值。
假设LookAt和Eye点在世界空间的坐标值是(0, 2, 10)
和(0, 3, 20)
。我们把点LookAt和Eye“锚定”在一起,然后把观察坐标系从Eye点处移动到世界坐标系原点O处。
$$ M_{t} = \left[\begin{matrix} 1 & 0 & 0 & -Eye_x \\ 0 & 1 & 0 & -Eye_y \\ 0 & 0 & 1 & -Eye_z \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] $$
$$ LookAt_{translate}= M_{translate} LookAt_{world} \left[\begin{matrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & -3 \\ 0 & 0 & 1 & -20 \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] \left[\begin{matrix} 0 \\ 2 \\ 10 \\ 1 \\ \end{matrix} \right]= \left[\begin{matrix} 0 \\ -1 \\ -10 \\ 1 \\ \end{matrix} \right] $$
完成平移之后,需要在保持观察坐标系的3条坐标轴U,V,N始终垂直的前提下,旋转它们并使其朝向世界坐标系的3条坐标轴x,y,z完全重合。也就是说,要构造一个矩阵,使得坐标轴坐标轴U,V,N的方向向量右乘这个矩阵的结果分别等于x,y,z的方向向量。
$$ M_{t} = \left[\begin{matrix} U_x & U_y & U_z & 0 \\ V_x & V_y & V_z & 0 \\ N_x & N_y & N_z & 0 \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] $$
$$ M_{t}U = \left[\begin{matrix} U_x & U_y & U_z & 0 \\ V_x & V_y & V_z & 0 \\ N_x & N_y & N_z & 0 \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] \left[\begin{matrix} U_x \\ U_y \\ U_z \\ 0 \\ \end{matrix} \right]= \left[\begin{matrix} U_xU_x + U_yU_y + U_zU_z \\ V_xU_x + V_yU_y + V_zU_z \\ N_xU_x + N_yU_y + N_zU_z \\ 0 \\ \end{matrix} \right] $$
我们可以看到结果实际上就是向量U分别与向量U,V,N的点积。因为U,V,N是互相垂直的,所以结果向量实际上就是$[1\quad0\quad0\quad 0]^T$。同理可得,$M_tV$和$M_tN$的值分别为 $[0\quad1\quad0\quad 0]^T$和$[0\quad0\quad1\quad 0]^T$。因此到了这一步,观察矩阵$M_{view}$为:
$$ M_{view} = M_{rotate}M_{translate} = \left[\begin{matrix} U_x & U_y & U_z & 0 \\ V_x & V_y & V_z & 0 \\ N_x & N_y & N_z & 0 \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] \left[\begin{matrix} 1 & 0 & 0 & -Eye_x \\ 0 & 1 & 0 & -Eye_y \\ 0 & 0 & 1 & -Eye_z \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] = \left[\begin{matrix} U_x & U_y & U_z & -Eye \cdot U \\ V_x & V_y & V_z & -Eye \cdot V \\ N_x & N_y & N_z & -Eye \cdot N \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] $$
注意,到这一步变换其实还没有结束,因为Unity的观察坐标系实际上是使用的右手坐标系(可能是因为OpenGL?),这两种坐标系的x轴和y轴是重合的,z轴则相反。因此,还必须对z轴取反,才能得到最后的矩阵$M_{view}$.
$$ M_{view} = M_zM_{rotate}M_{translate} = \left[\begin{matrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & -1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] \left[\begin{matrix} U_x & U_y & U_z & 0 \\ V_x & V_y & V_z & 0 \\ N_x & N_y & N_z & 0 \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] \left[\begin{matrix} 1 & 0 & 0 & -Eye_x \\ 0 & 1 & 0 & -Eye_y \\ 0 & 0 & 1 & -Eye_z \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] = \left[\begin{matrix} U_x & U_y & U_z & -Eye \cdot U \\ V_x & V_y & V_z & -Eye \cdot V \\ -N_x & -N_y & -N_z & Eye \cdot N \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] $$
Unity 3D中的观察空间坐标系
在各平台下,Unity 3D的观察坐标系统一使用右手坐标系。令在世界空间中顶点的坐标为vInWorldSpace
= (wx, wy, wz, 1)
,观察空间中的顶点坐标vInViewSpace = (vx, vy, vz, 1)
。通过内置变量unity_MatrixV,我们可以把顶点从给予左手坐标系的时间空间交换到右手坐标系的观察空间, 代码如下:
float4 vInViewSpace = mul(unity_MatrixV, vInWorldSpace);
投影变换
通常摄像机的取景范围(或者说是视野范围)是有限的。在渲染流水线中,通常使用视截体(view frustum)来定义这个取景范围。视截体是一个正棱台,其两个底面平行,且宽高比例相等。可以用4个参数定义,即fovY,aspect,near,far。
投影矩阵及其推导过程
通过投影变换可以将正棱台状的视截体转换乘一个轴对齐(axis-aligned)的立方体,该立方体所框定的空间就是裁剪空间。立方体的x和y的取值范围都是[-1, 1]
,z的取值范围是[0, 1]
。
也可以这么看
$$ A + \frac{B}{NearZ} = 0 \to A = -\frac{B}{NearZ} $$
$$ A + \frac{B}{FarZ} = 1 \to A= 1 - \frac{B}{FarZ} $$
$$ \frac{B}{NearZ} = \frac{B}{FarZ} - 1 \to \frac{B(FarZ-NearZ)}{NearZ\cdot FarZ} = -1 $$
$$ B = -\frac{NearZ\cdot FarZ}{FarZ - NearZ} $$
$$ A = \frac{FarZ}{FarZ - NearZ} $$
Unity 3D的裁剪空间坐标系
在各平台下,Unity 3D的裁剪空间坐标系统统一使用左手坐标系,并且在未经过透视除法之前是一个不等价于三维笛卡尔坐标的四维齐次坐标系。令在给予右手坐标系的观察空间中的顶点为vInViewSpace = (vx, vy, vz, 1)
,经投影变换后,交换到基于左手坐标系的裁剪空间的坐标为vInClipSpace = (cx, cy, cz, cw)
。由于投影变换不是仿射变换,因此顶点在裁剪空间中的齐次坐标vInClipSpace
的w分量不为1。
float4 vInClipSpace = UnityViewToClipPos(float3(vInViewPos.x, vInViewPos.y, vInViewPos.z));