游戏蛮牛学习群(纯技术交流,不闲聊):159852603
游戏蛮牛 手机端
开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

开发者专栏

关注:2332

当前位置:游戏蛮牛 技术专区 开发者专栏

__________________________________________________________________________________
开发者干货区版块规则:

  1、文章必须是图文形式。(至少2幅图)
      2、文章字数必须保持在1500字节以上。(编辑器右下角有字数检查)
      3、本版块只支持在游戏蛮牛原创首发,不支持转载。
      4、本版块回复不得无意义,如:顶、呵呵、不错......【真的会扣分的哦】
      5、......
__________________________________________________________________________________
查看: 1878|回复: 26

[士郎] 着色器特效--垂直雾(Vertical Fog)

[复制链接]  [移动端链接]
排名
3
昨日变化

6878

主题

7402

帖子

2万

积分

Rank: 16

UID
1231
好友
185
蛮牛币
9883
威望
30
注册时间
2013-7-29
在线时间
3535 小时
最后登录
2018-10-18

社区QQ达人活力之星原创精华达人突出贡献奖财富之证游戏蛮牛QQ群会员蛮牛妹VIP

发表于 2018-5-16 10:38:19 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
垂直雾(Vertical Fog)是个啥
        想必大家都知道雾特效, 一般来讲, 距离摄像机越远的点, 其受到雾特效的影响会越为严重. 这是最为常见的雾特效.
        但是还有一种雾, 在某一点的浓度与其和观察点间的距离关系似乎并不大, 而与其世界位置坐标有非常紧密的联系.
1.jpg
本人不太懂物理, 姑且理解为雾霾因自身受到的重力而产生了沉积作用, 使得距离地面较近的区域雾浓度特别高, 但是一旦高过某个阈值, 浓度则开始急剧下降.
2.jpg
3.jpg
以上三张照片全部来自谷歌图片, 摄于Dubai.

        知道了Vertical Fog是什么东西之后, 我们就需要知道这个特效有什么用.
        在很多Top-Down类型的游戏(比如LOL, Dota, Space Marshall)中想要加入雾特效的话, 使用传统的基于距离和深度的雾特效会导致效果失真. 这是因为Top-Down的视角是比较广的, 简单粗暴地糊上一层雾会导致许多距离摄像机较远, 但又很重要的部分被渲染为白茫茫的雾. 与此相反, 我们只希望在放置GameObject的那一层产生比较集中的雾, 因此可以考虑基于每一个点的世界坐标决定其雾的浓度.

所以说我们要干啥
        实现基于Image Effect的Vertical Fog效果. 当然了思路是相通的, 如果想要局部地添加雾特效, 也可以将类似的着色器特效应用于模型上, 然后注意调整模型的Blend与ZTest就好了.

想看懂这篇文章, 我得知道啥
        需要知道DepthBuffer或Unity的CameraDepthTexture. 对点元着色器与片元着色器有一定了解.

看完这篇文章, 我能知道啥
        你将知道如何在片元着色器中重新构建每一个像素点的世界坐标. (Reconstruct world position from Depth Buffer in Pixel Shader), 如何实现垂直雾特效.

首先, 假设我们什么都不知道
        恩, 假设我们什么都不知道, 只有一个处理前的图和处理后的效果图:
4.jpg
(原图)
5.jpg
(处理后)
6.jpg
(也是处理后, 只是摄像机的位置被调得高了很多)
通过上面的三张图, 我们能得到如下的结论:
  • 某点的雾的浓度, 和该点的世界Y坐标有关系.
  • 某点的雾的浓度, 和摄像机的位置, FOV, 角度等都没有任何关系.
  • 雾的浓度符合某种数学公式, 使其沉积在了比较低的区域.
  • 这个特效适合俯视被观察区域的情况. 如果身在雾中的话, 恐怕什么都看不清楚.
很明显, 只要能知道某一点的世界Y坐标, 那什么问题都解决了.

Naive思路(直白, 低效)
        在片元着色器中逆推出每一个片元的View Space坐标, 然后乘以_InverseView矩阵将之转化回World Space.
        首先我们将片元屏幕坐标重新映射到[-1, 1]的区间以回归到NDC Space, 随后将NDC Space转化为View Space, 再通过从C#脚本传入的摄像机的世界-摄像机变换矩阵的逆求出其世界坐标.
[AppleScript] 纯文本查看 复制代码
 float depth = LinearEyeDepth ( SAMPLE_DEPTH_TEXTURE( _CameraDepthTexture, i.uv ) );
        float2 p11_22 = float2 ( unity_CameraProjection._11, unity_CameraProjection._22 );

        float3 vpos = float3( ( i.uv * 2 - 1) / p11_22, -1 ) * depth;

        float4 wpos = mul( _InverseView, float4( vpos, 1 ) );
    	return wpos.y / 10;//下文中会说到为什么写得这么奇怪. 

   这段程序中vpos的计算过程为了让代码看起来简单点而做了一点变化, 更加直观的方式是这样的:
[AppleScript] 纯文本查看 复制代码
float2 ndc = i.uv * 2 - 1;
float3 vpos;
vpos.x = ndc / p11_22.x * depth;
vpos.y = ndc / p11_22.y * depth;
vpos.z = -depth;

   unity_CameraProjection中存储的是投影矩阵. (这个是OpenGL的版本)其形式如下:
7.jpg
点元着色器的坐标变化处理过程如下:
8.jpg
(这个图是DirectX的版本, 换到OpenGL的话, 倒数第二个框框中y的处理应该和x一样)

        我们现在相当于在倒数第二个框框内, 唯一不同的是我们采用的是uv坐标, 范围是[0, 1]而并非[0, width]与[0, height]. 因此第二段程序所做的, 就是利用uv坐标来求出NDC Space坐标. 注意, 到此为止完全和Z没有任何关系. 所以我们只需要让x分量除以, 让y分量除以即可.
        转化回NDC Space后, 由于我们本质上已经做过了标准化和剪裁, 因此倒数第四个与第五个框框跳过, 我们的逆推过程进入到了蓝色的大框框中. 而根据坐标变换规则, 我们有如下等式:
0.1.png
OpenGL中View Space的Z轴正方向背离View Frustum. 而通过CameraDepthTexture我们得到的值均为正, 因此需要特殊变换一下.
OK, 到此为止我们已经成功将Screen Space丢到了View Space中, 我们只需要在C#脚本中插入如下代码, 就可以将世界-摄像机变换矩阵的逆传入:
[AppleScript] 纯文本查看 复制代码
material.SetMatrix ( "_InverseView", GetComponent<Camera> ().cameraToWorldMatrix );

然后, 乘以这个矩阵即可:
[AppleScript] 纯文本查看 复制代码
float4 wpos = mul ( _InverseView, vpos );

现在说明下为什么我们最后要查看的是wpos.y / 10. 其实这个10是我顺手敲上去的(卖个萌), 人眼对于暗色的分辨能力高于对明亮颜色的分辨能力, 因此这个过程非常类似于Gamma Correction. 但是为什么我这没有用乘方的形式进行校正呢? 这是因为严格意义上来讲我们输出的是"坐标", 而游戏场景中的坐标可能会比较大, 比如厨房的柜子顶端其y轴坐标就达到了2.5m. 因此不如简单粗暴地除以10, 这样也很容易查看我们最后的结果是否正常.
结果:
9.jpg
如图所示, 越高的地方越明亮.
为什么上面介绍的方法不好? 我们都知道矩阵乘法是一个颇为耗资源的一个操作, 哪怕搬到GPU上也一样. 而上面的做法是在片元着色器中做坐标转换, 屏幕分辨率是多少就做了多少次矩阵乘法 ...
        不知道读者是不是和大萌喵一样有一种感觉: 一个特定的ViewPort Position和一个特定的深度值, 是能够唯一确定一个世界坐标的. 大萌喵不会画图 ... 诸位脑补一下哈, 透视投影的过程中, 处在同一条从摄像机射出的射线上的点, 最终会被绘制到同一个位置上. (这也就是深度测试的意义之一 --- 只让最近的那个点被绘制出来). 但是如果我们又同时知道了射线上的某个点到摄像机的距离, 那么这个点就是唯一确定的.
        那么最后我们要得到的世界坐标就是ray * depth + _WorldSpaceCameraPos.
        恩, 如果能快速得到这条射线就好了. 其实得到这条射线的方法简单的令人发指.
  • 我们可以在C#脚本中计算出摄像机到其View Frustum的远剪裁面的四个角的世界坐标射线.
  • 对于全屏幕的后期特效, 其实就是一个全屏幕的Quad, 四个顶点. 一个顶点对应上述的一个角.
  • 点元着色器输出至片元着色器的过程中自带插值 ... 我们什么都不用做, 就这么华丽丽地得到了想要的射线.

      求View Frustum四个角的世界坐标的C#程序:
