参考
1、Shadow Map 原理和改进
2、【OpenGL】02 - OpenGL中的坐标系
3、矩阵理论 (这个是京东地址)
4、维基百科(文中的数学概念出处)
5、msdn mul
6、msdn matrix
7、unity shader lab built-in
8、nvidia bump map tutorial
9、Computing Tangent Space Basis Vectors for an Arbitrary Mesh
一、基础
坐标系的定义是:对于一个n维系统,能够使每一个点和一组(n个)标量构成一一对应的系统。
我们的出发点是三维欧几里得空间,或三维实内积空间。对于这样的三维空间,最常见的坐标系就是笛卡尔坐标系,指定了三个互相垂直的向量x、y、z,这样每个空间中的点就可以表示成
考虑原点的存在,则对点V有:
这里的x、y、z,就是三维实数空间的一组标准正交基。
原点和基,就唯一的定义了一个坐标系。
如果要展开讨论数学概念实在是太难懂,我们下面直接看unity中的各种坐标系来讨论各种情况。本文中讨论的unity坐标系有以下几个:
- 本地坐标系。local space
- 世界坐标系。world space
- 相机坐标系(观察坐标系)。view space
- 投影坐标系。projection space
- 切线坐标系。tangent space
这几个坐标系将在下面逐个讨论。
二、本地坐标系
1、shader中的本地坐标系,与 Editor中的本地坐标系
在unity中,有一个肉眼可见的本地坐标系,当你选中一个物体时,就会显示出其本地坐标系。
图1、Editor中的本地坐标系
那么问题来了,这个坐标系,是shader中的本地坐标系吗?
产生这样的疑问是自然的,3ds max中建立模型使用的坐标系是右手坐标系,而unity editor中显示的本地坐标系是左手坐标系,显然是不一样的:
图2、3ds Max 中的本地坐标系
我们这里不对3dmax中的轴做任何修改,并在导出时选择z向上(即和在3dmax中看到的一样),则导入到unity中,并将旋转等reset,看到的是这样的:
图3、导入unity后的本地坐标系
注意此时方向已经和在3dsmax中不一样了。
那么在shader中的坐标系到底是哪个坐标系呢?我们使用下面这样的shader来观察以下:
Shader "Custom/TestCoordShader" {
SubShader{
Tags{ "RenderType" = "Opaque" }
LOD 200
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
float4 pos : SV_POSITION;
float4 col : COLOR;
};
v2f vert(appdata_base v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.col = v.vertex;
return o;
}
float4 frag(v2f i) : COLOR{
return i.col.x; // i.col.y; //i.col.z;
}
ENDCG
}
}
//FallBack "Diffuse"
}
将 fragment 中的颜色值分别修改为 x y z,就可以得到下面的结果:
图4、shader中本地坐标系的x、y、z方向
可以看到vertex的x、y、z方向和editor中显示的坐标轴向一致,且更仔细的观察的话,会发现原点位置也一致。因此我们可以得出结论:shader中的本地坐标系,就是在editor中看到的本地坐标系。
2、本地坐标系:左手坐标系
这里需要注意的是,本地坐标系是一个左手坐标系系统。
图5、左手坐标系和右手坐标系
这里不展开讨论左右手坐标系的问题,在后面会再探讨这个东西。
三、世界坐标系
1、世界坐标系的形态
图6、位于原点的正方体
和各自为政的本地坐标系不同,在unity中,每个scene都有唯一存在的一个世界坐标系。同样的,
世界坐标系也是一个左手坐标系。一般常见的用法,是使得x正方向为正右方,y正方向为正上方,z正方向为正前方(常用而已),如图6中所示。
2、从本地坐标系转换到世界坐标系
现在要将本地坐标系的点、向量等转换到世界坐标系中。
1)不移动原点的坐标系变换
考虑我们上面对于坐标系的要素描述:原点和基。这里的本地坐标系和世界坐标系的不同就在于这两点。
首先不考虑原点的变化,先假定本地坐标系和世界坐标系共原点O。
而在基的方面,本地坐标系和世界坐标系是同一个三维线性空间中选取两组不同的基构成的坐标系,分为称为 local 基 和 world 基。
容易看出,从 local 基到 world 基是一个线性变换(反之亦然),参考《矩阵理论》中对过渡矩阵和坐标系一节的讨论,可以知道,
任意点 p 在世界坐标系下的位置,可以用其在本地坐标系下的位置,左乘 local 基 到 world 基的过渡矩阵的逆矩阵得到。而这个逆矩阵,实际上就是 world 基 到 local 基的过渡矩阵。因此有:
公式1、不移动原点时,本地坐标系到世界坐标系的坐标轴变换和点变换
注意到 world 基的正交特性,不失一般性的,令 X_world = (1, 0, 0),Y_world = (0, 1, 0),Z_world = (0, 0, 1),带入到上面的坐标轴变换公式,可以求出M_local->world,则(其中 X_local_in_world 是指该local 基在世界坐标系下的表示):
公式2、坐标系变换与坐标轴的关系
这个结论是可以推广的,即:
点P在坐标系V下的坐标,相当于其在坐标系U下的坐标,左乘矩阵T,其中T为 坐标系U的三个基(列向量)在坐标系V下的坐标构成的矩阵。
将这个矩阵记做 p 从本地坐标系变换到世界坐标系的变换矩阵 Trans:
公式3、本地坐标系变换到世界坐标系的变换矩阵 Trans
2)不移动原点的坐标系变换——举例
图7、本地坐标系到世界坐标系变换举例
以这个cube的本地坐标系为例,其坐标轴在世界坐标系下的表示分别为:
这样就可以得到过渡矩阵
3)仿射变换——考虑原点的移动
前面的推导都是基于原点不移动得到的,这样假设的原因在于
线性变换并不能够表示原点的移动。那么如果现在需要考虑这一变化,则需要引入仿射变换。
所谓仿射变换,就是线性变换加平移,其实就是在刚才的线性变换的基础上再加上原点的移动。
由于已经不是线性变换,那么使用线性变换矩阵就无法表示了,也就是说,现在已经无法写成 p' = T * p 的形式。因此,引入齐次线性空间的概念,即增加第4维w,用来辅助表示移动。
考虑这样的仿射变换,首先保持原点不变,进行线性变换,其线性变换矩阵为A,再将原点平移b,则在该仿射变换下,任意点P有:
公式4、仿射变换
关于齐次变换的更多细节,可以参考之前的 博客(这篇文章较老,冲突的概念以本文为准)。
仔细考虑这个表示平移的b向量,会发现他是转换后的线性空间V的原点所在位置,在原线性空间U下的坐标
图8、仿射变换的原点变换
现在回到本地坐标系向世界坐标系的变换过程中,则可以得到齐次变换下的变换矩阵:
公式4、仿射变换矩阵与坐标轴和原点
考虑到齐次空间的特性,点的w分量为1,向量的w分量为0,则可以进一步写为:
4)在unity中获得这个转换矩阵
Unity中已经提供了获得这个矩阵的方便形式,在script中使用
gameObject.transform.localToWorldMatrix
在shader中使用如下的矩阵:
_Object2World
5)转换法线
将点或者普通的向量从本地坐标系转换到世界坐标系,只要按照上面说的,用该转换矩阵左乘原坐标值就可以了,但是
对于法线,还需要一些特殊的处理。
为什么?
回答这个问题,就需要搬出法线的定义:
三维平面的法线是垂直于该平面的三维向量。曲面在某点P处的法线为垂直于该点切平面的向量。所以,在转换之后,还需要保持原有的垂直关系。
按照上面举例中的变换矩阵,假定某条法线为(1,1,0),与之垂直的一个向量(1,-1,0)。如果左乘变换矩阵,则法线变为(3.5,3.5,-0.7),向量变为(0.7,-3.5,-3.5)。变换之后的内积为 -7,即已经不再垂直。
其实从数学上看这是显然的:
公式5、使用变换矩阵变换法线——不再垂直
而从直观上,原因是这样的:
图9、使用变换矩阵变换法线——不再垂直
因此要正确的进行法线的变换,要使得保持 V 和 N 的内积为0,因此要让
N 左乘 Trans的逆转置矩阵,这样有:
公式6、用逆转置矩阵转换法线
6)获取逆转置矩阵的捷径——mul的秘密
上面提到要转换法线,需要使用从本地坐标系到世界坐标系的仿射变换矩阵 (即 _Object2World) 的逆转置矩阵,即应该使用 _World2Object 的转置矩阵。
那么在shader中完成这个变换,应有:
float4 worldPos = mul(_Object2World, v.vertex);
float4 worldNorm = mul(transpose(_World2Object), v.normal);
但在实际使用中,常常见到这样的写法:
float4 worldPos = mul(_Object2World, v.vertex);
float4 worldNorm = mul(v.normal, _World2Object);
这两种写法的结果是一样的吗?是的,这里就必须说明 mul 的用法了( 官方文档)。
简单来说,如果 mul 的第一个参数是矩阵 M,第二个参数是向量 V,则结果 Out 为(以vector4为例):
如果第一个参数是向量 V,第二个参数是矩阵 M,则结果 Out 为
则可以进行如下推导:
公式7、mul与转置矩阵
注意到输出的vector4,在数据格式上,转置与不转置实际上并没有区别,(shader中,转置实际上只对矩阵有效果)。因此,
mul(V, M) 就相当于 mul( tranpose(M), V )更进一步的,
对于正交矩阵,其转置矩阵等于逆矩阵,那么通过这个方法,还可以得到他的逆矩阵,这个特性我们后面还会看到。
四、观察坐标系
1、观察坐标系的形态
View space是观察者眼中的世界,可以认为是观察者的本地坐标系,以观察者的位置为原点。但在 《Shadow Map 原理和改进》中可以看到,
观察坐标系又与 editor 中看到的 camera 的本地坐标系有本质的区别:它是右手坐标系
其形态如下所示:
图10、观察坐标系
2、左手坐标系和右手坐标系的互相转换——数学运算的独立性
在考虑如何从世界坐标系转换到观察坐标系之前,我们要先思考一个问题:上一节的转换公式是在世界坐标系和本地坐标系——两个都是左手坐标系——的情况下推导的,那么
现在要在左手坐标系和右手坐标系之间进行转换了,那么之前的推导是否仍然有效?
这里就要展开来谈谈矩阵运算与左右手规则的关系了。
1)线性变换定理与左右手无关
上一节讨论的线性变换,是从一组基变换到另一组基,对基本身没有任何要求,他们甚至可以不满足互相正交的特性,到底构成的是左手坐标系还是右手坐标系,容易看到,对定理本身是完全没有影响的。
2)矩阵乘法 mul 与左右手无关
矩阵乘法的本质实际上就是对点或向量进行变换,即更换基向量,从线性变换定理与左右手无关也容易看出 mul 也不挑剔他所在的线性空间。
3)点积与左右手无关
点积需要在实内积空间中进行,要使得下面的点积计算方式生效,
需要 a 和 b 在同一个线性空间内,且基向量两两正交。
公式8、点积
注意到unity中使用的左手坐标系和右手坐标系中的基向量都是满足正交条件的,因此点积都可以正常进行。
4)叉乘与左右手规则的关系
叉乘是唯一需要注意的。其数学公式如下:
公式8、叉乘的数学计算方法
可以看到,其本身也是与基向量的形态独立的,因此
计算公式本身(例如,cross函数本身)并不因左右手坐标系的变化而变化。
但是,计算叉乘还有一个方法(以下这段出自
wiki):
公式9、叉乘
这里方向向量 n 的确定,通常会介绍右手规则。但实际上,右手规则仅适用于右手坐标系下的情况,在左手坐标系中,需要使用左手规则。总结来说,就是
叉乘的数学表达式是独立于左右手坐标系的。但如果使用X手判定准则来进行方向的判断,则在左手坐标系中应使用左手准则,在右手坐标系中使用右手准则。
3、从世界坐标系变换到观察坐标系
经过了上面的讨论,我们可以放心大胆的说,可以按照本地坐标系变换到世界坐标系的思路,再从世界坐标系变换到观察坐标系。
在 Script 中获得这个变换矩阵的方法是:
Camera.worldToCameraMatrix
在shader中,这一步的单独的矩阵是:
综合从本地坐标系到观察坐标系的变换矩阵:
UNITY_MATRIX_MV
4、值得注意的 z 分量和 w 分量
到目前为止,无论是本地坐标系到世界坐标系的变换,还是世界坐标系到观察坐标系的变换,都不影响点(或向量)的 w 分量,仍然保持为 1 (或0,对于向量)。
另外,可以注意下 z 分量,可以看到,由于 z 的正方向的缘故,所以观察者可见的所有点的 z 分量都小于0,且距离越远,负的值越大。
五、投影坐标系
1、投影坐标系的形态
投影坐标系是将观察者眼中的世界进行截取和归一得到的(比较详细的讨论可以参考 《【OpenGL】02 - OpenGL中的坐标系》),首先截取世界中,camera的平截体包含的部分,然后又再次变为一个左手坐标系(对透视投影来说,坐标系的概念可能已经拓展)
图11、投影坐标系:正投影和透视投影
这就产生了问题,为什么前面也是左手坐标系(本地坐标系,世界坐标系),后面也是左手坐标系(投影坐标系),中间为什么要费力的插一个右手坐标系呢(观察坐标系)?实际上,在Opengl中,本地坐标系和世界坐标系都是右手坐标系,也就是说,直到投影空间中才变换为左手。而unity中的本地坐标系和世界坐标系是左手系统,所以显得观察坐标系比较的特别。
2、正投影
正投影见第一张图,在从观察坐标系变换到投影坐标系之后,点 (x,y,z)的取值范围有:
其变换矩阵可以参考我的坐标系那篇博客:
公式10、正投影的投影变换矩阵
3、透视投影
透视投影的情况要复杂的多,在这一步,w 值开始正式发挥作用。
先直接来看变换矩阵(关于求法仍建议阅读坐标系一文)
公式11、透视投影的投影变换矩阵
对原观察坐标系下点P,变换到投影坐标系下坐标P’为:
公式12、透视投影变换下点的变换情况
如果只考虑(x,y,z),那么和正投影是相同的,但是多了第四维w,使得 z 坐标轴的指向产生了弯曲
图12、透视坐标系
4、从观察坐标系到投影坐标系——获取变换矩阵
自己想要计算的话,可以用上面提供的公式计算,unity本身也为我们提供了获取这个变换矩阵的方式。
在shader中,这一步单独的变换矩阵:
UNITY_MATRIX_P
在script中,如果使用 camera.projectionMatrix ,可能会发现与 shader 中的这个矩阵有些差异,这是由于,在我的电脑上(目前不清楚是否和显卡有关),实际使用的变换矩阵,
最终会使得 z 的取值范围为 [0, 1],和上面公式略有不同。
因此在script中,需要使用下面的代码来获取投影变换矩阵:
GL.GetGPUProjectionMatrix(c.projectionMatrix, false)
5、值得注意的w分量
之前的 w 分量一直为1,现在终于有了作用。在正投影变换后,w的值仍然是1;但是在透视投影变换之后,w的值等于观察坐标系下的 -z。另外,在透视坐标系中,在 shader 中实际应该使用的 x、y、z分量应当是 P 在变换后的坐标值的 Px、Py、Pz分量除以 w 来获取。
一说到除以就必须当心0除问题,w有可能是0吗?答案是不可能,这是由于透视投影的camera的视锥体中的点,其z的取值范围是 [-ZFar, -ZNear],不会取值到0。
六、切线空间
1、切线空间的形态
在做 bump map的时候,会提到切线空间 tangent space。切线空间的具体意义这里不展开讨论(关于这个问题,解释的最清楚的是 《OpenGL的法线贴图教程》),这里主要讨论坐标系变换问题。
切线空间的坐标轴分别为:
- X轴——切线 tangent
- Y轴——副切线 biTangent
- Z轴——法线 normal
其中,法线即我们平时所说的法线:
切线选取的是,与法线垂直的,沿着贴图uv的u变量增长方向的向量:
副切线则选取与这两个向量都垂直的,一般通过叉乘得到。从而构成坐标系:
需要注意的是,
unity中,实际获得的 B 向量,可能与这张图中的 B 向量反向。
2、在unity中获得和使用切空间变换矩阵
在unity中,有这样一个宏 TANGENT_SPACE_ROTATION,可以获得变换到切线空间
#define TANGENT_SPACE_ROTATION \
float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w; \
float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )
使用的时候,可以这样用:
TANGENT_SPACE_ROTATION;
o.viewDirForParallax = mul (rotation, ObjSpaceViewDir(v.vertex));
这个变换矩阵有很多细节值得说明,我们下面一个一个来看。
3、这是个什么样的矩阵?
这个问题换个问法就是,float3x3如何构造?
实际上,float3x3是行优先填充元素的,可以看 这里。所以,这个矩阵是:
注意,这个矩阵作为变换矩阵是非常奇怪的。回忆我们在 第三节世界坐标系 中,第2小节的结论:
- 点P在坐标系V下的坐标,相当于其在坐标系U下的坐标,左乘矩阵T,其中T为 坐标系U的三个基(列向量)在坐标系V下的坐标构成的矩阵。
因此变换矩阵应当是一个由三个列向量构成的矩阵,而现在却是三个行向量,这是为什么?
4、转置与逆矩阵
现在回答前面的问题:原因在于,实际上这是一个逆矩阵。
首先,可以注意到,三个基向量(T,B,N)都是单位向量,且两两正交。则其构成的矩阵是一个正交矩阵。对于正交矩阵,其转置矩阵就是他的逆矩阵,因此有:
注意看后面的这个矩阵,他显然满足前面的结论,
T、B、N都是在本地坐标系下的值,因此这个列向量构成的 [T B N] 矩阵,是将点从切线坐标系变换到本地坐标系的变换矩阵。 那么这个矩阵的逆矩阵,就是将点从本地坐标系变换到切线坐标系的变换矩阵。而由于正交,其逆矩阵就是其转置矩阵,所以从本地坐标系变换到切线坐标系的变换矩阵,就是我们在第2小节里面看到的情况。
5、左手坐标系?右手坐标系?
由于 B 向量是叉乘得到的,那么一个值得关注的问题就是,这个坐标系到底是左手规则还是右手规则?这就牵涉到之前的另外一个结论:对于叉乘,在左手坐标系中使用左手规则,在右手坐标系中使用右手规则,但是无论使用哪个规则,其数学计算表达式并不会有任何不同。
而现在,N、T都是在本地坐标系中的向量,而本地坐标系是一个左手坐标系,所以 N 和 T 的叉乘使用左手规则,那么
在没有其他变数的情况下,T、B、N应该如下,构成一个左手坐标系:
但是现在还有一个变量
tangent.w,他的取值为 1 或 -1。如果为1,则上面的结论不变,如果为-1,则B则会反向,从而构成一个右手坐标系。
这个变量的存在是由于B 向量通常还代表着 uv 中 v 增长的方向,所以需要调整 B 轴使得他和 v 增长的方向一致。(这里可参考 这篇文章)
至此,对于坐标系的讨论基本就告一段落了。