Unity HDRP Custom Pass (Post processing) 后处理特效学习(二)总结

目录

B站视频

B站视频,录制不易,求关注、一键三连(点赞、投币、收藏)。置顶评论内有PPT(内有优秀参考资源链接)、工程、插件、群号。

官方社区

文字版总结,编写不易,求关注、点赞。手机版阅读困难的,记得是在电脑浏览器中查看 开发者社区

分享背景

突然开始做分享主要2个原因。
  • 有朋友和我说Unity是不是只是手游引擎呀,UE能做的Unity能不能做呀,恰巧看到虚幻官方月神的直播讲后处理,所以就试着也录一期Unity的,证明Unity并不只是手游引擎。
  • 最近被一个Unity官方合作的知名博主up主麦扣的群里的管理员傲娇修酒z侮辱了,发现如果没有粉丝,被人侮辱的时候甚至没有人帮我说话,所以希望吸引一些愿意帮我说话的粉丝。除非麦扣把侮辱人的管理员踢掉,否则这事情永远不会揭过。
另外这可能不是最好的分享,但是绝对是最详细的分享,不会不求甚解的去分享一些一眼就能看明白的东西。

感谢

首先要感谢一下叶月葵(Hazukiaoi),没有他,无法制作这系列视频和分享。

上一期有更新

上一期有更新,补充了很多内容和配图,务必重新阅读一遍。 Unity HDRP Custom Pass (Post processing) 后处理特效学习(一)总结

屏幕尺寸 _ScreenSize

屏幕尺寸_ScreenSize是一个float4,内容分别是(W,H,1/W,1/H)。W和H很好理解,就是屏幕宽高。那么1/W和1/H是什么用途的呢?可以回想上一期视频和分享中提到的PositionInput结构体中的positionNDC跟positionSS,_ScreenSize主要用于这两个坐标之间的转换。
    
    
positionNDC = positionSS * _ScreenSize . zw ; positionSS = positionNDC * _ScreenSize . xy ;

ddx/ddy

ddx

返回指定值相对于屏幕空间x坐标的偏导数。

ddy

返回指定值相对于屏幕空间y座标的偏导数。

计算方法

在三角形光栅化过程中,GPU一次运行许多片段着色器的实例,将它们组织成2×2像素的块。dFdx是将块中像素值的差值减去块左侧的像素值和右侧的像素值,dFdy是将底部像素的值减去顶部像素的值。请看下面的图片,其中网格表示渲染的屏幕像素,dFdx、dFdy表达式是为由片段着色器实例在(x、y)屏幕坐标处计算的通用值p提供的,属于2×2块的红色高亮显示的2×2块。导数可以对片段着色器中的每个变量进行计算。 对于向量和矩阵类型,导数是按元素计算的。
值得注意的是图上是以glsl中的dFdx和dFdy进行讲解,实际上Unity中使用的是hlsl中的ddx和ddy,并且是以左下角为坐标原点。其次,既然ddx和ddy是像图中一样,ddx(p(x,y))=p(x+1,y)-p(x,y),ddy(p(x,y))=p(x,y+1)-p(x,y)。对于显卡内部计算这个2x2块,并不知道旁边的2x2块的数据,那么他是怎么计算ddx(p(x+1,y))、ddx(p(x,y+1))、ddx(p(x+1,y+1))、ddy(p(x+1,y))、ddy(p(x,y+1))、ddy(p(x+1,y+1))的呢?
怀着这个疑问,进行了测试与探讨,最终得出结论,2x2块中,4个像素的ddx共享一个值,4个像素的ddy共享一个值。ddx只与p(x,y)、p(x+1,y)有关,ddy只与p(x,y)、p(x,y+1)有关。无论ddx与ddy都与p(x+1,y+1)无关。
可以自行查看工程中的CheckDDX场景进行验证,下面有些截图证明这个结论。
从上面几张图,可以看出,ddx跟左下p(x,y),右下p(x+1,y)有关,跟左上p(x,y+1),右上p(x+1,y+1)无关。看颜色变化,输出的是ddx的值。
从上面几张图,可以看出,ddy跟左下p(x,y),左上p(x,y+1)有关,跟右下p(x+1,y),右上p(x+1,y+1)无关。看颜色变化,输出的是ddy的值。
从上面几张图,可以看出,ddx ddy都跟右上右上p(x+1,y+1)无关。
那为什么一定要搞明白这个ddx和ddy的计算方式呢?那是因为后面介绍的第一种最简单的勾边方式,就是使用ddx、ddy对设备深度、线性深度、世界法线计算来达到勾边的目的。但是你会发现ddx和ddy的方式勾边会有断线的情况,其实这个就是因为这个4个像素共享ddx ddy值导致的,所以如果真的用ddx、ddy进行勾边,一般还会加上一个TAA的抗锯齿。