[AppleScript] 纯文本查看 复制代码
Matrix4x4 frustumCorners = Matrix4x4.identity;

        float fovWHalf = camFov * 0.5f;

        Vector3 toRight = m_camTrans.right * camNear * Mathf.Tan (fovWHalf * Mathf.Deg2Rad) * camAspect;
        Vector3 toTop = m_camTrans.up * camNear * Mathf.Tan (fovWHalf * Mathf.Deg2Rad);

        Vector3 topLeft = (m_camTrans.forward * camNear - toRight + toTop);
        float camScale = topLeft.magnitude * camFar/camNear;

        topLeft.Normalize();
        topLeft *= camScale;

        Vector3 topRight = (m_camTrans.forward * camNear + toRight + toTop);
        topRight.Normalize();
        topRight *= camScale;

        Vector3 bottomRight = (m_camTrans.forward * camNear + toRight - toTop);
        bottomRight.Normalize();
        bottomRight *= camScale;

        Vector3 bottomLeft = (m_camTrans.forward * camNear - toRight - toTop);
        bottomLeft.Normalize();
        bottomLeft *= camScale;

        frustumCorners.SetRow (0, topLeft);
        frustumCorners.SetRow (1, topRight);
        frustumCorners.SetRow (2, bottomRight);
        frustumCorners.SetRow (3, bottomLeft);

        material.SetMatrix ("_FrustumCornersWS", frustumCorners);

现在我们的问题是如何让这个矩阵代表的四个角与Screen Quad的四个角一一对应.
通过观察, 我们得到了如下关系:uv.x = 0, uv.y = 0  ------ index = 3;
uv.x = 1, uv.y = 0 ------ index = 2;
uv.x = 1, uv.x = 1 ------ index = 1;
uv.x = 0, uv.y = 1 ------ index = 0;
我们需要知道一个函数F(x, y) = index, 使其能符合上述关系. 否则我们在点元着色器中就要使用if来判断x与y的关系, 从而和z一一对应. 摆脱了矩阵乘法, 然后引入了一坨if ... 这波真亏.
容易推知如下关系: F(x, y) = abs (3 - x - 3 * y).

重获新生的点元着色器
[AppleScript] 纯文本查看 复制代码
v2f vert (appdata_img v)
			{
				v2f o;
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = v.texcoord.xy;

				int xx = (int)v.vertex.x;
				int yy = (int)v.vertex.y;
				int z = abs (3 - xx - 3 * yy);
				o.interpolatedRay = _FrustumCornersWS[ z ];

				o.interpolatedRay.w = ( v.vertex.z );

				return o;
			}


是时候计算雾的浓度了
        首先我们获取每一个片元的世界坐标. 由于射线射向的是远剪裁面, 因此这里将DepthBuffer Linearize的时候不要转化成EyeSpace ... 应该是01Space.
[AppleScript] 纯文本查看 复制代码
float depth = Linear01Depth ( SAMPLE_DEPTH_TEXTURE ( _CameraDepthTexture, UnityStereoScreenSpaceUVAdjust ( i.uv, _CameraDepthTexture_ST ) ) );
				float3 worldPos = ( depth * i.interpolatedRay ).xyz + _WorldSpaceCameraPos;

然后应用一个随着高度指数衰减的密度函数就可以了, 在这里大萌喵随便写了一个, 仅供参考啦:
[AppleScript] 纯文本查看 复制代码
return lerp (tex2D (_MainTex, i.uv), _FogColor, saturate(exp(-worldPos.y - _Start) * _Density));

最终效果
10.jpg

如何实现Fluffy效果
        我们也可以不急着输出, 而是将计算好的Factor存储到一张单独的RT中, 然后对那张RT进行提取, 模糊的操作, 然后再对颜色进行插值.

后记
        写这样一篇文章也算是有感而发 ... 之前在GameDev上有童鞋问怎么不在片元着色器中做矩阵乘法以重构世界坐标, 大萌喵解释了七八个回帖也没给人家弄明白- -那时候自己也是一知半解. 最近在看Siggraph实现别人的特效的时候, 发现作者居然也用了Naive方法 ...
知乎@音速键盘猫




跟我念“站长妹纸萌萌哒!”我说站长,你说YO!爱你们么么哒~
回复

使用道具 举报

5熟悉之中
539/1000
排名
4426
昨日变化

3

主题

58

帖子

539

积分

Rank: 5Rank: 5

UID
259967
好友
0
蛮牛币
210
威望
0
注册时间
2017-12-16
在线时间
158 小时
最后登录
2018-10-20
发表于 2018-5-16 10:57:18 | 显示全部楼层

回复

使用道具 举报

5熟悉之中
512/1000
排名
5257
昨日变化
1

1

主题

88

帖子

512

积分

Rank: 5Rank: 5

UID
246321
好友
0
蛮牛币
1136
威望
0
注册时间
2017-9-27
在线时间
169 小时
最后登录
2018-10-19
发表于 2018-5-16 11:02:35 | 显示全部楼层

回复

使用道具 举报

5熟悉之中
725/1000
排名
5188
昨日变化
2

7

主题

196

帖子

725

积分

Rank: 5Rank: 5

UID
193233
好友
4
蛮牛币
1810
威望
0
注册时间
2016-12-17
在线时间
252 小时
最后登录
2018-10-19
发表于 2018-5-16 11:14:04 | 显示全部楼层
虽然看不懂,但是很强大的样子
[发帖际遇]: ddddddd2 发帖时在路边捡到 1 蛮牛币,偷偷放进了口袋. 幸运榜 / 衰神榜

