关于ComputeScreenPos和ComputeGrabScreenPos的差别

Posted by 恶毒的狗 on January 2, 2020

一个Bug

今天QA报了一个渲染相关的bug:一个用了 扭曲 效果的翅膀特效在场景相机下显示正常,但是在UI相机上却有问题,截图如下:

img

扭曲背景 上下颠倒 了。


Bug的修正

这里用的 扭曲shader 是我们的美术同学从他们前项目搬过来的,代码很简单:

  • GrabPass 抓取当前屏幕做为扭曲背景。
  • 添加 UV扰动 后再采样屏幕背景,即可达到扭曲效果。

问题是,这里采样 GrabTexture 的时候用的是 screenUV 而非 grabUV,代码如下:

顶点着色器:

1
	o.screenPos = ComputeScreenPos (o.pos);

像素着色器:

1
2
	float2 sceneUVs = (i.screenPos.xy / i.screenPos.w) + (_Value * diffuseTex.a * float2(diffuseTex.r, diffuseTex.g) * i.vertexColor.a);
	half4 sceneColor = tex2D(_GrabTexture, sceneUVs);

修正这个问题很简单,把 ComputeScreenPos 换成 ComputeGrabScreenPos 即可,修正后的代码如下:

顶点着色器:

1
o.screenPos = ComputeGrabScreenPos (o.pos);

调整完之后就正常了,如下图:

img


关于ComputeScreenPos和ComputeGrabScreenPos的差别

修正容易,但是搞清楚 ComputeScreenPosComputeGrabScreenPos 的差别却要费一些功夫。

我们看一下相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
inline float4 ComputeNonStereoScreenPos(float4 pos) {
    float4 o = pos * 0.5f;    
    o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
    o.zw = pos.zw;
    return o;
}

inline float4 ComputeScreenPos(float4 pos) {
    float4 o = ComputeNonStereoScreenPos(pos);
#if defined(UNITY_SINGLE_PASS_STEREO)
    o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w);
#endif
    return o;
}

inline float4 ComputeGrabScreenPos (float4 pos) {
    #if UNITY_UV_STARTS_AT_TOP
    float scale = -1.0;
    #else
    float scale = 1.0;
    #endif
    float4 o = pos * 0.5f;    
    o.xy = float2(o.x, o.y*scale) + o.w;
#ifdef UNITY_SINGLE_PASS_STEREO
    o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w);
#endif
    o.zw = pos.zw;
    return o;
}

通过分析可以发现,这两个函数的主要差别就是 UNITY_UV_STARTS_AT_TOP_ProjectionParams.x 的差别。

我们知道 RenderTexture 的纹理坐标在 Direct3D-like 平台和 OpenGL-like 平台存在差异:

  • Direct3D-like平台,UNITY_UV_STARTS_AT_TOP = 1,纹理坐标0在顶部,并往下增长。
  • OpenGL-like平台,UNITY_UV_STARTS_AT_TOP = 0,纹理坐标0在底部,并往上增长。

当渲染到纹理的时,Unity遵从 OpenGL-like 平台的标准。

当工作在 Direct3D-like 平台时,为了兼容这个平台差异,Unity会 翻转投影矩阵 从而翻转 RenderTexture,这样既遵从了 OpenGL-like 平台的约定,又可以获取正确的采样结果。

_ProjectionParams.x 标识了投影矩阵是否经过翻转。

  • _ProjectionParams.x = 1表示没有翻转。
  • _ProjectionParams.x = -1表示翻转。

那么,是不是 Direct3D-like 平台下 RenderTexture 一定会进行翻转操作呢?如果没有翻转,而Unity又采用了 OpenGL-like 平台的约定,这种情况要怎么处理呢?

事实上,Unity在一些情况下确实不会翻转 RenderTexture,它的帮助文档 Platform-specific rendering differences 这一章节列举了 Direct3D-like 平台下不翻转 RenderTexture 的几种情况:

  • Image Effects + 抗锯齿
  • GrabPass