abs

返回指定值的绝对值。

clamp/saturate

clamp

将指定的值夹在指定的最小和最大范围内。

saturate

将指定的值夹在0到1的范围内。如果你是要夹在0到1的之间的话,应该使用saturate,他基本上是一个硬件级别的函数,几乎无消耗。

使用ddx、ddy进行勾边

使用设备深度

可以看到,简单的使用ddx、ddy就能做到一个简单的勾边。
首先来说计算方法,计算方法就是对设备深度数据进行了ddx、ddy计算后取绝对值相加,然后再通过saturate去把数值夹在0-1之间。
那么再来说说原理,原理就是ddx对ddy分别对每个像素对x、y计算偏导数,得到变化率,变化率高的地方代表设备深度差异大,是边缘。
那么,为什么表面会有亮面一样的效果呢?因为平面并不是平行于摄像机平面,有深度差异,所以变化率并不是0,因此会有微光,如果想去掉可以用后面介绍手动计算勾边的方法里的Contrast方法来增强对比度的方式来去除这些微光。
最后,来说说为什么断线,相信你阅读了前面的分享,你应该已经明白ddx和ddy是2x2块里4个像素共享ddx和ddy的值,因此,除了左下的像素,另外3个像素计算变化率的结果是不准确的,这就是造成断线的原因。
后续在介绍如何手动计算勾边的方法,再介绍如何与原场景融合。
    
    
// ddx & ddy outline by device depth md = posInput . deviceDepth * 1000 ; finalColor = abs ( ddx ( md ) ) + abs ( ddy ( md ) ) ; finalColor = saturate ( finalColor ) ;

使用线性深度

同样的,用相同的原理,通过线性深度使用ddx、ddy做到了一个简单的勾边。
    
    
// ddx & ddy outline by linear depth md = posInput . linearDepth * 10 ; finalColor = abs ( ddx ( md ) ) + abs ( ddy ( md ) ) ; finalColor = saturate ( finalColor ) ;

使用世界法线

同样的,用相同的原理,通过世界法线使用ddx、ddy做到了一个简单的勾边。
那么为什么地上有种脏脏的感觉呢,为什么都被勾上了弱边呢?那是因为地面是凹凸不平的发现,法线变化率有弱小变化。同样的,可以通过后面介绍手动计算勾边的方法里的Contrast方法来增强对比度的方式来去除这些弱边。

distance

返回两个向量之间的距离标量。

lerp

进行线性插值。

max

选择x和y中的较大者。

Contrast

((valueInput - 0.5f) * contrastInput) + 0.5f;
通过简单数值计算,达到增大或者降低对比度的目的。valueInput为原始值,contrastInput为缩放洗漱。
    
    
float Contrast ( float contrastInput , float valueInput ) { return ( ( valueInput - 0.5f ) * contrastInput ) + 0.5f ; }

手动计算进行勾边

使用世界法线

