Unity Shader - 深度图基础及应用

文章内容

  1. 深度图基础
  2. 访问深度图
  3. 利用深度图重建世界坐标
  4. 深度图应用
    • 渲染深度图
    • 相交高亮
    • 能量场
    • 全局雾效
    • 扫描线
    • 水淹
    • 垂直雾效
    • 边缘检测
    • 运动模糊
    • 景深
  5. 参考资料

深度图基础

深度图里存放了[0,1]范围的非线性分布的深度值,这些深度值来自NDC坐标。
在延迟渲染中,深度值默认已经渲染到G-buffer;而在前向渲染中,你需要去申请,以便Unity在背后利用Shader Replacement将RenderType为Opaque、渲染队列小于等于2500并且有ShadowCaster Pass的物体的深度值渲染到深度图中。


访问深度图

第一步:在C#中设置Camera.main.depthTextureMode = DepthTextureMode.Depth;

可以在主摄像机的Camera组件下看见提示:

这表明了主摄像机渲染了深度图

第二步:在Shader中声明_CameraDepthTexture

sampler2D _CameraDepthTexture;

第三步:访问深度图

//1.如果是后处理,可以直接用uv访问
//vertex
//当有多个RenderTarget时,需要自己处理UV翻转问题
#if UNITY_UV_STARTS_AT_TOP //DirectX之类的
    if(_MainTex_TexelSize.y < 0) //开启了抗锯齿
        o.uv.y = 1 - o.uv.y; //满足上面两个条件时uv会翻转,因此需要转回来
#endif
//fragment
float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv));
 
//2.其他:利用投影纹理采样
//vertex
o.screenPos = ComputeScreenPos(o.vertex);
//fragment
float depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos));

float linear01Depth = Linear01Depth(depth); //转换成[0,1]内的线性变化深度值
float linearEyeDepth = LinearEyeDepth(depth); //转换到摄像机空间

重建世界坐标

利用覆盖屏幕的uv值和深度图中的深度,我们可以重建出物体在世界空间中的坐标。
主要有以下两种方法:

  1. 利用VP矩阵的逆矩阵对NDC坐标进行转换。
  2. 找到从摄像机指向该点的方向向量(需要是单位向量),将该方向向量乘上深度值就能得到摄像机指向该点的向量,将该向量加上摄像机位置就能得到该点的世界坐标。

1. 利用VP矩阵重建

首先要在C#脚本中传递当前的VP逆矩阵:

Matrix4x4 currentVP = VPMatrix;
Matrix4x4 currentInverseVP = VPMatrix.inverse;
mat.SetMatrix("_CurrentInverseVP", currentInverseVP);
Graphics.Blit(source, destination, mat);

然后在Shader中首先制造NDC坐标:

float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv.zw));
float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, depth * 2 - 1, 1); //NDC坐标

利用当前的VP逆矩阵将NDC坐标转换到世界空间:

float4 D = mul(_CurrentInverseVP, H);
float4 W = D / D.w; //将齐次坐标w分量变1得到世界坐标

具体用法可以到下面的MotionBlur例子中查看。

2. 利用方向向量重建

首先需要知道,Post Process实际上是渲染一个覆盖屏幕的Quad,因此屏幕四个角对应摄像机的视椎体四个角。
首先是算出摄像机到四个角的向量:

float halfHeight = near * tan(fov/2);
float halfWidth = halfHeight * aspect;
Vector3 toTop = up * halfHeight;
Vector3 toRight = right * halfRight;

Vector3 toTopLeft = forward + toTop - toRight;
Vector3 toBottomLeft = forward - toTop - toRight;
Vector3 toTopRight = forward + toTop + toRight;
Vector3 toBottomRight = forward - toTop + toRight;

假设有个绿点在toTopLeft所在线上,利用相似三角形,可以得到:

toGreen / depth = toTopLeft / near

而depth是能够在Shader中获得的,因此我们只需要传递toTopLeft / near到Shader中就能计算出toGreen:

toTopLeft /= cam.nearClipPlane;
toBottomLeft /= cam.nearClipPlane;
toTopRight /= cam.nearClipPlane;
toBottomRight /= cam.nearClipPlane;
            
Matrix4x4 frustumDir = Matrix4x4.identity;
frustumDir.SetRow(0, toBottomLeft);
frustumDir.SetRow(1, toBottomRight);
frustumDir.SetRow(2, toTopLeft);
frustumDir.SetRow(3, toTopRight);
mat.SetMatrix("_FrustumDir", frustumDir);

在Vertex中判断出对应顶点所在的向量:



可以看到uv值和对应的索引值正好是二进制的关系,所以可以如下求出:

int ix = (int)o.uv.z;
int iy = (int)o.uv.w;
o.frustumDir = _FrustumDir[ix + 2 * iy];

你可能奇怪这样只能求到4个角线上的点,但vertex到fragment的过程中是有个东西叫插值的,这个插值正好能把每个像素所在的向量求出。
然后我们就能在fragment中求出世界坐标了:

float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv.zw));
float linearEyeDepth = LinearEyeDepth(depth);
float3 worldPos = _WorldSpaceCameraPos + linearEyeDepth * i.frustumDir.xyz;

具体的用法可以到下面的垂直雾效例子中找到。


渲染深度图

输出[0,1]范围的深度值即可,如下:

fixed4 frag (v2f i) : SV_Target
{
    float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv));
    float linear01Depth = Linear01Depth(depth);
    return linear01Depth;
}

完整代码点这里

PrintDepthMap场景

PS: 如果代码没错,而看到的是全黑的,那么应该就是摄像机的Far Clip Plane设得太大。


相交高亮

思路是判断当前物体的深度值与深度图中对应的深度值是否在一定范围内,如果是则判定为相交。
首先访问当前物体的深度值:

//vertex
COMPUTE_EYEDEPTH(o.eyeZ);

然后访问深度图。由于此时不是Post Process,因此需要利用投影纹理采样来访问深度图:

//vertex
o.screenPos = ComputeScreenPos(o.vertex);
//fragment
float screenZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)));

最后就是进行相交判断:

float halfWidth = _IntersectionWidth / 2;
float diff = saturate(abs(i.eyeZ - screenZ) / halfWidth); //除以halfWidth来控制相交宽度为_IntersectionWidth

fixed4 finalColor = lerp(_IntersectionColor, col, diff);
return finalColor;

完整代码点这里

IntersectionHighlight场景

能量场

在相交高亮效果的基础上,加上半透明边缘高亮,就能制造出一个简单的能量场效果:

float3 worldNormal = normalize(i.worldNormal);
float3 worldViewDir = normalize(i.worldViewDir);
float rim = 1 - saturate(dot(worldNormal, worldViewDir)) * _RimPower;

float screenZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)));  
float intersect = (1 - (screenZ - i.eyeZ)) * _IntersectionPower;
float v = max (rim, intersect);

return _MainColor * v;

完整代码点这里

ForceField场景

全局雾效

思路是让雾的浓度随着深度值的增大而增大,然后进行的原图颜色和雾颜色的插值:

fixed4 frag (v2f i) : SV_Target
{
    fixed4 col = tex2D(_MainTex, i.uv.xy);
    float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv.zw));
    float linearDepth = Linear01Depth(depth);
    float fogDensity = saturate(linearDepth * _FogDensity);
    fixed4 finalColor = lerp(col, _FogColor, fogDensity);
    return finalColor;
}

完整代码点这里

Fog场景

扫描线

思路与相交高亮效果类似,只是这里是Post Process。自定义一个[0,1]变化的值_CurValue,根据_CurValue与深度值的差进行颜色的插值:

fixed4 frag (v2f i) : SV_Target
{
    fixed4 originColor = tex2D(_MainTex, i.uv.xy);
    float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv.zw));
    float linear01Depth = Linear01Depth(depth);
    float halfWidth = _LineWidth / 2;
    float v = saturate(abs(_CurValue - linear01Depth) / halfWidth); //线内返回(0, 1),线外返回1
    return lerp(_LineColor, originColor, v);
}

