项目中有时会有一些效果需求,如重建片元在世界空间的坐标或者对屏幕指定区域进行颜色操作等,这时就需要获取到片元对应的屏幕坐标(Screen Space Coordinate)。在Unity中有三种方法可以获取到屏幕坐标,分别是:
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
float4 frag (v2f i) : SV_Target
{
float2 screenPos = i.pos.xy;
return tex2D(_MainTex, i.uv.xy);
}
这是一段最常见的顶点矩阵变换代码,o.pos
变量用来存储经过MVP矩阵变换的顶点坐标值,此时在顶点方法(vert)中 o.pos
存储的是裁切空间的坐标(Clip Space Coordinate)。而在片元方法(frag)中 i.pos.xy
的值就是屏幕空间的坐标值。这是因为Unity的默认渲染管线中进行了一些列操作的原因,这些操作包括:透视除法,映射到屏幕空间坐标范围。
裁切空间坐标简称为clipPos的话,则 clipPos.x
和 clipPos.y
经过透视除法范围从 [-w, w] 变到了 [-1, 1],再 *0.5 + 0.5 后映射到 [0, 1] 区间,然后根据屏幕宽高把 clipPos.x
映射到 [0, width],clipPos.y
映射到 [0, height] 区间。clipPos.z
存储了深度值,范围是 [0,1],而 clipPos.w
存储了视空间的z坐标,用于进行透视矫正。
unity还提供了一个单独的语义,用来获取屏幕坐标。Unity 文档 中的实例代码如下:
// note: no SV_POSITION in this struct
struct v2f {
float2 uv : TEXCOORD0;
};
v2f vert (
float4 vertex : POSITION, // vertex position input
float2 uv : TEXCOORD0, // texture coordinate input
out float4 outpos : SV_POSITION // clip space position output
)
{
v2f o;
o.uv = uv;
outpos = UnityObjectToClipPos(vertex);
return o;
}
sampler2D _MainTex;
fixed4 frag (v2f i, UNITY_VPOS_TYPE screenPos : VPOS) : SV_Target
{
// screenPos.xy will contain pixel integer coordinates.
// use them to implement a checkerboard pattern that skips rendering
// 4x4 blocks of pixels
// checker value will be negative for 4x4 blocks of pixels
// in a checkerboard pattern
screenPos.xy = floor(screenPos.xy * 0.25) * 0.5;
float checker = -frac(screenPos.r + screenPos.g);
// clip HLSL instruction stops rendering a pixel if value is negative
clip(checker);
// for pixels that were kept, read the texture and output it
fixed4 c = tex2D (_MainTex, i.uv);
return c;
}
在v2f结构体里没有SV_POSITION
语义的变量,而是用了一个 out float4
来把经过MVP变换后的顶点坐标传给了下一个流程,在frag
方法中用 VPOS
语义指定的screenPos
变量来承接屏幕空间坐标值,尝试在v2f结构体中加入SV_POSITION
语义,会得到语义重复的报错,从报错也可以感受到SV_POSITION
和VPOS
语义应该是同一回事,而文档里也做了说明:
Additionally, using the pixel position semantic makes it hard to have both the clip space position (SV_POSITION) and VPOS in the same vertex-to-fragment structure.
还有一种方式是使用Unity自带的宏,在顶点方法中使用 ComputeScreenPos
宏,在片元方法中把计算结果的xy分量除以w分量,此时xy分量范围是[0,1],一般用于去读取深度图或者grabpass抓取的图等地方,要得到正确的屏幕空间坐标还需要再乘以屏幕的宽高,Unity内置的宏 _ScreenParams
中提供了关于屏幕宽高的数据。
主要代码如下:
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 screenPos : TEXCOORD1;
};
v2f vert (appdata_base v)
{
v2f o;
o.uv = v.texcoord;
o.pos = UnityObjectToClipPos(v.vertex);
o.screenPos = ComputeScreenPos (o.pos);
return o;
}
sampler2D _MainTex;
fixed4 frag (v2f i) : SV_Target
{
// i.screenPos 在 [0,1] 区间 //
float2 screenPos = i.screenPos.xy/i.screenPos.w;
screenPos.xy *= _ScreenParams.xy;
float4 finalColor;
if(screenPos.x >= 300 && screenPos.x <= 500)
{
finalColor = float4(1,1,1,1);
}
else
{
finalColor = float4(0,0,0,1);
}
return finalColor;
}
这个代码主要功能就是用来测试屏幕坐标是否准确,在 x方向 [300, 500] 范围内显示白色,其余范围显示黑色,效果如图:
关于为什么在frag
中除以w分量,而不在vert
中除,目前还没想明白。对 clipPos.x/clipPos.w
进行插值 和 先对clipPos.x
、clipPos.w
进行插值然后再 clipPos.x/clipPos.w
的差别我在纸上做过计算,虽然知道应该是前者正确,但是我怎么计算都感觉后者正确才对啊, 如果有知道的同学强烈要求告知下哈,多谢了。
作为对比,我把Shader修改成在vert中除以了w分量,而在frag中不再除以w,代码如下:
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 screenPos : TEXCOORD1;
};
v2f vert (appdata_base v)
{
v2f o;
o.uv = v.texcoord;
o.pos = UnityObjectToClipPos(v.vertex);
o.screenPos = ComputeScreenPos (o.pos);
// 在vert中除以w分量 //
o.screenPos.xy /= o.screenPos.w;
return o;
}
sampler2D _MainTex;
fixed4 frag (v2f i) : SV_Target
{
// frag中不再除以w //
float2 screenPos = i.screenPos.xy; //i.screenPos.w;
screenPos.xy *= _ScreenParams.xy;
float4 finalColor;
if(screenPos.x >= 300 && screenPos.x <= 500)
{
finalColor = float4(1,1,1,1);
}
else
{
finalColor = float4(0,0,0,1);
}
return finalColor;
}
得到的效果没有变化,可能这个测试比较简单所以没有体现出来两种方式的差别,我在写Decal效果时也用到了ComputeScreenPos
的计算结果作为uv去读取深度图,在 decal的测试 中如果把w分量的除法在vert中进行,则会看到明显的区别,
能看出来vert中除以w时贴花表面有变化,不是贴在物体上的感觉,换个角度效果更明显: