感受高数
不懂数学者不得入内。 ----------------------柏拉图学院门口的碑文
声明:本篇为博主阅读冯乐乐所著《Unity Shader入门精要》所记录的笔记
原作者博客地址 https://me.youkuaiyun.com/candycat1992
如有不实请指正,如有侵权请联系本人删除。
计算机图形学之所以深奥难懂,很大原因是在于它是建立在虚拟世界上的数学模型。数学渗透到图形学的方方面面,当然也包括Shader。在Shader学习的过程中,我们最常使用的就是矢量和矩阵(即数学的分支之一–线性代数)
一、笛卡尔坐标系
1.二维笛卡尔坐标系
OpenGL与DirectX使用了不同的笛卡尔坐标系。
OpenGL左下角为原点
DirectX左上角为原点
2.三维笛卡尔坐标系
xyz三轴被称为笛卡尔坐标系的基矢量(basis vector),通常情况下,这三个坐标轴之间是互相垂直的,且长度为1,这样的基矢量被称为标准正交基(orthonormal basis),但这不是必须的,如相互垂直但长度不为一则被称为正交基(orthonormal basis),与二维笛卡尔类似,三维笛卡尔坐标系也分左手坐标系和右手坐标系。
Unity使用的是左手坐标系。
二、点和矢量
点(point)是n维空间中的一个位置,它没有大小,宽度这类概念,我们可以用2个或者3个实数来表示一个点的坐标,如P=(Px,Py);P=(Px,Py,Pz);
矢量(Vector,向量)是n维空间的一种包含了模(magnitude)和方向(direction)的有向线段,是一个相对量。
1.矢量和标量的乘法/除法
矢量的每个分量和标量相乘:
kv=(KVx,KVy,KVz)
2*(1,2,3)=(2,4,6)
(2,3,4)/2=(1,1.5,2)
2.矢量的加法和减法
分量之间相加减
a+b=(ax+bx,ay+by,az+bz)
3.矢量的模
每个分量的平方之和进行开平方。
4.单位矢量
模为1的矢量,也被称为被归一化的矢量(normalized vector)
5.矢量的点积
矢量之间的乘法与标量不同,常见的乘法有两种,点积(dot product,也被称为内积)和差积(cross product,也被称为外积outer product)
点积的公式有两种形式
公式第一种:
两个三维矢量的点积是把两个矢量对应分来那个相乘后再取和,最后的公式是一个标量
a点b=(ax,ay,az)点(bx,by ,bz)= axbx+ayby+azbz;
(1,2,3)点(0.5,4,2.5)=0.5+8+7.5=16
公式第二种:
a点b=|a|*|b|*cos夹角
点积的几何意义很重要,因为点积几乎应用到图形学的各个方面。其中一个几何意义就是投影(projection)
点积结果的正负与ab方向有关
性质1:点积可结合标量乘法
上面的“结合”是说,点积的操作数之一,可以是另一个运算的结果,即矢量和标量的相乘结果 ka点b = a点kb = k(a点b)
性质2:点积可结合矢量的加法和减法。
这里的“结合”指的是,点积的操作数可以是矢量相加或相减后的结果
性质3:一个矢量和本身进行点积的结果,是该矢量的模的平方
6.矢量的叉积(cross product)
另一个重要的矢量运算就是叉积,与点积不同的是,矢量叉积的结果仍然是一个矢量,而非标量
aXb=(ax,ay,az)X(bx,by,bz)=(aybz-azby,azbx-axbz,axby-aybx)
(1,2,3)X(-2,-1,4)=( (2)(4)-(3)(-1), (3)(-2)-(1)(4), (1)(-1)-(2)(-2) )=( 11,10,3 )
叉积不满足交换率,也不满足结合律。
对两个向量的叉积进行计算得到的是一个同时垂直与两个向量的新向量。
模的值=|aXb|=|a|X|b|sin
三、矩阵(matrix)
1.定义
由mXn个标量组成的长方形数据。
网格结构,有行(row)列(column)之分
Mij表示矩阵M的第i行,第j列。
2.和矢量联系起来
矢量可以看作是nX1的列矩阵(column matrix)或 1Xn的行矩阵(row matrix)
其中n对应矩阵维度。
3.矩阵运算
(1)矩阵与标量的乘法
KM=MK
各元素直接相乘
(2)矩阵和矩阵的乘法
矩阵之间相乘会得到一个新的矩阵,并且这个矩阵的维度和两个原矩阵都有关系。
一个rXn的矩阵A和nXc的矩阵相乘,他们的结果AB是一个rXc大小的矩阵,(注意,第一矩阵的列数必须与第二矩阵的行数相等)
C=AB
Cij=ai1b1j+ai2b2j+ … ainbnj;
对于每个元素Cij,我们找到A中第i行和B中第j列,然后把他们对应的元素相乘后再加起来。
在shader计算中我们跟过的是使用4X4矩阵来运算的。
性质1:矩阵乘法不满足交换律
AB != BA
性质2:矩阵乘法满足结合律
(AB)C = A(BC)
4.特殊矩阵
(1)方块矩阵(Square matrix)
简称方阵,行列数相等,三维渲染中最常用的就是3x3 或4x4方阵
特殊:
对角元素:行号列号相等的元素
对角矩阵:除了对角元素都为0的矩阵
(2)单位矩阵(Identity matrix)
特殊的对角矩阵 In来表示。任何矩阵和他相乘还是其本身。
(3)转置矩阵(transposed matrix)
实际上是对一个矩阵执行转置运算,矩阵M的转置矩阵为MT,第i行变成第i列,第j列变第j行。
性质1:矩阵装置的转置等于原矩阵
性质2:矩阵串接的转置,等于反向串接哥哥矩阵的转置
(AB)T=BTAT
(4)逆矩阵(inverse matrix)
MM-1=I(单位矩阵)
并非所有矩阵都有逆矩阵。
如果一个矩阵有逆矩阵,那么这个矩阵就是可逆的,或者是奇异的。
判断方式:如果一个矩阵的行列式不为0,那么他就是可逆的
性质1:逆矩阵的逆矩阵是原矩阵本身
性质2:单位矩阵的逆矩阵是他本身
性质3:转置矩阵的逆矩阵是逆矩阵的转置
(MT)-1 = (M-1)T
性质4:矩阵串接相乘后的逆矩阵等于反向串接各个矩阵的逆矩阵
(AB)-1 = B-1 A-1
逆矩阵是有几何意义的,我们知道一个矩阵可以表示一个变换,而逆矩阵允许我们还原这个变换,或者是计算这个矩阵的反向变换
M-1(Mv)=(M-1M)v = v
(5)正交矩阵
如果一个方阵M和它的转置矩阵MT的乘积是单位矩阵的话,我们就说这个矩阵是正交的)反过来也是成立的。也就是说
MMT=MTM=I
MT=M-1
这个式子非常有用,因为在三维变幻中我们经常会需要使用逆矩阵来求解反向的变换。而逆矩阵运算量远大于转置矩阵。
5.行矩阵还是列矩阵
矢量可以转化为一个行矩阵或者一个列矩阵,本身是没区别的,但是计与另一个矩阵进行计算时就会出现差异。
在Unity中常规做法是把矢量放在矩阵右侧计算。即转化为列矩阵。
四、矩阵的几何意义:变换
1.什么是变换(Transform)
指的是我们把一些数据,如点、方向矢量甚至是颜色等,通过某种方法进行转换的过程,在计算机图形学领域,变换非常重要。
(1)线性变换(linear transform)
保留矢量加标量乘的变换。
f(x)+f(y) = f(x+y)
kf(x)=f(kx)
缩放(scale)就是一种线性变换,f(x)=2x
旋转(rotation)也是一种线性变换。
对于线性变换来说,如果我们需要三维的矢量变换,那么使用3x3的矩阵就可以表示所有的线性变换。
线性变换还包括错切(shear)、镜像(mirroring,也被称为正交投影 orthographic projection)等。
(2)平移变换
例如f(x)=x+(1,2,3); 满足标量乘法,但不满足标量加法,如果令x=(1,1,1)那么
f(x)+f(x) = (4,6,8)
f(x+x)=(3,4,5)
可见两个运算得到的结果是不一样的,因此我们不能用一个3x3的矩阵来表示一个平移变换。这是我们不希望看到的,毕竟平移变换非常常见。
(3)仿射变换(affine transform)
仿射变换就是合并线性变换和平移变换类型。仿射变换可以用一个4x4矩阵来表示,为此我们需要把矢量扩展到四维空间下,就是齐次坐标空间(homogeneous space )。
变换名称 | 是线性变换? | 是仿射变换? | 是可逆矩阵? | 是正交矩阵? |
---|---|---|---|---|
平移矩阵 | N | Y | Y | N |
绕坐标轴旋转的旋转矩阵 | Y | Y | Y | Y |
绕任意轴旋转的旋转矩阵 | Y | Y | Y | Y |
按坐标轴缩放的缩放矩阵 | Y | Y | Y | N |
错切矩阵 | Y | Y | Y | N |
镜像矩阵 | Y | Y | Y | Y |
正交投影矩阵 | Y | Y | N | N |
透视投影矩阵 | N | N | N | N |
2.齐次坐标(homogeneous cpprdinate)
我们知道由于3x3矩阵不能表示平移操作,我们就把他扩到了4维矩阵。为此,我们需要把矢量扩展到四维矢量,也就是齐次坐标。
那么如何把三维矢量转化成其次坐标呢?
对于一个点,把三维坐标转换成其次坐标是把w分量设为1
而对于矢量坐标,需要把其w分量设置为0,这样的设置会导致当用一个4x4矩阵对一个点进行变换时,平移、缩放、旋转都会施加于该点。但是如果是用于变换一个方向矢量,平移的效果就会被忽略。
3.分解基础变换矩阵
我们都已经知道,可以使用一个4x4的矩阵来表示平移、旋转和缩放。我们把表示纯平移、纯旋转和纯缩放的变换矩阵叫基础变换矩阵,这些矩阵有一些共同点,我们可以把一个基础变换矩阵分解成4个组成部分
M3X3 t3X1
01X3 1
其中,左上角的矩阵M3x3表示旋转和缩放,t3x1用于表示平移,01x3是零矩阵,即01x3 = [0,0,0] ,右下角是标量1.
4.平移矩阵
[1 0 0 tx][x]. [x+tx]
[0 1 0 ty][y]= [y+ty]
[0 0 1 tz][z]. [z+tz]
[0 0 0 1 ][1]. [1]
点的xyz分量分别增加了一个位置偏移。在3d中的可视化效果是,在(x,y,z)在空间中平移了(tx,ty,tz)个单位。
但当我们对一个矢量进行平移变换
[1 0 0 tx][x]. [x]
[0 1 0 ty][y]= [y]
[0 0 1 tz][z]. [z]
[0 0 0 1 ][0]. [0]
可以发现平移对矢量没有任何影响。
5.缩放矩阵
我们可以对一个模型沿着空间的xyz轴进行缩放。同样我们可以使用矩阵成绩表示另一个缩放变换:
[kx 0 0 0][x]. [xkx]
[0 ky 0 0][y]= [yky]
[0 0 kz 0][z]. [zkz]
[0 0 0 1][0]. [0]
如果缩放系数k1=k2=k3。我们把这样的缩放称为统一缩放(uniform scale)否则称为非统一缩放
缩放的逆矩阵是使用原版缩放洗漱的倒数来对点或方向矢量进行缩放,即
[1/kx 0 0 0]
[0 1/ky 0 0]
[0 0 1/kz 0]
[0 0 0 1]
6.旋转矩阵
旋转是三种常见变换矩阵中最复杂的一种,我们知道旋转操作需要指定一个旋转轴,这个轴不一定是空间中的坐标轴。
~输入法找不到夹角- - 更是不知道矩阵怎么写,心痛
如果我们需要把点绕着x旋转@度,可以使用下面的矩阵
[1 0 0 0]
[0 cos@ -sin@ 0]
[0 sin@ cos@ 0]
[0 0 0 1]
绕y轴旋转也是类似的
[ cos@ 0 sin@ 0]
[ 0 1 0 0 ]
[ -sin@ 0 cos@ 0 ]
[0 0 0 1 ]
最后是绕z轴旋转
[cos@ -sin@ 0 0 ]
[sin@ cos@ 0 0 ]
[0 0 1 0 ]
[0 0 0 1 ]
原来全角的空格在这里能占位,不想回去改了
7.复合变换
我们可以把平移、旋转、缩放组合起来形成一个复杂的变换过程。
例如,可以对一个模型先进行(2,2,2)的缩放,再绕y轴旋转30度,最后向z轴平移4个单位,符合通过矩阵的传来来实现。上面的变换过程可以使用下面的公式来计算:Pnew=MtransformMrotationMscal@Pold;
绝大多数情况下 先缩放再旋转最后平移。
五、坐标空间(陷入数学雷区…)
渲染的过程可以理解成把一个个顶点经过层层处理最终转化到了屏幕上的过程,这一节就学习这个转化过程是如何实现的,或者说,顶点是经过了哪些坐标空间后,最后被画在了我们的屏幕上。
1.为什么要使用这么多不同的坐标空间
一些概念只有在特定空间下才有意义,比如门口的拖鞋与你手上的袋子?分别是两个物体各自为原点的坐标空间。
2.坐标空间的变换
每个坐标空间都是另一个坐标空间的子空间,反过来每个空间都有一个父坐标空间。对坐标空间的变换实际上就是在父空间和子空间之间对点和矢量进行变换。
假设有一父坐标空间P和一子坐标空间C 有两个需求
1.子坐标空间的矢量Ac转换到父坐标空间下表示Ap:Ap=Mc-pAc
2.父坐标空间的矢量Bp转换到字坐标空间下表示Bc:Bc=Mp-cBp
其中 Mc-p表示的是字坐标空间变换到父坐标空间的变换矩阵,而Mp-c是其逆矩阵。
如何解Mc-p?
1.从字坐标空间的原点Oc开始
2.向x轴方向移动x个单位: Oc+aXc
3.向y轴方向移动y个单位: Oc+aXc+bYc
4.向z轴方向移动z个单位: Oc+aXc+bYc+cZc
最后
Ap=Oc+aXc+bYc+cZc
=(Xoc,Yoc,Zoc)+ a(Xxc,Yxc,Zxc)+b(Xyc,Yyc,Zyc)+c(Xzc,Yzc,Zzc)
=(Xoc,Yoc,Zoc)+ [ Xxc Xyc Xzc][a]
[Yxc Yyc Yzc][b]
[Zxc Zyc Zzc][c]
=
深陷雷区,无法理解,容后再补
下个月熬过考试就去复习高数
3.顶点坐标空间的变换流程
在渲染流水线中,一个顶点要经过多个坐标空间变换才能最终被画在屏幕上,一个顶点最初是在模型空间被定义的,最终被变换到屏幕空间中,得到真正的屏幕像素坐标。
4.模型空间(model space)
也被称为对象空间(object space)或局部空间(local space)。
每个模型都有自给独立的坐标空间,当他移动或旋转的时候,模型空间也会随着他移动和旋转。
在模型空间的方向概念
前(forward)后(back)左(left)右(rught)上(up)下(down)
我们称其为自然方向。
5.世界空间(world space )
最上层空间,层级最大。
定点变换的第一步就是将顶点坐标从模型空间变换到世界空间中。这个变换通常被称为模型变换(model transform)
6.观察空间(view space)
也被称为摄像机空间(camera space),摄像机决定渲染游戏所使用的视角,在观察空间中摄像机位于原点,同样,其坐标的旋转是可以任意的。
其中由于世界空间为左手坐标系,观察空间为右手坐标系,所以世界的z+轴是摄像机的后方。
定点变换的第二步,就是将顶点坐标从世界空间变换到观察空间中,这个变换通常被称为观察变换(view transform)
而观察空间与屏幕空间不同的地方在于观察空间是3D空间,而屏幕空间是2D空间,从观察空间到屏幕空间还需要一个操作就是投影(projection)
7.裁剪空间(clip space)
计算公式详见UnityShader入门精要P.79
也被称为齐次裁剪空间,这个用于变换的矩阵叫裁剪矩阵(clip matrix),也被称为矩阵投影(projection matrix)
裁剪空间的目标是能够方便的对渲染图元进行裁剪:完全位于这块空间内部的图元将会被保留,完全位于这块空间外部的图元将会被剔除,而这块空间边界相交的图元就会被裁剪。具体有视锥体(view frustum)来决定
视锥体指的是空间中的一块区域,这块区域决定了摄像机可以看到的空间。这个区域由6个面包围而成,这些平面也被称为裁剪平面(clip planes)。
视锥体由两种类型:一种是正交投影(orthographic projection),另一种是透视投影(perspective projection)
在透视投影中,地板上的平行线并不会保持平行,李摄像机越近网格越大,离摄像机越远网格越小。
而在正交投影中,所有的网格大小都一样,而且平行线会一直保持平行。
由此可以看出,透视投影模拟了人眼看世界的方式(适合3D),正交投影则完全保留了物体的距离和角度(适合2D)。
视锥体的6块裁剪屏面中,有两块裁剪平面比较特殊,他们分别被称为近裁剪平面(near clip plane)和远裁剪屏幕(far clip plane),决定了摄像机可以看到的深度范围。
投影矩阵有两个目的:
1.为投影做准备,虽然投影矩阵的名称包含了投影二字,但是他并不能进行真正的投影工作,而是为投影做准备,真正的投影发生在后面的齐次除法(homogeneous division)过程中。而经过投影矩阵的变换后,顶点w分量将会具有特殊的意义。
2.对x,y,z进行缩放。用6个裁剪平面来进行裁剪比较麻烦,而经过投影矩阵的缩放后,我们可以使用w分量作为一个范围值,如果xyz都在这个范围内,就说明改点位于裁剪空间内
8.屏幕空间
首先,进行标准的齐次除法(homogeneous division),也被称为透视除法(peispective disvision),用齐次坐标系的w分量去处理xyz分量,这个步骤得到的坐标在OpenGl中也被称为归一化的设备坐标(Normalized Devvice Coordinates,NDC),经过齐次除法以后,透视投影的裁剪空间会变换到一个立方体,而正交投影本身就是一个立方体,W分量为1,齐次除法不会对其产生影响。
经过齐次除法后,两种投影的视锥体都变换到一个相同的立方体内,现在,我们可以根据变换后的xy坐标来映射出输出窗口的对应像素坐标。Unity中屏幕坐下的像素坐标为(0,0)右上角的屏幕坐标为(pixelWidth,pixelHeight),由于当前xy的坐标都是-1,所以这个映射的过程就是一个缩放的过程。
在Unity中,从裁剪空间到屏幕空间的转换是由Unity完成的,我们的定点着色器需要把顶点转换到裁剪空间即可。
9.总结
顶点着色器最基本的任务就是把顶点坐标从模型空间转换到裁剪空间中(模型变换、观察变换、投影变换,在顶点着色器中通常串联成一个矩阵,即MVP矩阵)。
片元着色器中,我们通常也可以得到该片元在屏幕空间的像素位置。
六、法线变换(陷入数学雷区…下一篇就学习线性代数)
法线(normal)也被称为法矢量(normal vector),在上面我们已经看到如何使用变换的矩阵来变换一个顶点或一个方向矢量,但发现是需要我们特殊处理的一种方向矢量,在游戏中我们不仅需要变换它的顶点,还需要变换顶点法线,以方便后续处理(如片元着色器)中计算光照等。
一般来说,点和绝大多数的方向矢量都可以使一个4x4或3x3的变换矩阵Ma->b 从A变换到B空间中,但在变换发现的时候,如果使用同一个变换矩阵,可能就无法确保法线的垂直性。
我们先来了解另一种方向矢量 切线(tangent) 也被称为切矢量(tangent vector)与法线类似,切线往往也是模型顶点携带的一种信息,他通常与纹理空间对齐,而且与法线方向垂直。
由于切线是两个顶点之间的差值计算得到的,因此我们可以直接使用用于变换顶点的变换矩阵来变化切线。
七、UnityShader的内置变量(数学篇)
1.变换矩阵
变量名 | 描述 |
---|---|
UNITY_MATRIX_V | 当前的观察矩阵,用于将顶点/方向矢量从世界空间变换到观察空间。 |
UNITY_MATRIX_P | 当前的投影矩阵,用于将顶点/方向矢量从观察空间变换到裁剪空间 |
UNITY_MATRIX_VP | 当前的观察,投影矩阵,用于将顶点/方向矢量从世界空间变换到裁剪空间 |
UNITY_MATRIX_MV | 当前的模型,观察矩阵,用于将顶点/方向矢量从模型空间变换到观察空间 |
UNITY_MATRIX_T_MV | UNITY_MATRIX_MV 即模型、观察矩阵的转置矩阵 |
UNITY_MATRIX_IT_MV | UNITY_MATRIX_MV 即模型、观察矩阵的逆转置矩阵,用于将法线从模型空间变换到观察空间,也珂用于得到 UNITY_MATRIX_MV的逆矩阵。 |
UNITY_MATRIX_MVP | 当前的模型,观察,投影矩阵,用于将顶点/方向矢量从模型空间变换到裁剪空间 |
_Object2World | 当前的模型矩阵,用于将顶点、方向矢量从模型空间变换到世界空间 |
_World2Object | _Object2World的逆矩阵 ,用于将顶点、方向矢量从世界空间变换到模型空间 |
2.摄像机和屏幕参数
变量名 | 类型 | 描述 |
---|---|---|
_WorldSpaceCameraPos | float3 | 该摄像机在世界空间中的位置 |
_ProjectionParams | float4 | x=1.0(或-1.0 如果正在使用一个反转的投影矩阵进行渲染)。y=Near ,z=Far, w = 1.0 + 1.0/Far 其中Far和Near是远近裁剪平面与相机间的距离 |
_ScreenParams | float4 | x=width,y=height,z=1.0+1.0/width,w=1.0+1.0/height 其中 width和height分别是该相机渲染目标(render target )的像素宽度和高度。 |
_ZBufferParams | float4 | x=1-Far/Near , y=Far/Near , z = x/Far , w=y/Far 该变量用于线性化Z缓存中的深度值。 |
unity_OrthoParams | float4 | x= width,y=height, z没有定义 , w=1.0 (该相机为正交相机)或 w=0.0 (该相机为透视相机),其中width和height 是正交相机的宽度和高度。 |
unity_CameraProjection | float4x4 | 该相机的投影矩阵 |
unity_CameraInvProjection | float4x4 | 该相机的投影矩阵的逆矩阵 |
unity_CameraWorldClipPlanes[6] | float4 | 该相机的6个才见面在世界空间下的等式:左右下上近远。 |
八、其他及扩展
1.使用3x3还是4x4变换矩阵
对于线性变换(旋转缩放等)仅用3x3就足够表示,但如果存在平移变换就需要4x4变换矩阵。因此,在对顶点的变换中,我们通常使用4x4的变换矩阵。当然,在变换前我们需要把点坐标转换成其次坐标的表示,即把顶点的w分量设为 1 。而在对方向矢量的变换着,通常使用3x3就足够了。
2.CG中的矢量和矩阵类型
在CG中 矩阵类型是由float3x3、float4x4 等关键词来声明和定义的,而对于float3、float4等类型的变量,我们既可以把他当作一个矢量,也可以当成一个1xn的行矩阵或nx1的列矩阵,这取决于运算的种类和他们在运算中的位置。
3.Unity中的屏幕坐标:ComputeScreenPos/VPOS/WPOS
在顶点/片元着色器中,有两种方式来获得片元的屏幕坐标。
片元的屏幕坐标方法(1)
一种是在片元着色器的输入中声明VPOS或WPOS语义。
(VPOS是HLSL中对屏幕坐标的语义,而WPOS是CG中对屏幕坐标的语义,两者在UnityShader中等价。)我们可以在HLSL/CG中通过语义的方式来定义顶点/片元着色器的默认输入,而不需要自给定义输入输出的数据结构。
fixed4 frag(float4 sp : VPOS) : SV_Target{
//用屏幕坐标除以屏幕分辨率 _ScreenParms.xy ,得到视口空间中的坐标
return fixed4(sp.xy/_ScreenParams.xy,0.0,1.0);
}
VPOS/WPOS语义定义的输入是一个float4类型的变量,我们已经知道它的xy值代表了屏幕空间的像素坐标,如果屏幕分辨率400x300 那么x的范围就是 [0.5 , 400.5] y[0.5, 300.5] 。
(这里的像素坐标不是整数值,这是因为Opengl和DirectX10以后的版本认为像素中心对应的是浮点数中的0.5)
z的范围是[0,1]在摄像机的近裁剪面处z=0,在裁剪远处 z =1 。
w分量需要考虑摄像机的投影类型,如果是透视投影,w的范围是[1/Near,1/Far],如果是正交投影,w的值恒定为1。
在代码的最后,我们把屏幕空间处理屏幕分辨率来得到视口空间(viewport space)的坐标(视口坐标很简单,左下(0,0,)右上(1,1)计算方法就是xy值除以屏幕分辨率对应xy)
片元的屏幕坐标方法(2)
通过Unity提供的computeScreenPos函数。这个函数在UnityCG.cginc里被定义,通常的用法需要两个步骤,首先在顶点着色器中将ConputeScreenPos的结果保存在输出结构体中,然后在片元着色器中进行一个齐次除法运算后得到视口空间下的坐标
struct vertOut{
float4 pos : SV_POSITION;
float4 scrPos : TEXCOORD0;
}
vertOut vert(appdata_base v){
vertOut o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
//第一步:把ComputeScreenPos的结果保存达scrPos中
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 fixed(wcoord , 0.0,1.0);
}
这种方法实际上是手动实现了屏幕映射的过程,而且它得到的坐标直接就是视口空间中的坐标。得到公式:
viewportx=clipx/(2clipw) + 1/2
viewporty=clipy/(2clipw) + 1/2
即首先对裁剪空间下的坐标进行齐次除法,得到范围在[-1,1]的NDC,然后将其映射到范围在[0,1]视口空间下的坐标。
那么ComputeScreenPos是如何做到的?我们可以在UnityCG.cginc文件中找到ComputeScreenPos函数的定义如下:
inline float4 ComputeScreenPos(float4 pos){
float4 o = pos * 0.5f;
#if defined(UNITY_HALF_TEXEL_OFFSET)
o.xy = float2(o.x,o.y*_ProjectionParams.x) + o.w * _ScreenParams.zw;
#else
o.xy=float2(o.x,o.y*_ProjectionParams.x) + o.w;
#endif
o.zw=pos.zw;
return o ;
}
ComputeScreenPos的输入参数pos是经过MVP矩阵变换后再裁剪空间中的顶点坐标。UNITY_HALF_TEXEL_OFFSET是Unity在某些DirectX平台上使用的宏,暂时忽略。这样 我们主要关注else中的部分 _ProjectionParams.x在默认情况下是1(如果我们使用了一个翻转的投影矩阵的话就是-1 ,但这种情况很少见),那么上述代码实际输出了:
Outputx=clipx/2+clipw/2;
Outputy=clipy/2+clipw/2;
Outputz=clipz;
Outputw=clipw;
可以看出,这里的xy并不是真正的视口空间下的坐标,因此,我们在片元着色器中再进行一步处理,即除以它的w分量。
为什么不在ComputeScreenPos中进行除以w分量的操作呢,原因是如果Unity在顶点着色器中这么做的华,就会破坏插值的结果(从顶点到片元着色器的过程中会有一个插值的过程。而投影空间不是线性的,而插值是线性的)
经过除法操作后,我们可以得到该片元在视口空间的坐标了,也就是一个xy范围都在[0,1]之间的值。如果是透视投影 z[-Near,Far] w[Near,Far], 如果是正交投影 z[-1,1],w恒为1。