unity-shader-深度图及其应用
需要 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));
深度图的使用时比较耗费批次的, 场景中的 不透明 ( opaque ) 物体需要渲染两遍, 第一遍 ( 也就是多出来的那一倍 ) 渲染是为了的得到深度纹理.
所以 深度图 不等于 深度缓存
深度纹理并非深度缓冲中的数据,而是通过特定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 调试)
需要注意的是 能量场 ( 透明渲染 ) _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 中可以看出
所以可以用 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
两者的共同点是 参数都是 用从 深度图 中采样出来的 深度值.
//vert
o.screenPos = ComputeScreenPos(clipPos); // ComputeScreenPos 的参数是剪裁空间下的位置
//frag
float depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos));
LinearEyeDepth
可以用在 物体a 需要与场景的 深度值 作比较, 进而判断出是否相交. 此时需要保证 物体a 不会渲染都深度图中, 不然不能做比较. 可以参考 [深度图 不等于 深度缓存](#深度图 不等于 深度缓存)
比较的两个值需要转换到 同一空间 下 (一般就是是 视空间) 才能进行比较, 具体代码可以参考 相交判断
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;
}
后处理直接绘制 深度图
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);
}
参考: 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 更快渲染
将透明物体的渲染设为 3000, 让它比 opaque 更后渲染, 就正常可以正常混合到 胶囊体, 应为颜色缓存区已经有了胶囊体的颜色了.
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