阐明:
注意几点:
0 矩阵右乘行向量和矩阵左乘列向量,两个矩阵互为逆矩阵
1 普通转换和mul,mul函数计算列矩阵时左乘矩阵,计算行矩阵时右乘矩阵
2叉乘法和左右手系,左手系用左手,右手系用右手,axb的四个手指指向a,旋转到b(向小和方向转动180度的两个角),拇指的方向就是叉积的方向
3 unity观测系统的z方向,unity观测系统是右手系,其他都是局部坐标,世界坐标,投影坐标都是左手系,所以观测系统轴是反的
4 投影系统中w与uv延伸方向的关系,w=1或-1 uv延伸为正向或反向
一、Shadow Map的原理及改进
2. [OpenGL] 02 - OpenGL中的坐标系
3.矩阵论(这是京东的地址)
4.维基百科(文中数学概念来源)
5. msdn 多路复用
6.msdn矩阵
7. Unity shader lab内置
8. nvidia凹凸贴图教程
9. 计算任意网格的切线空间基向量
1. 基础知识
坐标系的定义是:对于一个n维系统,可以使每个点和一组(n)标量形成一一对应的系统。
我们的出发点是三维欧氏空间,或者说三维实内积空间。对于这样的三维空间,最常见的坐标系是笛卡尔坐标系,它指定了三个相互垂直的向量x,y,和 z,这样每个空间中的点可以表示为
考虑原点的存在,则对于V点:
这里的x、y、z是三维实数空间的一组正交基。
原点和底点唯一地定义了一个坐标系。
如果讨论数学概念太难,那我们直接unity看各种坐标系,讨论各种情况。 本文讨论的unity坐标系如下:
下面将对这些坐标系一一进行讨论。
二、局部坐标系 1、shader中的局部坐标系和Editor中的局部坐标系
在unity中,有一个肉眼可见的局部坐标系。 当您选择一个对象时,将显示其局部坐标系。
图 1. 编辑器中的局部坐标系
那么问题来了,这个坐标系是shader中的局部坐标系吗?
有这样的疑惑是很自然的。 在3ds max中建立模型所用的坐标系是右手坐标系,而unity编辑器中显示的局部坐标系是左手坐标系,两者有明显区别:
图 2. 3ds Max 中的局部坐标系
这里我们不对3dmax中的轴做任何修改,导出的时候选择z向上(就是和3dmax中看到的一样),然后导入到unity中,重新设置旋转等等,大家看到的是这个:
图3 导入unity后的局部坐标系
注意此时的方向与3dsmax中的方向不同。
那么shader中的坐标系是哪个坐标系呢? 我们使用以下着色器来观察以下内容:
[cpp]
Shader"Custom/TestCoordShader"{ SubShader{ Tags{"RenderType"="Opaque"} LOD200 Pass{ CGPROGRAM #pragmavertexvert #pragmafragmentfrag #include"UnityCG.cginc" structv2f{ float4pos:SV_POSITION; float4col:颜色; }; v2fvert)(appdata_base { v2fo; o.pos=mul(UNITY_MATRIX_MVP,v.vertex); o.col=v.vertex; return; } float4frag(v2fi):COLOR{ returni.col.x;//i.col. y;// i.col.z; } ENDCG } } //回退“扩散” }
将fragment中的颜色值分别修改为xyz,可以得到如下结果:
图 4. 着色器中局部坐标系的 x、y 和 z 方向
可以看到顶点的x、y、z方向与编辑器中显示的坐标轴是一致的,仔细观察会发现原点位置也是一致的。 所以我们可以得出结论,着色器中的局部坐标系就是在编辑器中看到的局部坐标系。
2.局部坐标系:左手坐标系
这里需要注意的是,局部坐标系是左手坐标系。
图 5. 左手坐标系和右手坐标系
左手坐标系的问题我们这里先不讨论,以后再讨论这个问题。
三、世界坐标系 1、世界坐标系的形状
图 6. 原点处的立方体
不同于独立的局部坐标系,在unity中,每个场景都有一个唯一的世界坐标系。 同样,世界坐标系也是左手坐标系。 常见的用法是让x的正方向在右,y的正方向在正上方,z的正方向在正前方(常用),如图6所示。
2.从局部坐标系转换到世界坐标系
现在需要将局部坐标系的点、向量等变换到世界坐标系中。
1)不移动原点的坐标系变换
考虑我们上面对坐标系元素的描述:原点和底面。 这里的本地坐标系和世界坐标系的区别就在于这两点。
首先不考虑原点的变化,假设局部坐标系和世界坐标系有一个共同的原点O。
就基而言,局部坐标系和世界坐标系是在同一三维线性空间中选取两个不同的基组成的坐标系,称为局部基和世界基。
很容易看出它是从局部基到世界基的线性变换(反之亦然)。 参考《矩阵论》中关于转移矩阵和坐标系的讨论,我们可以知道任意一点p在世界坐标系中的位置,可以通过其在局部坐标系中的位置乘以逆矩阵得到从本地基地到世界基地的过渡矩阵。 而这个逆矩阵其实就是world base到local base的过渡矩阵。 所以有:
公式1 原点不动时,从局部坐标系到世界坐标系的坐标轴变换和点变换
注意world basis的正交性,不失一般性,令X_world = (1, 0, 0), Y_world = (0, 1, 0), Z_world = (0, 0, 1),带入上面可以得到M_local->world的坐标轴变换公式,那么(其中X_local_in_world指的是局部基地在世界坐标系中的表示):
公式2、坐标系变换与坐标轴的关系
这个结论可以推广,即:点P在坐标系V中的坐标等价于它在坐标系U中的坐标3D植物,左乘矩阵T,其中T为三个基(列向量)坐标系U中坐标系V下的坐标构成的矩阵。
将该矩阵表示为局部坐标系到世界坐标系的变换矩阵Trans:
公式3,局部坐标系到世界坐标系的变换矩阵Trans
2)不移动原点的坐标系变换——举例
图 7. 局部坐标系到世界坐标系转换示例
以该立方体的局部坐标系为例,其坐标轴在世界坐标系中的表示为:
这样就可以得到转移矩阵
3)仿射变换——考虑原点的移动
前面的推导都是建立在原点不动的基础上的。 之所以这样假设,是因为线性变换不能代表原点的移动。 所以如果现在需要考虑这种变化,就需要引入仿射变换。
所谓仿射变换就是线性变换加上平移。 其实就是在刚才的线性变换的基础上进行原点的移动。
由于不再是线性变换,所以不能用线性变换矩阵来表示,即现在不能写成p' = T * p的形式。 因此引入了齐次线性空间的概念,即加入第四维w来辅助表示运动。
考虑这样的仿射变换,首先保持原点不变,进行线性变换。 线性变换矩阵为A,然后原点平移b。 在这种仿射变换下,任意点 P 有:
公式4、仿射变换
更详细的齐次变换可以参考之前的博客(这篇文章比较老,概念冲突以本文为准)。
仔细考虑代表平移的b向量,会发现它是转换后的线性空间V的原点位置,与原线性空间U下的坐标
图 8. 仿射变换的原点变换
现在回到局部坐标系到世界坐标系的变换过程,可以得到齐次变换下的变换矩阵:
公式4、仿射变换矩阵与坐标轴与原点
考虑到齐次空间的特点,点的w分量为1,向量的w分量为0,则可以进一步写成:
4)统一得到这个变换矩阵
Unity 已经提供了一种方便的形式来获取这个矩阵,在脚本中使用
[锐利]
游戏对象.transform.localToWorldMatrix
在着色器中使用以下矩阵:
[cpp]
_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中完成这个变换,应该有:
[cpp]
float4worldPos=mul(_Object2World,v.vertex); float4worldNorm=mul(转置(_World2Object),v.normal);
但是在实际使用中,我们经常会看到这样的写法:
[cpp]
float4worldPos=mul(_Object2World,v.vertex); float4worldNorm=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原理与改进》中可以看到,观察坐标系和小编看到的相机局部坐标系有本质区别:是右手坐标系
它的形式如下:
图 10. 观测坐标系
2、左手坐标系与右手坐标系的相互转换——数学运算的独立性
在考虑如何从世界坐标系转换到观测坐标系之前,我们不得不思考一个问题:上一节的转换公式是在世界坐标系和局部坐标系的情况下——都左——手坐标系——推导,所以现在需要在左手坐标系和右手坐标系之间进行转换,之前的推导还有效吗?
这里我们就展开说一下矩阵运算和左手定则的关系。
1)线性变换定理与左右手无关
上一节讨论的线性变换是从一组基变换到另一组基。 对基地本身没有要求。 它们甚至可能不满足相互正交的特性。 无论它们构成左手坐标系还是右手坐标系,很容易看出对定理本身绝对没有影响。
2)矩阵乘法mul与左右手无关
矩阵乘法的本质其实就是对点或者向量进行变换,即替换基向量。 从线性变换定理不难看出mul对自己所在的线性空间并不挑剔。
3)点积与左右手无关
点积需要在实数内积空间进行。 为了使下面的点积计算方法有效,a和b需要在同一个线性空间中,并且基向量相互正交。
公式 8,点积
注意unity中使用的左手坐标系和右手坐标系中的基向量满足正交条件,所以可以正常进行点积。
4)叉积与左右手定则的关系
叉积是唯一需要注意的事情。 其数学公式如下:
公式8,叉积的数学计算方法
可以看出它也与基向量的形状无关,所以计算公式本身(例如交叉函数本身)不会因为左手坐标系的变化而变化。
但是,还有另一种计算叉积的方法(以下段落来自wiki):
公式 9,叉积
这里方向向量n的确定通常要引入右手法则。 但实际上,右手定则只适用于右手坐标系的情况,在左手坐标系下,需要用到左手定则。 综上所述unity 左手坐标系,叉积的数学表达式与左右手坐标系无关。 但是,如果用X手准则判断方向,则在左手坐标系中应使用左手准则,在右手坐标系中应使用右手准则。
3.从世界坐标系到观测坐标系的变换
经过上面的讨论,我们可以自信地说,我们可以从局部坐标系变换到世界坐标系,再从世界坐标系变换到观测坐标系。
在Script中得到这个变换矩阵的方法是:
[cpp]
相机.worldToCameraMatrix
在着色器中,此步骤的单独矩阵是:
[cpp]
UNITY_MATRIX_V
合成局部坐标系到视图坐标系的变换矩阵:
[cpp]
UNITY_MATRIX_MV
4. 值得注意的 z 和 w 组件
到目前为止,无论是从局部坐标系到世界坐标系的变换,还是从世界坐标系到观测坐标系的变换,都没有影响点(或向量)的w分量,它仍然为1(或0,对于矢量)。
另外,可以注意z分量。 可以看出,由于z为正方向,观察者可见的所有点的z分量均小于0,且距离越远负值越大。
五、投影坐标系 1、投影坐标系的形状
投影坐标系是截取并归一化观察者眼中的世界得到的(更详细的讨论请参考《[OpenGL]02 - OpenGL中的坐标系》),先截取世界,截锥相机包含部分,然后又变成左手坐标系(对于透视投影,坐标系的概念可能已经扩展)
图 11. 投影坐标系:正交投影和透视投影
这就产生了一个问题,为什么前面也是左手坐标系(局部坐标系,世界坐标系)unity 左手坐标系,后面也是左手坐标系(投影坐标系),为什么很难插入一个中间的右手坐标系(观测坐标系)? 事实上,在Opengl中,局部坐标系和世界坐标系都是右手坐标系,即直到投影空间后才转化为左手坐标系。 unity中的局部坐标系和世界坐标系都是左手系,所以看起来观测坐标系比较特殊。
2.正射投影
对于正交投影,请参见第一张图片。 从观测坐标系变换到投影坐标系后,点(x,y,z)的取值范围为:
它的变换矩阵可以参考我坐标系的博客:
等式 10. 正交投影的投影变换矩阵
3.透视投影
透视投影的情况要复杂得多,在这一步,w 值就发挥作用了。
先直接看变换矩阵(方法还是推荐看坐标系的文章)
等式 11. 透视投影的投影变换矩阵
对于原观测坐标系中的点P,投影坐标系中的坐标P'变换为:
公式12 透视投影变换下点的变换
如果只考虑(x,y,z),和正射投影一样,只是增加了第四维w,使得z坐标轴的方向弯曲
图 12. 透视坐标系
4.从观测坐标系到投影坐标系——得到变换矩阵
如果您想自己计算,可以使用上面提供的公式进行计算。 Unity本身也为我们提供了获取这个变换矩阵的方法。
在着色器中,这一步是一个单独的变换矩阵:
[cpp]
UNITY_MATRIX_P
在脚本中,如果使用 camera.projectionMatrix,您可能会发现与着色器中的矩阵有些差异。 这是因为,在我的电脑上(不清楚是否与显卡有关),实际的变换矩阵最终会使z的取值范围为[0, 1],与上面的公式略有不同。
所以在脚本中,需要使用如下代码获取投影变换矩阵:
[cpp]
GL.GetGPUProjectionMatrix(c.projectionMatrix,false)
5.值得注意的w组件
之前的w分量一直是1,现在终于可以用了。 正交变换后,w的值仍然为1; 但经过透视投影变换后,w的值在观察坐标系中等于-z。 另外,在透视坐标系中,shader中应该用到的x、y、z分量应该是P变换后的坐标值的Px、Py、Pz分量除以w得到的。
说到除0,一定要小心除0,有没有可能w是0? 答案是不可能的。 这是由于透视投影的camera frustum中的点,其z范围为[-ZFar, -ZNear],不会取值为0。
6. 切空间 1. 切空间的形状
做bump map的时候会提到切线空间。 切线空间的具体含义这里就不多说了(关于这个问题,解释最清楚的是《OpenGL Normal Map Tutorial》)。 这里主要讨论坐标系转换问题。
切空间的坐标轴为:
其中,normal就是我们通常所说的normal:
切线选择是垂直于法线并沿纹理uv变量u的增长方向的向量:
短切线选择垂直于这两个向量,通常通过叉积获得。 从而形成坐标系:
需要注意的是,在unity中,实际得到的B向量可能与这张图中的B向量相反。
2、在unity中获取并使用切线空间变换矩阵
unity中有这么个宏TANGENT_SPACE_ROTATION,可以变换到切线空间
[cpp]
#defineTANGENT_SPACE_ROTATION\ float3binormal=cross(规范化(v.normal),规范化(v.tangent.xyz))*v.tangent.w;\ float3x3rotation=float3x3(v.tangent.xyz,binormal,v.normal)
使用时,可以这样使用:
[cpp]
TANGENT_SPACE_ROTATION; o.viewDirForParallax=mul(旋转,ObjSpaceViewDir(v.vertex));
这个变换矩阵有很多值得说明的细节,我们一一来看。
3、这是什么矩阵?
另一种问这个问题的方法是,float3x3 是如何构造的?
float3x3其实就是行优先填充元素,可以看这里。 所以,这个矩阵是:
注意这个矩阵作为变换矩阵很奇怪。 回忆一下我们在第 3 节世界坐标系第 2 节中的结论:
所以变换矩阵应该是三个列向量的矩阵,现在却变成了三个行向量,为什么呢?
4.转置和逆矩阵
现在回答前面的问题:原因是,其实这是一个逆矩阵。
首先,可以注意到三个基向量(T,B,N)都是单位向量并且相互正交。 那么由它构成的矩阵就是一个正交矩阵。 对于一个正交矩阵,它的转置矩阵就是它的逆矩阵,所以有:
注意下面的矩阵,显然满足前面的结论,T,B,N都是局部坐标系下的值,所以这个列向量构成的[TBN]矩阵就是把点从切坐标变换过来system 到局部坐标系的变换矩阵。 那么这个矩阵的逆矩阵就是将点从局部坐标系变换到切线坐标系的变换矩阵。 由于是正交的,它的逆矩阵就是它的转置矩阵,所以从局部坐标系到切线坐标系的变换矩阵就是我们在第2节看到的。
5.左手坐标系? 右手坐标系?
由于B向量是叉乘得到的,一个值得关注的问题是这个坐标系是左手定则还是右手定则? 这就牵扯到之前的另一个结论:叉乘法,左手坐标系用左手定则,右手坐标系用右手定则,但不管用哪一种,数学计算表达式不会有任何不同。
现在,N和T在局部坐标系下都是向量,而局部坐标系是左手坐标系,所以N和T的叉积使用左手法则,那么在没有其他变量的情况下, T、B、N应如下,构成左手坐标系:
但是现在有一个变量tangent.w,他的值是1或者-1。 如果为1,则上述结论不变,如果为-1,则B反转,从而形成右手坐标系。
这个变量的存在是因为B向量通常表示uv中v增长的方向,所以需要调整B轴使其与v增长的方向一致。 (在这里参考这篇文章)
So far, the discussion on the coordinate system has basically come to an end.