unity-shader-深度图及其应用


title: unity-shader-深度图及其应用
categories: Unity3d-Shader
tags: [unity, shader, 深度图, depth]
date: 2019-05-13 20:44:51
comments: false

unity-shader-深度图及其应用


前篇

  • Unity Shader - 深度图基础及应用 (比较详细的记录了深度图) - https://www.jianshu.com/p/80a932d1f11e
  • 深度值精度 - https://learnopengl-cn.readthedocs.io/zh/latest/04 Advanced OpenGL/01 Depth testing/#_3
  • 深度冲突 - https://learnopengl-cn.readthedocs.io/zh/latest/04 Advanced OpenGL/01 Depth testing/#_5
  • Unity Shader 基础(3) 获取深度纹理 - https://www.cnblogs.com/zsb517/p/6655546.html

开启渲染 深度图

需要 Camera 组件的 depthTextureMode 中的深度位设置为1, 也就是用 运算 DepthTextureMode.Depth

using UnityEngine;

public class DepthTextureTest : MonoBehaviour {
    void OnEnable() {
        GetComponent<Camera>().depthTextureMode |= DepthTextureMode.Depth;
    }

    void OnDisable() {
        GetComponent<Camera>().depthTextureMode &= ~DepthTextureMode.Depth;
    }
}

然后在 shader 中定义 uniform 变量 , 类型时 sampler2D, 命名必须是 _CameraDepthTexture

sampler2D _CameraDepthTexture;

// 采样深度图, 用模型的 屏幕坐标([0, 1]区间) 去 采样 场景深度图 获取场景 深度值 (非线性的 [0, 1] 区间)
float depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos));

深度图

  • Unity Shader - 深度图基础及应用 (介绍的挺详细的) - https://www.jianshu.com/p/80a932d1f11e

深度图的使用时比较耗费批次的, 场景中的 不透明 ( opaque ) 物体需要渲染两遍, 第一遍 ( 也就是多出来的那一倍 ) 渲染是为了的得到深度纹理.

所以 深度图 不等于 深度缓存

  • 深度图是提前把 不透明 ( opaque ) 物体全都渲染了一遍从而得到的
  • 深度缓存 是在渲染每一个物体是根据给定的 深度比较条件 从而写进去的值.
  • 注意: 通过增加 深度写入pass 的方式, 也不能将该物体写入到 深度图 中, 因为pass只在正常渲染下起作用.
    参考: [透明 ( transparent ) 物体写入深度的姿势](#透明 ( transparent ) 物体写入深度的姿势)

unity-shader-深度图及其应用_第1张图片

深度纹理并非深度缓冲中的数据,而是通过特定Pass获得

深度图 不等于 深度缓存

Unity4.X和Unity5.X版本的实现方式不太一样,Unity4.X通过"RenderType" 标签通过Camera Shader 替换获取,Unity5通过ShadowCaster Pass获取,Unity5官方文档描述:

Depth texture is rendered using the same shader passes as used for shadow caster rendering (ShadowCaster pass type). So by extension, if a shader does not support shadow casting (i.e. there’s no shadow caster pass in the shader or any of the fallbacks), then objects using that shader will not show up in the depth texture.
Make your shader fallback to some other shader that has a shadow casting pass, or If you’re using surface shaders, adding an addshadow directive will make them generate a shadow pass too.
Note that only “opaque” objects (that which have their materials and shaders setup to use render queue <= 2500) are rendered into the depth texture.

对于自身带有 ShadowCaster Pass 或者 FallBack 中含有,并且 Render Queue 小于等于 2500 的渲染对象才会出现在深度纹理中,详细的测试可以参考:【Unity Shader】Shadow Caster、RenderType和_CameraDepthTexture

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

实际测试

即使关了 深度写入 ZWrite Off , 深度图 还是有这个对象的深度信息. (使用 framedebugger 调试)

unity-shader-深度图及其应用_第2张图片


注意

需要注意的是 能量场 ( 透明渲染 ) _CameraDepthTexture 中只保存了场景中不透明物体的深度信息,因此这个时候无法从CameraDepthTexture 中获取 能量场 的深度信息,所以要在 vert 中计算顶点的深度,这里我利用了 COMPUTE_EYEDEPTH 这个内置的宏。在之后的 frag 内就可以很方便的获取场景和能量场当前片元的深度了。
参考: http://www.php361.com/index.php?c=index&a=view&id=5257

//vert
o.screenPos = ComputeScreenPos(o.vertex);
COMPUTE_EYEDEPTH(o.screenPos.z);

//frag
float sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)));
float partZ = i.screenPos.z;

相交判断

以上面 能量场 (ForceField) 为例, ForceField 丢到透明度队列渲染, 则该物体不会再就不会写入到深度图中, 从 Frame Debug 中可以看出

unity-shader-深度图及其应用_第3张图片

所以可以用 vert 计算出 深度值, 与 深度图 中的值 ( 也就是场景中 不透明 物体的z值 ) 相比较, 小于某个阈值时可以定义为相交.

//vert
o.screenPos = ComputeScreenPos(clipPos); // ComputeScreenPos 的参数是剪裁空间下的位置
COMPUTE_EYEDEPTH(o.screenPos.z); // 计算出 顶点位置 视空间 的 z 值, 保存到 o.screenPos.z 

//frag
float depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)); // 用模型的 屏幕坐标([0, 1]区间) 去 采样 场景深度图 获取场景 深度值
float sceneZ = LinearEyeDepth(depth); // 把 深度值 转换到 视空间
float partZ = i.screenPos.z;

// 两者相减就是深度的差异diff,再用1 - diff就得到了一个“相交程度”。
float diff = sceneZ - partZ; // 两个值都在同一空间下, 就可以做比较了
float intersect = (1 - diff) * _IntersectPower;

相关宏

#	define COMPUTE_EYEDEPTH(o) o = -UnityObjectToViewPos( v.vertex ).z # 计算出 视空间 的 z 值
#   define SAMPLE_DEPTH_TEXTURE_PROJ(sampler, uv) (tex2Dproj(sampler, uv).r)
#	define UNITY_PROJ_COORD(a) a

LinearEyeDepth 与 Linear01Depth 的使用场景

两者的共同点是 参数都是 用从 深度图 中采样出来的 深度值.

//vert
o.screenPos = ComputeScreenPos(clipPos); // ComputeScreenPos 的参数是剪裁空间下的位置

//frag
float depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos));
  • LinearEyeDepth

    可以用在 物体a 需要与场景的 深度值 作比较, 进而判断出是否相交. 此时需要保证 物体a 不会渲染都深度图中, 不然不能做比较. 可以参考 [深度图 不等于 深度缓存](#深度图 不等于 深度缓存)

    比较的两个值需要转换到 同一空间 下 (一般就是是 视空间) 才能进行比较, 具体代码可以参考 相交判断

    unity-shader-深度图及其应用_第4张图片

  • Linear01Depth

    可以用在 控制的值 (材质球暴露出来设置) 与 深度值 作比较, 所以要把 深度值 约束在 [0, 1] 区间内就得用到这个函数. 控制的值 就可以在 [0,1] 区间与 深度比较了.

    例如扫描线效果

    float4 frag_depth(v2f_img i) : SV_Target {
        float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
        float lnr01Depth = Linear01Depth(depth);
        fixed4 screenTexture = tex2D(_MainTex, i.uv);
    
        float near = smoothstep(_ScanValue, lnr01Depth, _ScanValue - _ScanLineWidth); // _ScanValue 就是 控制值, 在 [0, 1] 区间
        float far = smoothstep(_ScanValue, lnr01Depth, _ScanValue + _ScanLineWidth);
        fixed4 emissionClr = _ScanLineColor * (near + far);
    
        return screenTexture + emissionClr;
    }
    

    unity-shader-深度图及其应用_第5张图片

    后处理直接绘制 深度图

    float4 frag_depth(v2f_img i) : SV_Target {
        float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
        float lnr01Depth = Linear01Depth(depth);
    
        return fixed4(lnr01Depth, lnr01Depth, lnr01Depth, 1);
    }
    

    unity-shader-深度图及其应用_第6张图片


透明 ( transparent ) 物体写入深度的姿势

参考: Unity3D - Shader - 开启深度写入的半透明效果 - https://blog.csdn.net/biezhihua/article/details/78690574

正常来说 透明物体 是不会写入深度的, 但是可以通过增加多一个 pass 只用来写入深度, 但不输出颜色.

注意: 即使通过增加 深度写入pass 的方式, 也不能将该物体写入到 深度图 中, 因为pass只在正常渲染下起作用.

Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}

// 仅仅是把模型的深度信息写入深度缓冲中
// 从而剔除模型中被自身遮挡的片元
Pass {
    // 开启深度写入
    ZWrite On
    // 用于设置颜色通道的写掩码(Wirte Mask)
    // ColorMask RGB|A|0|(R/G/B/A组合)
    // 当为0时意味着该Pass不写入任何颜色通道,也就不会输出任何颜色
    ColorMask 0
}

实测一下, 将透明物体的渲染设为 1000, 让它比 opaque 更快渲染

  • 有深度写入pass的结果. 可以正常遮挡后面渲染的 胶囊体 , 同时和天空盒混合了

    unity-shader-深度图及其应用_第7张图片

  • 没有深度写入pass的结果, 不能遮挡 胶囊体

    unity-shader-深度图及其应用_第8张图片

将透明物体的渲染设为 3000, 让它比 opaque 更后渲染, 就正常可以正常混合到 胶囊体, 应为颜色缓存区已经有了胶囊体的颜色了.

unity-shader-深度图及其应用_第9张图片

踩坑

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

深度值 推到 世界坐标位置

  • Unity3D 片元NDC空间z值(ZBuffer)转View空间z值,公式推导 - https://blog.csdn.net/u012149999/article/details/78678901

  • 全面认识Depth - 这里有关于Depth的一切 - https://zhuanlan.zhihu.com/p/25095708

  • shader 推导流程, 可以参照 运动模糊

    //使用宏和纹理坐标对深度纹理进行采样,得到深度值
    float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
    
    //构建当前像素的NDC坐标,xy坐标由像素的纹理坐标映射而来,z坐标由深度值d映射而来
    float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1);
    //使用 当前帧的视角 * 投影矩阵 的逆矩阵 对H进行变换
    float4 D = mul(_CurrentViewProjectionInverseMartix, H);
    //把结果除以它的w分量,得到该像素世界空间下的坐标
    float4 worldPos = D / D.w;
    

为什么要除以 分量w ? 参考说明 剪裁坐标

因为经过了 投影矩阵, 分量w 就不在为1, 所以要求得具体的世界坐标, 就需要 除以 分量w 的影响


深度偏移

Offset 修改深度偏移值

	Pass
		{
			Name "FORWARD" 
			Tags { "LightMode" = "ForwardBase" }

			ZWrite On
			Offset 3000,0

你可能感兴趣的:(Unity3d,Unity3D,Unity3D-Shader)