回复 支持 反对

使用道具 举报

7日久生情
1959/5000
排名
1038
昨日变化
1

0

主题

365

帖子

1959

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
78535
好友
5
蛮牛币
1962
威望
0
注册时间
2015-3-10
在线时间
586 小时
最后登录
2018-10-21
发表于 2018-5-16 11:51:29 | 显示全部楼层
楼主太厉害了谢谢分享

回复 支持 反对

使用道具 举报

5熟悉之中
864/1000
排名
2712
昨日变化

0

主题

107

帖子

864

积分

Rank: 5Rank: 5

UID
53937
好友
0
蛮牛币
716
威望
0
注册时间
2014-11-7
在线时间
254 小时
最后登录
2018-9-1
发表于 2018-5-16 11:56:49 | 显示全部楼层
没有一个完整的shader!
[发帖际遇]: kaitiren 乐于助人,奖励 2 蛮牛币. 幸运榜 / 衰神榜

回复 支持 反对

使用道具 举报

5熟悉之中
541/1000
排名
5462
昨日变化
1

2

主题

142

帖子

541

积分

Rank: 5Rank: 5

UID
252889
好友
2
蛮牛币
1840
威望
0
注册时间
2017-11-6
在线时间
155 小时
最后登录
2018-10-16
QQ
发表于 2018-5-17 08:24:59 | 显示全部楼层
{:104:}

回复

使用道具 举报

2初来乍到
125/150
排名
14107
昨日变化
5

0

主题

32

帖子

125

积分

Rank: 2Rank: 2

UID
237430
好友
0
蛮牛币
246
威望
0
注册时间
2017-8-14
在线时间
39 小时
最后登录
2018-6-29
发表于 2018-5-17 09:12:21 | 显示全部楼层

回复

使用道具 举报

7日久生情
1631/5000
排名
3081
昨日变化

3

主题

867

帖子

1631

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
246489
好友
1
蛮牛币
2708
威望
0
注册时间
2017-9-28
在线时间
311 小时
最后登录
2018-10-11

活力之星

发表于 2018-5-17 09:16:30 | 显示全部楼层
厉害了,6666
[发帖际遇]: 一个袋子砸在了 ss_luck 头上,ss_luck 赚了 1 蛮牛币. 幸运榜 / 衰神榜

回复

使用道具 举报

6蛮牛粉丝
1461/1500
排名
1954
昨日变化

0

主题

375

帖子

1461

积分

Rank: 6Rank: 6Rank: 6

UID
136635
好友
0
蛮牛币
1462
威望
0
注册时间
2016-2-15
在线时间
430 小时
最后登录
2018-10-19
发表于 2018-5-17 09:17:46 | 显示全部楼层
厉害了,学习学习

回复

使用道具 举报

7日久生情
1884/5000
排名
1438
昨日变化
1

0

主题

589

帖子

1884

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
164723
好友
0
蛮牛币
3518
威望
0
注册时间
2016-8-29
在线时间
469 小时
最后登录
2018-7-31
发表于 2018-5-17 12:12:11 | 显示全部楼层

回复

使用道具 举报

5熟悉之中
649/1000
排名
8800
昨日变化
1

0

主题

424

帖子

649

积分

Rank: 5Rank: 5

UID
250036
好友
0
蛮牛币
1495
威望
0
注册时间
2017-10-21
在线时间
99 小时
最后登录
2018-10-18
发表于 2018-5-17 13:12:43 | 显示全部楼层
走过路过水一个

回复

使用道具 举报

5熟悉之中
867/1000
排名
3590
昨日变化

0

主题

270

帖子

867

积分

Rank: 5Rank: 5

UID
229748
好友
0
蛮牛币
2498
威望
0
注册时间
2017-7-1
在线时间
207 小时
最后登录
2018-9-27
发表于 2018-5-17 13:34:12 | 显示全部楼层
感谢分享

回复

使用道具 举报

5熟悉之中
867/1000
排名
3299
昨日变化
2

6

主题

127

帖子

867

积分

Rank: 5Rank: 5

UID
229395
好友
1
蛮牛币
2005
威望
0
注册时间
2017-6-29
在线时间
304 小时
最后登录
2018-9-7
发表于 2018-5-17 14:53:11 | 显示全部楼层

回复

使用道具 举报

5熟悉之中
691/1000
排名
5528
昨日变化

3

主题

309

帖子

691

积分

Rank: 5Rank: 5

UID
269155
好友
2
蛮牛币
1170
威望
0
注册时间
2018-2-22
在线时间
139 小时
最后登录
2018-10-20
发表于 2018-5-17 14:58:35 | 显示全部楼层
真棒啊啊啊啊

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

快速回复 返回顶部 返回列表