我们先来看整段代码,首先这个方法原理上跟ddx和ddy的方式类似,不一样的是,他并不会像ddx、ddy那样2x2块里的4个像素都共享一个值,而是每个像素都会单独计算。
    
    
// offset outline by world normal linearDepth = posInput . linearDepth ; outlineWidth = _OutlineWidth ; outlineWidth = saturate ( 1 - linearDepth / 1000 ) * outlineWidth ; NormalData normalData0 ; NormalData normalData1 ; NormalData normalData2 ; DecodeFromNormalBuffer ( posInput . positionSS , normalData0 ) ; DecodeFromNormalBuffer ( posInput . positionSS + float2 ( outlineWidth , 0 ) , normalData1 ) ; DecodeFromNormalBuffer ( posInput . positionSS + float2 ( 0 , outlineWidth ) , normalData2 ) ; distanceXNormal = distance ( normalData0 . normalWS , normalData1 . normalWS ) ; distanceYNormal = distance ( normalData0 . normalWS , normalData2 . normalWS ) ; distanceSumNormal = distanceXNormal + distanceYNormal ; distanceSumNormal = distanceSumNormal / _DivideFactor ;
    
    
distanceSum = distanceSumNormal ; distanceSum = distanceSum / _DivideFactor ; distanceSum = Contrast ( _ContrastInput , distanceSum ) ; distanceSum = saturate ( distanceSum ) ; finalColor = lerp ( color , _OutlineColor , distanceSum ) ; //finalColor = color + distanceSum * _OutlineColor;
与ddx、ddy不同的是,ddx、ddy只能跟旁边一个像素对比,自己写的话,可以与偏移好几个像素的进行对比。那么你可以控制勾边的宽度。
    
    
linearDepth = posInput . linearDepth ; outlineWidth = _OutlineWidth ; outlineWidth = saturate ( 1 - linearDepth / 1000 ) * outlineWidth ;
那么我们先来看前三行代码,这里的目的是根据远近来控制勾边的宽度。我们这里先获取线性深度。然后,我们给outlineWidth赋值一个初值_OutlineWidth,这是shader定义的属性,可以方便我们通过材质进行控制勾边的宽度。最后,我们的Near是0.3,Far是1000,那么linearDepth/1000可以计算出与我们的远近比例。最近的为0.3/1000=0.0003,最远为1000/1000=1。那么(1-linearDepth/1000)就变成了最近的是0.9997,最远的是0。这里采用saturate是你可以Far设置1000以上,那么通过saturate可以压回到0到1之间,不会导致有负数的勾边的宽度,这里再乘以初始的勾边的宽度,达到近处勾边粗,远处勾边细的目的。
    
    
NormalData normalData0 ; NormalData normalData1 ; NormalData normalData2 ; DecodeFromNormalBuffer ( posInput . positionSS , normalData0 ) ; DecodeFromNormalBuffer ( posInput . positionSS + float2 ( outlineWidth , 0 ) , normalData1 ) ; DecodeFromNormalBuffer ( posInput . positionSS + float2 ( 0 , outlineWidth ) , normalData2 ) ;
接下来我们读取p(x,y)、p(x+oultineWidth,y),p(x,y+outlineWidth)的法线数据,用于后续的对比。
    
    
distanceXNormal = distance ( normalData0 . normalWS , normalData1 . normalWS ) ; distanceYNormal = distance ( normalData0 . normalWS , normalData2 . normalWS ) ;
由于法线是float3的向量,因此这里采取最简单的对比方式,通过distance对比两个向量的距离标量,达到计算变化率的目的。
    
    
distanceSumNormal = distanceXNormal + distanceYNormal ;
把x跟y的变化率相加,类似ddx、ddy方式里面的计算方式。
    
    
distanceSum = distanceSumNormal ; distanceSum = distanceSum / _DivideFactor ; distanceSum = Contrast ( _ContrastInput , distanceSum ) ; distanceSum = saturate ( distanceSum ) ; //color = float4(0,0,0,1); finalColor = lerp ( color , _OutlineColor , distanceSum ) ;
类似_OulineWidth,我们给一个_DivideFacter的shader属性,因为距离最大可能是2,通过控制_DivideFactor来达到控制勾线强度的目的。用Contrast函数控制对比度,达到把法线导致的脏脏的弱小勾边去除。最后,通过saturate,把值压回到0到1之间。最后,通过lerp在场景颜色和_OutlineColor勾边的颜色之间进行线性差值,达到在原场景颜色上勾边的效果。我们为了观察方便,先把_DivideFactor和_ContrastInput都设置回1,通过去掉color的那行注释可以看到初始勾边效果。 仔细查看,你可以发现工作台上的木棍,上方的勾边没了,那是因为木棍表面刚好平行地面,法线一致,distance为0。所以没能勾出来边,可以通过用设备深度勾边或者混合使用世界法线、设备深度勾边来解决。其次,地面有脏脏的效果,这里,可以通过调节_ContrastInput来增大对比度达到去除这个因为地面呕吐不平法线导致的微小勾边的脏脏的效果去掉。 可以根据场景需要自由调节_ContrastInput和_DivideFactor的值来控制勾边效果。把color那行注释,可以在原场景颜色上勾边了。 除了lerp的勾边方式,还可以使用加法的方式进行勾边,能提亮的效果。
    
    
finalColor = color + distanceSum * _OutlineColor ;