完整代码点这里

ScanLine场景

水淹

利用上面提到的第二种重建世界空间坐标的方法得到世界空间坐标,判断该坐标的Y值是否在给定阈值下,如果是则混合原图颜色和水的颜色:

fixed4 frag (v2f i) : SV_Target
{
    fixed4 col = tex2D(_MainTex, i.uv.xy);
    float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv.zw));
    float linearEyeDepth = LinearEyeDepth(depth);
    float3 worldPos = _WorldSpaceCameraPos.xyz + i.frustumDir * linearEyeDepth;
                
    if(worldPos.y < _WaterHeight)
        return lerp(col, _WaterColor, _WaterColor.a); //半透明

    return col;
}

完整代码点这里

WaterFlooded场景

垂直雾效

利用上面提到的第二种重建世界空间坐标的方法得到世界空间坐标,让雾的浓度随着Y值变化:

fixed4 frag (v2f i) : SV_Target
{
    fixed4 col = tex2D(_MainTex, i.uv.xy);
    float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv.zw));
    float linearEyeDepth = LinearEyeDepth(depth);
    float3 worldPos = _WorldSpaceCameraPos + linearEyeDepth * i.frustumDir.xyz;

    float fogDensity = (worldPos.y - _StartY) / (_EndY - _StartY);
    fogDensity = saturate(fogDensity * _FogDensity);
                
    fixed3 finalColor = lerp(_FogColor, col, fogDensity).xyz;
    return fixed4(finalColor, 1.0);
}

完整代码点这里

VerticalFog场景

边缘检测

思路是取当前像素的附近4个角,分别计算出两个对角的深度值差异,将这两个差异值相乘就得到我们判断边缘的值。
首先是得到4个角:

//vertex
//Robers算子
o.uv[1] = uv + _MainTex_TexelSize.xy * float2(-1, -1);
o.uv[2] = uv + _MainTex_TexelSize.xy * float2(-1, 1);
o.uv[3] = uv + _MainTex_TexelSize.xy * float2(1, -1);
o.uv[4] = uv + _MainTex_TexelSize.xy * float2(1, 1);

然后是得到这4个角的深度值:

float sample1 = Linear01Depth(UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv[1])));
float sample2 = Linear01Depth(UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv[2])));
float sample3 = Linear01Depth(UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv[3])));
float sample4 = Linear01Depth(UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv[4])));

最后就是根据对角差异来得到判断边缘的值:

float edge = 1.0;
//对角线的差异相乘
edge *= abs(sample1 - sample4) < _EdgeThreshold ? 1.0 : 0.0;
edge *= abs(sample2 - sample3) < _EdgeThreshold ? 1.0 : 0.0;

return edge;
// return lerp(0, col, edge); //描边

完整代码点这里

EdgeDetection场景

PS:上面这种只用深度值来检测边缘的效果并不太好,最好结合法线图来判断,原理都是一样的。


运动模糊 (Motion Blur)

MotionBlur场景

运动模糊主要用在竞速类游戏中用来体现出速度感。这里介绍的运动模糊只能用于周围物体不动,摄像机动的情景。
思路是利用上面提到的重建世界坐标方法得到世界坐标,由于该世界坐标在摄像机运动过程中都是不动的,因此可以将该世界空间坐标分别转到摄像机运动前和运动后的坐标系中,从而得到两个NDC坐标,利用这两个NDC坐标就能得到该像素运动的轨迹,在该轨迹上多次取样进行模糊即可。
首先是得到世界坐标(这里使用提到的第一种重建方法):

float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv.zw));
float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, depth * 2 - 1, 1); //NDC坐标
float4 D = mul(_CurrentInverseVP, H);
float4 W = D / D.w; //将齐次坐标w分量变1得到世界坐标

然后是计算出运算前后的NDC坐标:

float4 currentPos = H;
float4 lastPos = mul(_LastVP, W);
lastPos /= lastPos.w;