对于 GrabPass,Unity文档做了特别说明:在 Direct3D-like 平台下,GrabPass 不会进行 RenderTexture 的翻转操作,因此我们需要在shader中手工翻转uv以获取正确的采样结果。

ComputeGrabScreenPos 这里只需要判断 UNITY_UV_STARTS_AT_TOP 的取值:

  • 如果是 Direct3D-like 平台(UNITY_UV_STARTS_AT_TOP = 1),我们就需要手工翻转uv。
  • 如果是 OpenGL-like 平台(UNITY_UV_STARTS_AT_TOP = 0),则无需翻转uv。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
inline float4 ComputeGrabScreenPos (float4 pos) {
    #if UNITY_UV_STARTS_AT_TOP
    float scale = -1.0;
    #else
    float scale = 1.0;
    #endif
    float4 o = pos * 0.5f;    
    o.xy = float2(o.x, o.y*scale) + o.w;
#ifdef UNITY_SINGLE_PASS_STEREO
    o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w);
#endif
    o.zw = pos.zw;
    return o;
}

Direct3D-like 平台下,如果我们用 _ProjectionParams.x 来判断是否需要手工翻转uv就错了,因为 RenderTexture 并未发生翻转,此时 _ProjectionParams.x = 1。


关于投影矩阵的翻转

bug是修正了,也知道了原因:对于GrabPass,我们应该用 UNITY_UV_STARTS_AT_TOP 而非 _ProjectionParams.x 去判断是否要手工进行uv翻转。

但是还有一个疑问没有解开:前文说了,这个 扭曲 特效在场景相机下工作正常,在UI相机下才有问题,这又是为什么呢?

说到相机,我们游戏内一共3个相机,渲染顺序如下:

场景相机(开后处理) –> UI相机1(关后处理) –> UI相机2(关后处理)

出现问题的相机是 UI相机2,此时我们并没有开 抗锯齿,并且 场景相机 处于 关闭 状态。

如果我们打开 场景相机,或者把 UI相机1 的后处理打开,又或者把 UI相机2 的后处理打开,这些情况下这个bug都不会出现。

似乎 多相机 以及 Image Effect的开关 也会影响 _ProjectionParams.x 的设值。

可惜的是,Unity文档对 投影矩阵的翻转 语焉不详,只是告诉你 _ProjectionParams.x = -1 即代表了翻转:

x is 1.0 (or –1.0 if currently rendering with a flipped projection matrix)

没有源码的情况下,何时翻转投影矩阵就比较难说清楚了。

不过我们只需要记得:

  • _ProjectionParams.x = -1 代表翻转了投影矩阵,在计算 屏幕坐标 的时候,如果发生了 投影矩阵翻转,那么我们也需要在shader中手工翻转uv,这样才能获得正确的 屏幕坐标
  • Unity的 ComputeScreenPos 帮我们处理好了这个过程。

早前在写 Fantastic SSR Water 这个插件的时候,我也遇到过类似的问题。

Fantastic SSR Water 是一款关于水的插件,用 屏幕空间反射 实现水的反射。

  • 因为需要在屏幕空间计算 光线步进,因此我需要计算屏幕坐标 screenUV
  • 因为用了 GrabPass 去抓取屏幕颜色以计算反射颜色,因此我还需要计算 grabUV

当时,我错误的把 screenUVgrabUV 等同了,然后发现只有在特定的设置选项下渲染才正确,设置选项包括:

  • 平台的选择
  • 前向渲染/延迟渲染的选择
  • 抗锯齿开关的选择

后面,我用 ComputeScreenPos 去计算 screenUV,用 ComputeGrabScreenPos 去计算 grabUV,问题就解决了,在各种设置组合下渲染都正确。

最后,附 Fantastic SSR Water 截图一张:

img

好了,拜拜。