关于ComputeScreenPos和ComputeGrabScreenPos的差别

一个Bug

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

screenshot1.png

扭曲背景 上下颠倒 了。


Bug的修正

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

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

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

顶点着色器:

    o.screenPos = ComputeScreenPos (o.pos);

像素着色器:

    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 即可,修正后的代码如下:

顶点着色器:

o.screenPos = ComputeGrabScreenPos (o.pos);

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

screenshot2.png

关于ComputeScreenPos和ComputeGrabScreenPos的差别

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

我们看一下相关代码:

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。
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 截图一张:

screenshot3.jpg

个人主页

本文的个人主页链接:https://baddogzz.github.io/2020/01/02/GrabUV-Bug/

好了,拜拜。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,193评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,306评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,130评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,110评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,118评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,085评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,007评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,844评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,283评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,508评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,395评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,985评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,630评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,797评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,653评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,553评论 2 352