使用线性深度

我们先来看整段代码,首先这个方法和上面使用世界法线不一样的是使用的是设备深度。
    
    
// offset outline by device depth linearDepth = posInput . linearDepth ; outlineWidth = _OutlineWidth ; outlineWidth = saturate ( 1 - linearDepth / 10000 ) * outlineWidth ; float2 positionCS0 = varyings . positionCS . xy ; float depth0 = LoadCameraDepth ( positionCS0 ) ; PositionInputs posInput0 = GetPositionInput ( positionCS0 , _ScreenSize . zw , depth , UNITY_MATRIX_I_VP , UNITY_MATRIX_V ) ; float2 positionCS1 = varyings . positionCS . xy + float2 ( outlineWidth , 0 ) ; float depth1 = LoadCameraDepth ( positionCS1 ) ; PositionInputs posInput1 = GetPositionInput ( positionCS1 , _ScreenSize . zw , depth1 , UNITY_MATRIX_I_VP , UNITY_MATRIX_V ) ; float2 positionCS2 = varyings . positionCS . xy + float2 ( 0 , outlineWidth ) ; float depth2 = LoadCameraDepth ( positionCS2 ) ; PositionInputs posInput2 = GetPositionInput ( positionCS2 , _ScreenSize . zw , depth2 , UNITY_MATRIX_I_VP , UNITY_MATRIX_V ) ; distanceXDepth = distance ( posInput0 . linearDepth , posInput1 . linearDepth ) ; distanceYDepth = distance ( posInput0 . linearDepth , posInput2 . linearDepth ) ; distanceSumDepth = distanceXDepth + distanceYDepth ;
    
    
distanceSum = distanceSumDepth ; distanceSum = distanceSum / _DivideFactor ; distanceSum = Contrast ( _ContrastInput , distanceSum ) ; distanceSum = saturate ( distanceSum ) ; //color = float4(0,0,0,1); finalColor = lerp ( color , _OutlineColor , distanceSum ) ; //finalColor = color + distanceSum * _OutlineColor;
接下来具体看看计算的方法。
    
    
float2 positionCS0 = varyings . positionCS . xy ; float depth0 = LoadCameraDepth ( positionCS0 ) ; PositionInputs posInput0 = GetPositionInput ( positionCS0 , _ScreenSize . zw , depth , UNITY_MATRIX_I_VP , UNITY_MATRIX_V ) ; float2 positionCS1 = varyings . positionCS . xy + float2 ( outlineWidth , 0 ) ; float depth1 = LoadCameraDepth ( positionCS1 ) ; PositionInputs posInput1 = GetPositionInput ( positionCS1 , _ScreenSize . zw , depth1 , UNITY_MATRIX_I_VP , UNITY_MATRIX_V ) ; float2 positionCS2 = varyings . positionCS . xy + float2 ( 0 , outlineWidth ) ; float depth2 = LoadCameraDepth ( positionCS2 ) ; PositionInputs posInput2 = GetPositionInput ( positionCS2 , _ScreenSize . zw , depth2 , UNITY_MATRIX_I_VP , UNITY_MATRIX_V ) ;
前三行跟使用世界法线的方法一样,略过。同理,这里读取p(x,y)、p(x+oultineWidth,y),p(x,y+outlineWidth)的设备深度,用于后续的对比。
    
    
distanceXDepth = distance ( posInput0 . linearDepth , posInput1 . linearDepth ) ; distanceYDepth = distance ( posInput0 . linearDepth , posInput2 . linearDepth ) ;
类似的通过distance方式获得变化率。
    
    
distanceSumNormal = distanceXNormal + distanceYNormal ;
类似的把x跟y的变化率相加。
    
    
distanceSum = distanceSumDepth ; distanceSum = distanceSum / _DivideFactor ; distanceSum = Contrast ( _ContrastInput , distanceSum ) ; distanceSum = saturate ( distanceSum ) ; //color = float4(0,0,0,1); finalColor = lerp ( color , _OutlineColor , distanceSum ) ;
最后通过类似的方式把颜色附加到场景颜色中去。我们为了观察方便,先把_DivideFactor和_ContrastInput都设置回1,通过去掉color的那行注释可以看到初始勾边效果。 仔细查看,你可以发现工作台上的木棍,棱角上的边没了,因为这是通过深度来求变化率,对于棱角法线变化率大的地方,深度变化并不大。其次,地面有发光的效果,在ddx、ddy中使用的深度的方法中已经解释过了。这里,可以通过调节_ContrastInput来增大对比度达到去除这个发光效果。 可以根据场景需要自由调节_ContrastInput和_DivideFactor的值来控制勾边效果。把color那行注释,可以在原场景颜色上勾边了。 类似除了lerp的勾边方式,还可以使用加法的方式进行勾边,能提亮的效果。
    
    
finalColor = color + distanceSum * _OutlineColor ;

