·本专栏文章记录笔者阅读学习《Unity Shader入门精要》的感想与笔记,方便日后复习与查找
一、数学基础
1.笛卡尔坐标系
二维笛卡尔坐标系:
包含原点与X,Y轴。
DirectX中的二维笛卡尔坐标系
OpenGL中的二维笛卡尔坐标系
三位笛卡尔坐标系:
包含原点与X,Y,Z轴,其中XYZ轴组成一对标准正交基,根据Z轴与XY的关系可以分为左手坐标系与右手坐标系(即具有不同的旋向性)
标准正交基:①组成的各个矢量相互垂直②各个矢量为单位矢量
左手坐标系:旋转方向(从Z轴正方向处转到X轴正方向处)的正方向是顺时针(就像你左手点赞👍的时候,四指的指向)
右手坐标系:旋转方向(从Z轴正方向处转到X轴正方向处)的正方向是逆时针
左手坐标系与右手坐标系中表述相同的位置(相同空间中视觉位置)所用的值是会不同的,所以如果从坐标表述从左手换到右手,或者从右手换到左手的时候,是需要对这个位置的坐标值进行一定的计算变换的
2.点
用来表示平面或者空间中的一个位置,无体积与长度
例如:P=(x,y,z)就表示一个点
3.矢量
作用:
用来表示空间中的偏移量(某个点的偏移),具有方向与大小,不包含位置信息
例如:V=(x,y,z)就表示一个向量(看起来与点表述类似)
因此:点*矢量 = 沿这个矢量偏移到的另一个点
运算:
矢量与标量的乘除:k(x,y,z) 【对矢量大小的放大与缩小】
矢量与矢量的加减: 【对矢量的偏移量的叠加,-号就是把矢量反向反转】
矢量与矢量的点乘:dot(v1,v2) 【得到一个标量,包含两个矢量的夹角信息;满足交换律】
矢量与矢量的叉乘:cross(v1,v2) 【得到一个新矢量,这个矢量垂直于v1,v2,方向在左手坐标系中由左手法则判断,在右手坐标系中由右手法则判断,它们只是看起来不一样而已,实际上值是一样的;满足反交换律】
v1·v2 = |v1||v2|cosθ
v1×v2 = |v1||v2|sinθ 【平行四边形面积】
点乘常用于判断两个矢量的夹角大小,两个点的前后关系
叉乘常用于判定面的朝向
4.矩阵
作用:
可以用来表示空间中的变换(缩放、旋转、平移)
- 表示平移的时候,为了让他继续保持可以矩阵相乘,需要采用齐次坐标系,即让三维矩阵变成四位矩阵,此时
- 点的第四维数是1
- 矢量的第四维是0(因为矢量本身无位置信息,所以不会受到平移的影响)
- 变换的顺序一般按照缩放、旋转、平移
- 对旋转而言它又分为绕x,y,z轴旋转,在Unity中给定的旋转顺序是zxy(不过这是按世界坐标来旋转的时候的,如果按本地坐标系(此时旋转轴会随着旋转而旋转),则是按yxz旋转,但是这两种情况下的旋转的结果都是一样的
运算:
矩阵与标量的乘除:kM (使得k与M中每个元素相乘除即可)
矩阵与矩阵的乘法:【①不满足交换律 ②满足结合律 ③相乘矩阵需满足一定的行列关系】
特殊矩阵:
1.方块矩阵
就是方形的矩阵了,mxm
2.单位矩阵
对角线上元素都是1,除了对角线其它元素都是0
任何矩阵和单位矩阵相乘都是自身(不论是左乘还是右乘)
3.转置矩阵
就是把一个矩阵的行列进行交换
转置矩阵对于矩阵与矢量相乘时的作用:
可以看出来,这个时候可以让它看起来满足交换律
对称矩阵的定义与转置:
若M 为对称矩阵 则
4.逆矩阵
如果 A·B = I 那就说明 A 与 B互相为对方的逆矩阵
即
如果一个矩阵不存在逆矩阵(比如零矩阵,那他就是奇异矩阵,反之是非奇异矩阵)
5.正交矩阵
如果一个矩阵 ,那他就是正交矩阵
显然可以发现此时
正交矩阵观察判定:
①它的行/列矢量相互垂直
②它的行/列矢量的模是1
二、坐标变换
1.父坐标与子坐标
坐标系是由原点与三条能构成标准正交基的矢量构成的
而这些之所以能被定义也是因为在另一个坐标系的基础上的,所以我们可以知道坐标系之间是有层次关系的
定义父空间:P 子空间:C
而要把在一个子空间中的点转化成父空间中进行表示为
,需要使用一个空间变换矩阵
即:
所以只要我们写出了这个空间变换矩阵即可
显然,我们还可以通过这个矩阵来推得空间C中的各个矢量轴与原点位置
同时,显然xc,yc,zc是标准正交基,如果把这个矩阵降为三维的话,那它一定是正交矩阵
三维的矩阵可以用来表示对矢量的空间变换(因为矢量并不包含位置信息)这个时候我们就可以很轻松地得到这个空间变换矩阵的逆矩阵(就是它的转置矩阵)
这个矩阵的推导过程就是想象你是如何通过点的坐标和点位置去找到这个点的,比如已知点的在C空间的坐标是
,原点是
,那就
①从原点开始,沿Xpc轴移动
距离
②然后再沿Ypc轴,移动
距离
③再沿Zpc轴,移动
jv距离
就可以得到
然后把这个矩阵进行一些小小的数学变换,就可以从中提取到
了
2.模型坐标→世界坐标
模型空间:是模型在建模软件中确定好它的原点与三个矢量轴的,自己独立的坐标空间,移动旋转的时候,它会跟随模型一起移动旋转【左手坐标系】
世界坐标:是整个游戏场景中的最大空间【左手坐标系】
- 这一步的变换叫做:模型变换
直接:
3.世界坐标→观察空间坐标
观察空间坐标:又称为摄像机空间,以摄像机为原点,摄像机的模型坐标的坐标轴作为坐标轴
是三维的【右手坐标系】
- 这一步的变换叫做:观察变换
此时:
4.观察空间坐标→裁剪空间坐标
裁剪空间坐标:由摄像机的视锥体决定(即能看到的范围),视锥体内的顶点才会被渲染,视锥体外的会被直接剔除。【左手坐标系】
- 这一步的变换叫:裁剪变换或者投影变换(并不是实际投影了,还没降维,只是给下一步真正投影做准备)
透视投影:
正交投影:
透视投影会使得顶点的w分量也发生变化(在OpenGL中范围是[-w,w],DirectX中则是[0,w])【空间范围是一个锥体中】
正交投影则不会改变w分量(还是1)【空间范围是一个立方体】
是否在裁剪空间中的判断(不论是投影还是正交都这么判断):
5.裁剪空间坐标→归一化设备坐标
归一化设备坐标(NDC):把裁剪空间中的顶点坐标中的x,y,z都用w去除以。空间范围是一个立方体(这个空间中的x,y分量范围都是[-1,1],z分类在OpenGL定义中是[-1,1],在DirectX中是[0,1])【左手坐标系】
(不过正交裁剪空间本来就是一个立方体了)
- 这一步叫做:除法透视(正交投影x,y,z分量除以的都是1,所以做了和没做差不多)
6.归一化设备坐标→屏幕空间坐标
屏幕空间坐标:Unity中屏幕的左下角是(0,0),右上角是(pixelWidth,pixelHeight)【左手坐标系】
这一步叫:屏幕映射(就是把x,y这些[-1,1]范围的值,拉伸到[0,pixelWidth]和[0,pixelHeight]这个范围里而已)
剩下的z分量与w分量:
可以作为
被存入深度缓冲中
w已经完成了对顶点在裁剪空间中的坐标进行齐次除法的操作,之后还可以用于透视插值中
我们可以操控的工作:
在我们把顶点坐标变换到裁剪空间之后,由裁剪空间到屏幕空间是Unity底层帮助我们完成的,我们的顶点着色器只需要把顶带你转化到裁剪空间即可
大概是这么个流程了,还是很简单的对吧
三、其他
1.法线变换
法线是在进行空间变换的时候,不能直接用上述对顶点进行变换操作的矩阵
这是因为如果存在非统一缩放的时候,得到的新法线方向可能会不与表面垂直了
不过切线不会有这个问题,而切线·法线 = 0,所以我们可以通过切线来求得对法线的变换矩阵
变换矩阵如下:
即为原变换矩阵的逆转置矩阵
如果变换矩阵本身是正交矩阵
- 即此时只存在旋转变换,此时
- 若还有统一缩放,缩放系数为k ,那就
2.Unity Shader中的与数学相关内置变量
- 如果一个变换矩阵MV只存在旋转变换,那它就是正交矩阵
此时:UNITY_MATRIX_MV = UNITY_MATRIX_T_MV
- 如果这个变换矩阵MV存在旋转变换和统一缩放(比例系数为k)
此时:UNITY_MATRIX_MV = UNITY_MATRIX_T_MV
- 可以通过UNITY_MATRIX_IT_MV得到MV矩阵的逆矩阵
即为:transpose(UNITY_MATRIX_IT_MV),可以用它把顶点从观察空间中变换会模型空间中
mul(transpose(UNITY_MATRIX_IT_MV), viewPos) = mul(viewPos,UNITY_MATRIX_IT_MV)
可以看出,交换矩阵与矢量相乘的位置的时候,要得到相同的结果对矩阵进行转置即可。
3.Cg中的矢量与矩阵
矩阵(M)声明:float3x3,float4x4【Cg中按行优先填充矩阵,Unity脚本中Matrix4x4按行优先填充】
矢量(v)声明:float3,float4
mul(M,v) != mul(v,M) 【相乘的位置决定按列矢量还是行矢量相乘】
mul(M,v) == mul(v,transpose(M))
mul(v,M) == mul(transpose(M), v)
【可以看出,如果有时候我们想要略去转置矩阵的步骤,可以采用左乘的方式】
4.Unity中的屏幕坐标
如果我们想要得到片元在屏幕上的像素位置要这么做呢?
方法一:VPOS/WPOS语义
VPOS是HLSL对屏幕坐标的语义
WPOS是Cg对屏幕坐标的语义
使用方法如下:
fixed4 frag(float4 sp : VPOS) : SV_Target {
//用屏幕坐标除以屏幕分辨率_ScreenParams.xy,得到视口空间中坐标
return fixed4(sp.xy/_ScreenParams.xy, 0.0, 1.0);
}
视口坐标就是把屏幕坐标归一化,让左下角是(0,0)右上角的(1,1)
1.对于sp的xy分量:如果屏幕分辨率为400X300,则x的范围是[0.5,400.5] y的范围是[0.5,300.5] (因为OpenGL 和DirectX10以后版本认为像素中心对应的浮点值为0.5)
2.对于sp的z分量:范围为[0,1] (在近裁剪面为0,远裁剪面为1)
3.对于sp的w分量:考虑投影类型
若为透视投影:w分量范围为[]
若为正交投影:w分量始终为1
方法二:Unity中提供的ComputeScreenPos函数
使用方法:
①在顶点着色器中把ComputeScreenPos的结果保存在输出结构体中
②在片元着色器中把得到的输出结构体进行一次齐次除法后得到视口空间下坐标
struct vertout{
float4 pos:SV_POSITION;
float4 scrPos : TEXCOORD0;
};
vertout vert(appdate_base v){
vertout o;
//第一步:把ComputeScreenPos函数的输出值保存到scrPos中
o.pos = mul(UNITY_MATRIX_MVP, v.pos);
o.scrPos = ComputeScreenPos(o.pos);
return o;
}
fixed4 frag(vertout i) : SV_Target{
//第二步:用scrPos.xy除以scrPos.w得到视口空间中坐标
float2 wcoord = i.scrPos.xy / i.scrPos.w;
return fixed4(wcoord, 0.0, 1.0);
}
Unity中ComputeScreenPos的定义如下:
inline float4 ComputeScreenPos (float4 pos)
{
float4 o = pos * 0.5f;
o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
o.zw = pos.zw;
return o;
}
从这里我们看出,它是对裁剪空间中的顶点的坐标位置的
显然此时还不是视口空间下坐标,因此我们需要在片元着色器中再对他用w分量进行一次齐次除法,这样得到的就是视口空间下的像素位置了
那为什么不直接在ComputeScreenPos这里面直接执行这个齐次除法呢?
答:因为从顶点着色器到片元着色器这一步要进行插值操作,如果提取除了的话会对插值操作造成影响(导致是对x/w,y/w进行插值,如果是透视投影会导致透视失真),所以只能等插值完了之后再除,也就是在片元着色器中完成这个最后的齐次除法
除法操作后各分量范围
①xy: [0,1]
②zw:
透视投影:z:[-Near,Far], w:[Near, Far]
正交投影:z:[-1,1], w:恒为1
四、总结
①三维坐标系由原点和三个组成标准正交基的矢量组成,分为左手坐标系与右手坐标系,在其中旋转的正方向不同,可以用左右手法则判断正方向【Unity中使用的是左手坐标系,但是观察空间使用的是右手坐标系】。同一个位置在不同的坐标系中会呈现不同的数值。
②空间变换相关的操作从模型空间到裁剪空间是在顶点着色器中需要完成的,而从裁剪空间到屏幕空间以及视口空间Unity会帮助我们完成
③矢量只有方向和大小,没有位置属性,所以对他的变换矩阵可以不用齐次矩阵来变换。同时矢量的叉乘可以用来判断这个面的朝向。
④矩阵表示变换,矩阵相乘是不满足交换律的,要交换顺序而且结果不变的话需要用到转置。正交矩阵的逆矩阵和转置矩阵是相同的
⑤要在片元着色器中得到片元的像素在视口空间下的坐标位置,可以用VPOS/WPOS语义,或者通过ComputeScreenPos函数来计算,之后再在片元着色器中进行齐次除法