最后就是在轨迹上多次取样进行模糊:

//采样两点所在直线上的点,进行模糊
fixed4 col = tex2D(_MainTex, i.uv.xy);
float2 velocity = (currentPos - lastPos) / 2.0;
float2 uv = i.uv;
uv += velocity;
int numSamples = 3;
for(int index = 1; index < numSamples; index++, uv += velocity)
{
    col += tex2D(_MainTex, uv);
}
col /= numSamples;

完整代码点这里


景深 (Depth Of Field)

DepthOfField场景

景深是一种聚焦处清晰,其他地方模糊的效果,在摄影中很常见。
思路是首先渲染一张模糊的图,然后在深度图中找到聚焦点对应的深度,该深度附近用原图,其他地方渐变至模糊图。
第一步是使用SimpleBlur Shader渲染模糊的图,这里我只是简单地采样当前像素附近的9个点然后平均,你可以选择更好的模糊方式:

v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);

    o.uv[0] = v.uv + _MainTex_TexelSize.xy * float2(-1, -1) * _BlurLevel;
    o.uv[1] = v.uv + _MainTex_TexelSize.xy * float2(-1, 0) * _BlurLevel;
    o.uv[2] = v.uv + _MainTex_TexelSize.xy * float2(-1, 1) * _BlurLevel;
    o.uv[3] = v.uv + _MainTex_TexelSize.xy * float2(0, -1) * _BlurLevel;
    o.uv[4] = v.uv + _MainTex_TexelSize.xy * float2(0, 0) * _BlurLevel;
    o.uv[5] = v.uv + _MainTex_TexelSize.xy * float2(0, 1) * _BlurLevel;
    o.uv[6] = v.uv + _MainTex_TexelSize.xy * float2(1, -1) * _BlurLevel;
    o.uv[7] = v.uv + _MainTex_TexelSize.xy * float2(1, 0) * _BlurLevel;
    o.uv[8] = v.uv + _MainTex_TexelSize.xy * float2(1, 1) * _BlurLevel;

    return o;
}
            
fixed4 frag (v2f i) : SV_Target
{
    fixed4 col = tex2D(_MainTex, i.uv[0]);
    col += tex2D(_MainTex, i.uv[1]);
    col += tex2D(_MainTex, i.uv[2]);
    col += tex2D(_MainTex, i.uv[3]);
    col += tex2D(_MainTex, i.uv[4]);
    col += tex2D(_MainTex, i.uv[5]);
    col += tex2D(_MainTex, i.uv[6]);
    col += tex2D(_MainTex, i.uv[7]);
    col += tex2D(_MainTex, i.uv[8]);
    col /= 9;
    return col;
}

第二步就是传递该模糊的图给DepthOfField Shader:

RenderTexture blurTex = RenderTexture.GetTemporary(source.width, source.height, 16);
Graphics.Blit(source, blurTex, blurMat);
dofMat.SetTexture("_BlurTex", blurTex);
Graphics.Blit(source, destination, dofMat);

第三步就是在DepthOfField Shader中根据焦点来混合原图颜色和模糊图颜色:

fixed4 col = tex2D(_MainTex, i.uv.xy);
fixed4 blurCol = tex2D(_BlurTex, i.uv.zw);
float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv.zw));
float linearDepth = Linear01Depth(depth);
float v = saturate(abs(linearDepth - _FocusDistance) * _FocusLevel);
return lerp(col, blurCol, v); 

完整代码点这里


完整项目地址

https://github.com/KaimaChen/Unity-Shader-Demo/tree/master/UnityShaderProject


参考

Unity Docs - Camera’s Depth Texture
Unity Docs - Platform-specific rendering differences
神奇的深度图:复杂的效果,不复杂的原理
SPECIAL EFFECTS WITH DEPTH
GPU Gems - Chapter 27. Motion Blur as a Post-Processing Effect
《Unity Shader 入门精要》
《Unity 3D ShaderLab 开发实战详解》

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

推荐阅读更多精彩内容