使用混合世界法线、设备深度

我们先来看整段代码,这个方法兼顾了世界法线和设备深度,可以在法线变化大和深度变化大的地方同时勾上边。
    
    
// offset outline by world normal linearDepth = posInput . linearDepth ; outlineWidth = _OutlineWidth ; outlineWidth = saturate ( 1 - linearDepth / 1000 ) * outlineWidth ; NormalData normalData0 ; NormalData normalData1 ; NormalData normalData2 ; DecodeFromNormalBuffer ( posInput . positionSS , normalData0 ) ; DecodeFromNormalBuffer ( posInput . positionSS + float2 ( outlineWidth , 0 ) , normalData1 ) ; DecodeFromNormalBuffer ( posInput . positionSS + float2 ( 0 , outlineWidth ) , normalData2 ) ; distanceXNormal = distance ( normalData0 . normalWS , normalData1 . normalWS ) ; distanceYNormal = distance ( normalData0 . normalWS , normalData2 . normalWS ) ; distanceSumNormal = distanceXNormal + distanceYNormal ; // offset outline by device depth linearDepth = posInput . linearDepth ; outlineWidth = _OutlineWidth ; outlineWidth = saturate ( 1 - linearDepth / 10000 ) * outlineWidth ; float2 positionCS0 = varyings . positionCS . xy ; float depth0 = LoadCameraDepth ( positionCS0 ) ; PositionInputs posInput0 = GetPositionInput ( positionCS0 , _ScreenSize . zw , depth , UNITY_MATRIX_I_VP , UNITY_MATRIX_V ) ; float2 positionCS1 = varyings . positionCS . xy + float2 ( outlineWidth , 0 ) ; float depth1 = LoadCameraDepth ( positionCS1 ) ; PositionInputs posInput1 = GetPositionInput ( positionCS1 , _ScreenSize . zw , depth1 , UNITY_MATRIX_I_VP , UNITY_MATRIX_V ) ; float2 positionCS2 = varyings . positionCS . xy + float2 ( 0 , outlineWidth ) ; float depth2 = LoadCameraDepth ( positionCS2 ) ; PositionInputs posInput2 = GetPositionInput ( positionCS2 , _ScreenSize . zw , depth2 , UNITY_MATRIX_I_VP , UNITY_MATRIX_V ) ; distanceXDepth = distance ( posInput0 . linearDepth , posInput1 . linearDepth ) ; distanceYDepth = distance ( posInput0 . linearDepth , posInput2 . linearDepth ) ; distanceSumDepth = distanceXDepth + distanceYDepth ;
    
    
distanceSum = _NormalFactor * distanceSumNormal + ( 1 - _NormalFactor ) * distanceSumDepth ; //distanceSum = max(distanceSumDepth, distanceSumNormal); distanceSum = distanceSum / _DivideFactor ; distanceSum = Contrast ( _ContrastInput , distanceSum ) ; distanceSum = saturate ( distanceSum ) ; //color = float4(0,0,0,1); finalColor = lerp ( color , _OutlineColor , distanceSum ) ; //finalColor = color + distanceSum * _OutlineColor;
这里对distanceSum有两种方式,一种是通过Shader属性_NormalFactor和(1-_NormalFactor)来控制以哪种勾边为主。

     
     
distanceSum = _NormalFactor * distanceSumNormal + ( 1 - _NormalFactor ) * distanceSumDepth ;
一种是通过取max的方式,来确保两种模式的都勾到边,大家可以自行测试。
    
    
//distanceSum = max(distanceSumDepth, distanceSumNormal);

下期预告

下一期分享,将会分享alelievr的HDRP-Custom-Passes库中的TIPS特效,并且介绍其中的原理等。最后,对于本文,求关注、点赞。对于B站视频,求关注、一键三连(点赞、投币、收藏)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值