Unity Shader 中获取屏幕坐标

项目中有时会有一些效果需求,如重建片元在世界空间的坐标或者对屏幕指定区域进行颜色操作等,这时就需要获取到片元对应的屏幕坐标(Screen Space Coordinate)。在Unity中有三种方法可以获取到屏幕坐标,分别是:

  1. SV_POSITION 语义的xy变量
  2. VPOS 语义
  3. ComputeScreenPos

SV_POSITION 语义的xy变量

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.xclipPos.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坐标,用于进行透视矫正。

VPOS 语义

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_POSITIONVPOS语义应该是同一回事,而文档里也做了说明:

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.

ComputeScreenPos

还有一种方式是使用Unity自带的宏,在顶点方法中使用 ComputeScreenPos 宏,在片元方法中把计算结果的xy分量除以w分量,此时xy分量范围是[0,1],一般用于去读取深度图或者grabpass抓取的图等地方,要得到正确的屏幕空间坐标还需要再乘以屏幕的宽高,Unity内置的宏 _ScreenParams 中提供了关于屏幕宽高的数据。
Unity Shader 中获取屏幕坐标_第1张图片

主要代码如下:

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] 范围内显示白色,其余范围显示黑色,效果如图:
Unity Shader 中获取屏幕坐标_第2张图片

关于为什么在frag中除以w分量,而不在vert中除,目前还没想明白。对 clipPos.x/clipPos.w 进行插值 和 先对clipPos.xclipPos.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中进行,则会看到明显的区别,

frag中除以w:
Unity Shader 中获取屏幕坐标_第3张图片

vert中除以w:
Unity Shader 中获取屏幕坐标_第4张图片

能看出来vert中除以w时贴花表面有变化,不是贴在物体上的感觉,换个角度效果更明显:
Unity Shader 中获取屏幕坐标_第5张图片

参考链接:
https://docs.unity3d.com/Manual/SL-BuiltinFunctions.html
https://forum.unity.com/threads/what-does-the-function-computescreenpos-in-unitycg-cginc-do.294470/
https://dev.rbcafe.com/unity/unity-5.3.3/en/Manual/SL-VertexFragmentShaderExamples.html

你可能感兴趣的:(Unity,Shader)