如何在Unity URP中利用Sobel添加屏幕空间深度边缘光
仔细观察原神中的人物,可以发现角色周围有一层白色的内描边边缘光:
如何在Unity URP中实现类似的效果呢?参考了Adrian Mendez大佬的Youtbue教程,我也尝试着用添加Render Feature的方式实现该效果。实现大致思路是使用Sobel算子对相机深度图像进行边缘检测:
根据大佬提供的思路写了一个Shader,大概就是在原图的基础上把边缘光再叠加上去:
Shader "Unlit/SobelRimLight"
{
Properties
{
_MainTex("Texture", 2D) = "white" {}
_ThicknessX("ThicknessX", Float) = 0.01
_ThicknessY("ThicknessY", Float) = 0.01
_MaxThickness("MaxThickness", Float) = 0.01
_Intensity("Intensity", Range(0,1)) = 0.01
_LerpValue("LerpValue", Range(0,1)) = 1
_Distance("Distance", Float) = 1
_Color("RimLightColor",Color) = (0,0,0)
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 scrPos : TEXCOORD2;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _ThicknessX;
float _ThicknessY;
float _MaxThickness;
float _Intensity;
float _LerpValue;
float _Distance;
sampler2D _CameraDepthTexture;
float4 _CameraDepthTexture_ST;
float4 _Color;
static float2 sobelSamplePoints[9] = {
float2(-1,1),float2(0,1),float2(1,1),
float2(-1,0),float2(0,0),float2(1,0),
float2(-1,-1),float2(0,-1),float2(1,-1),
};
static float sobelXMatrix[9] = {
1,0,-1,
2,0,-2,
1,0,-1,
};
static float sobelYMatrix[9] = {
1,2,1,
0,0,0,
-1,-2,-1,
};
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
o.scrPos = ComputeScreenPos(o.vertex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
float2 sobel = 0;
//获得深度
float originDepth = tex2D(_CameraDepthTexture, i.uv);
//转化为线性0-1深度,这样0.5就表示处于相机到farPlane一半的位置了
originDepth= Linear01Depth(originDepth);
//找到该像素离相机的真实距离,_ProjectionParams.z表示_Camera_FarPlane的距离。https://docs.unity.cn/Packages/com.unity.shadergraph@6.9/manual/Camera-Node.html
float depthDistance = _ProjectionParams.z*originDepth;
float2 adaptiveThickness = float2(_ThicknessX, _ThicknessY);
if (depthDistance <= 0)
adaptiveThickness = float2(_MaxThickness, _MaxThickness);
else
{//根据距离对边缘光厚度进行线性缩放
adaptiveThickness = adaptiveThickness / depthDistance;
}
adaptiveThickness = min(adaptiveThickness, float2(_MaxThickness, _MaxThickness));
for (int id = 0; id < 9; id++)
{
float2 screenPos = i.uv + sobelSamplePoints[id] * adaptiveThickness;
float depth = tex2D(_CameraDepthTexture, screenPos);
depth = Linear01Depth(depth);
sobel += depth * float2(sobelXMatrix[id], sobelYMatrix[id]);
}
fixed4 previousColor = tex2D(_MainTex, i.uv);
fixed4 rimLightColor = _Color * step(_Distance,length(sobel)*_ProjectionParams.z);
//叠加到原来的颜色上
fixed4 col = previousColor + lerp(previousColor*rimLightColor, rimLightColor, _LerpValue) * _Intensity;
return col;
}
ENDCG
}
}
}
由于是专门用来后处理的Shader,使用的话先创建一个材质球,可以看到有如下参数(本菜鸟自己改写的,参数定义可能不太符合规范):
ThicknessX和Y分别控制横向和纵向的边缘光宽度,MaxThickness控制最大边缘光宽度;Intensity控制边缘光的强度;LerpValue越大,边缘光颜色就越像设置的RimLightColor的颜色;Distance控制边缘的深度阈值。不过这种方法计算的边缘光对distance阈值的设置比较敏感,如果过大的话会导致物体重叠处的边缘光消失。比如自我遮挡的时候深度变化比较小时就没有边缘光了,设置太小的话就感觉有点像基于深度的描边。另外我看原神里的貌似竖直方向几乎没有边缘光,所以把ThicknessY设置为0了,可能是为了避免角色鞋底出现所谓边缘光的缘故吧...
这里还是像之前一样使用添加Blit Renderer Feature的方式,先把相关代码从Github弄下来,然后可以在UniversalRenderPipelineAsset_Renderer上点击Add Renderer Feature添加一个Blit Feature,再把材质拖进去就行了:
效果对比,用Sobel边缘光应该能让人物看上去更加精致?
不知道是不是好看了一些,但原神都有这样的效果那肯定是合理的。
传统N dot V的方法特定视角下会出现看上去不太正常的大片边缘光,Sobel就不会有这个问题。但由于是基于深度的屏幕后处理,Sobel会有物体重叠时边缘光消失的现象:
反正我觉得Sobel边缘光效果更好看一点点,大不了也可以两个一起用...
参考链接
Genshin Impact Character Shader Breakdown Unity URP
【JTRP】屏幕空间深度边缘光 Screen Space Depth Rimlight
如何在Unity URP中自定义ToneMapping
URP_BlitRenderFeature