文章目录
深度(Depth),在各种效果中都会用到,学会在Unity中如何获取,也是很有用的。
后续很多一些其他的效果都会与深度有关。
Unity官方在SIGGRAPH2011有分享过一些使用深度制作的特效,文档:SIGGRAPH2011 Special Effect with Depth.pdf
如果多年后,下载不了,链接无效了,可以点击这里(Passworld:cmte)下载(我收藏到网盘了)
由于篇幅原因,本文只介绍:
- 如何获取纹理;
- 如何读取数据;
- 理解数据的来源。
获取深度的方法
下面再简单介绍一次吧:
- 在Unity中获取,可以通过脚本层(CSharp层)设置Camera.depthTextureMode属性;然后在Shader定义对应变量获取。之前写过一篇,关于后处理获取深度描边的文章。此方法要留意设置好Shader tag的Queue=Geometry(<=2500),和LightMode=ShadowCaster。
- 通过Camera.RenderWithShader替换Shader,渲染到RT,在传入Shader的方式。之前写过一篇,关于Camera.RenderWithShader等相关用法。此方法,通常只要有RenderType=Opaque就好了。
设置Camera.depthTextureMode方式获取
在CSharp脚本层,设置Camera.depthTextureMode = DepthTextureMode.Depth;
或是Camera.depthTextureMode = DepthTextureMode.DepthNormals;
,然后在Shader层声明_CameraDepthTexture
或是_CameraDepthNormalsTexture
;来获取,如下伪代码:
CSharp脚本
public class ScriptName : MonoBehaviour {
private Camera cam;
private void Start() {
cam = gameObject.GetComponent<Camera>();
cam.depthTextureMode |= DepthTextureMode.Depth;
cam.depthTextureMode |= DepthTextureMode.DepthNormals;
}
...
}
Shader
...
sampler2D _CameraDepthTexture; // 如果Camera.depthTextureMode = DepthTextureMode.Dept的Flag就可以获取这个uniform
sampler2D _CameraDepthNormalsTexture; // 如果Camera.depthTextureMode = DepthTextureMode.DepthNormals的Flag就可以获取该uniform
...
Camera.RenderWithShader方式获取
现在脚本层对Camera.RenderWithShader到RT上,再传入Shader,如下伪代码:
参考这篇:定制自己的 Depth Texture
用于绘制深度的Shader
// vertex:
## o.depth = o.pos.z; // 这是错误的
o.depth = -mul(UNITY_MATRIX_MV, i.vertex).z; // 这是正确的
// fragment:
// 这是错误的
{
## float n = _ProjectionParams.y;
## float f = _ProjectionParams.z;
## float c = (i.depth - n) / f;
## return c;
}
// 这是正确的
{
float f = _ProjectionParams.z;
float c = i.depth / f;
}
或是这篇:Unity Shader 基础(3) 获取深度纹理
v2f vert( appdata_base v )
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.nz.xyz = COMPUTE_VIEW_NORMAL;
o.nz.w = COMPUTE_DEPTH_01;
return o;
}
fixed4 _Color;
fixed4 frag(v2f i) : SV_Target
{
//clip(_Color.a-0.01);
return EncodeDepthNormal (i.nz.w, i.nz.xyz);
}
调用Camera.RenderWithShader的CSharp
public class ScriptName : MonoBehaviour {
...
private Camera cam;
private int depthHash;
private RenderTexture customDepthRT;
private void Start() {
depthHash = Shader.PropertyToID("_CustomDepthTexture");
cam = gameObject.GetComponent<Camera>();
customDepthRT = RenderTexture.GetTemporary(Screen.width, Screen.height);
}
private void OnPreRender() {
var srcTT = cam.targetTexture;
var srcClearFlags = cam.clearFlags;
var srcBgColor = cam.backgroundColor;
// 测试RenderWithShader
// RenderWithShader是调用时就会去执行替换,并渲染相机的内容
cam.backgroundColor = Color.black;
cam.clearFlags = CameraClearFlags.Color;
cam.targetTexture = replaceColorRT; // 渲染目标到rt
// 立刻渲染Shader中的pass有"RenderType"的tag的对象
// 所以你的替换使用的Shader中的pass的tag要设置好友"RenderType"="Opaque"就好了
// 这里最好用"RenderType"="Opaque",因为内置的很多shader,也都是对不透明对象用这个tag标记了的
cam.RenderWithShader(replaceShader, "RenderType");
cam.targetTexture = srcTT; // 恢复渲染目标,等设置
cam.clearFlags = srcClearFlags;
cam.backgroundColor = srcBgColor;
// 设置全局的_CustomDepthTexture纹理
Shader.SetGlobalTexture(depthHash , replaceColorRT);
}
...
}
使用_CustomDepthTexture的Shader
...
sampler2D _CustomDepthTexture; // 声明一下对应的全局纹理就好了,然后在后续着色器中都可以使用到了
...
使用Unity提供的深度纹理时,注意下面的问题:
- 直接使用_CameraDepthTexture纹理R通道值显示时,几乎黑色或是白色的问题,一般都是因为Camera的near,far差值太大导致,因为该_CameraDepthTexture R通道存储的不是线性,它存储的是NDC下的z值。
- _CameraDepthNormalTexture解码BA通道出来的深度值不会有_CameraDepthTexture的问题,因为在编码时,就处理了线性。
- 深度纹理只绘制shader中的tag的"Queue"="Geometry"队列之前的所有物体,Geometry=2500,就是说<=2500的,都会绘制到深度纹理中。并且shader中要有某个pass的tag有LightMode=ShadowCaster的。关于LightMode的pass tag可以查看官方手册或是API的说明。
实践
简写说明
什么是ndc,ndc.z
什么是ndc,它是NDC(Normalized Device Coordinates)的简写。
ndc.z就是ndc坐标空间下的z轴的值。
ndc是在GPU内置的几何变换管线中的一部分处理,有兴趣可以参考之前写的一篇用英文总结(因为这些描述用英文表达比较准确):OpenGL Transformation 几何变换的顺序概要(MVP,NDC,Window坐标变换过程)
什么是eyeZ
eyeZ中的eye代表:View/Eye/Camera Space。
所以eyeZ可以理解为:视图空间下z轴值。
使用unity内置_CameraDepthTexture纹理
注意:在unity的_CameraDepthTexture中R通道存储的是非线性的ndc下的z值,这点与后面使用的_CameraDepthNormalsTexture的不同。参考:Unity3D中的深度纹理和法线纹理。
但是unity在正交相机下,_CameraDepthTexture的是一个EyeZ/Far的线性值(后面有将)==Linear01Depth。
Shader
先准备好要获取,并显示深度的shader
// jave.lin 2020.03.09
Shader "Custom/GetDepthTexture" {
SubShader {
Cull Off ZWrite Off ZTest Always
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _CameraDepthTexture;
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target {
return Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv));
}
ENDCG
}
}
}
Material
材质:GetDepthTextureMat,设置好使用上面的shader
CSharp
脚本挂载到Camera上,设置好Mat材质:GetDepthTextureMat
// jave.lin 2020.03.09
using UnityEngine;
public class GetDepthTextureScript1 : MonoBehaviour
{
public Material mat;
private Camera cam;
private void Start()
{
cam = gameObject.GetComponent<Camera>();
cam.depthTextureMode |= DepthTextureMode.Depth;
cam.depthTextureMode |= DepthTextureMode.DepthNormals;
}
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
Graphics.Blit(null, destination, mat);
}
}
运行效果
使用_CameraDepthNormalsTexture纹理
注意:在unity的_CameraDepthNormalsTexture中BA(ZW)通道存储的是Linear01Depth=线性z值/far的比例值,这点与后面使用的_CameraDepthTexture的不同。Unity3D中的深度纹理和法线纹理
就只调整shader就好了。
// jave.lin 2020.03.09
Shader "Custom/GetDepthTexture" {
SubShader {
Cull Off ZWrite Off ZTest Always
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
//sampler2D _CameraDepthTexture;
sampler2D _CameraDepthNormalsTexture;
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target {
//return Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv));
float4 depthNormals = SAMPLE_RAW_DEPTH_TEXTURE(_CameraDepthNormalsTexture, i.uv);
return DecodeFloatRG (depthNormals.zw);
}
ENDCG
}
}
}
可以看到frag函数中,我们该用了_CameraDepthNormalsTexture的方式了。
自定义的深度图
深度纹理像素编码的Shader
在VS阶段使用COMPUTE_VIEW_NORMAL宏得到视图空间下的法线,COMPUTE_DEPTH_01宏得到视图空间下的深度。
COMPUTE_VIEW_NORMAL宏内容如下:
#define COMPUTE_VIEW_NORMAL normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal))
COMPUTE_DEPTH_01宏内容如下:
#define COMPUTE_DEPTH_01 -(UnityObjectToViewPos( v.vertex ).z * _ProjectionParams.w)
然后在FS阶段EncodeDepthNormal(depth, normal),将法线编码到像素的xy(rg)通道,将深度编码到像素的zw(ba)通道。
EncodeDepthNormal宏内容如下:
// Encoding/decoding [0..1) floats into 8 bit/channel RG. Note that 1.0 will not be encoded properly.
inline float2 EncodeFloatRG( float v )
{
float2 kEncodeMul = float2(1.0, 255.0);
float kEncodeBit = 1.0/255.0;
float2 enc = kEncodeMul * v;
enc = frac (enc);
enc.x -= enc.y * kEncodeBit;
return enc;
}
// Encoding/decoding view space normals into 2D 0..1 vector
inline float2 EncodeViewNormalStereo( float3 n )
{
float kScale = 1.7777;
float2 enc;
enc = n.xy / (n.z+1);
enc /= kScale;
enc = enc*0.5+0.5;
return enc;
}
inline float4 EncodeDepthNormal( float depth, float3 normal )
{
float4 enc;
enc.xy = EncodeViewNormalStereo (normal);
enc.zw = EncodeFloatRG (depth);
return enc;
}
然后是我们的shader
// jave.lin 2020.03.09
Shader "Custom/CustomDepthTexture" {
SubShader {
Tags { "RenderType"="Opaque" }
LOD 100
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 vertex : SV_POSITION;
float4 depthNormals : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.depthNormals.xyz = COMPUTE_VIEW_NORMAL;
o.depthNormals.w = COMPUTE_DEPTH_01;
return o;
}
fixed4 frag (v2f i) : SV_Target {
return EncodeDepthNormal(i.depthNormals.w, i.depthNormals.xyz);
}
ENDCG
}
}
}
Unity 官方提供的:获取对象的深度信息的例子
下面是例子:
Shader "Render Depth" {
SubShader {
Tags { "RenderType"="Opaque" }
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
float4 pos : SV_POSITION;
float2 depth : TEXCOORD0;
};
v2f vert (appdata_base v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
UNITY_TRANSFER_DEPTH(o.depth);
return o;
}
half4 frag(v2f i) : SV_Target {
UNITY_OUTPUT_DEPTH(i.depth);
}
ENDCG
}
}
}
CSharp
调用深度纹理像素编码的Shader
然后设置到全局的uniform纹理_CustomDepthNormalsTexture;
// jave.lin 2020.03.09
using UnityEngine;
public class GetDepthTextureScript2 : MonoBehaviour
{
public Material mat; // 将_CustomDepthNormalsTexture解码并显示用的shader材质
public Shader shader; // 将深度,与法线编码到_CustomDepthNormalsTexture上的shader
private GameObject camGO; // 临时用的Camera,用于每帧绘制前先将_CustomDepthNormalsTexture绘制到RT
private Camera cam; // 原来主Camera
[SerializeField] private RenderTexture rt; // 打上[SerializeField]的Attribute方便在Inspector上双击查看RT的内容
private int depthHash; // 获取属性字符的HASH
private void Start()
{
camGO = new GameObject("DrawCustomDepthNormalsTextureCam");
camGO.SetActive(false);
cam = camGO.AddComponent<Camera>();
cam.CopyFrom(gameObject.GetComponent<Camera>());
depthHash = Shader.PropertyToID("_CustomDepthNormalsTexture");
}
private void OnPreRender()
{
if (rt == null || rt.width != Screen.width || rt.height != Screen.height)
{
if (rt != null) RenderTexture.ReleaseTemporary(rt);
rt = RenderTexture.GetTemporary(Screen.width, Screen.height, 24);
}
// 临时Camera不需要备份
备份原始属性
//var srcTT = cam.targetTexture;
//var srcClearFlags = cam.clearFlags;
//var srcBgColor = cam.backgroundColor;
// 测试RenderWithShader
// RenderWithShader是调用时就会去执行替换,并渲染相机的内容
cam.backgroundColor = new Color(0.5f, 0.5f, 1f); // 默认的法线值
cam.clearFlags = CameraClearFlags.Color;
// 渲染目标到rt
cam.targetTexture = rt;
// 立刻渲染Shader中的pass有"RenderType"的tag的对象
// 所以你的替换使用的Shader中的pass的tag要设置好友"RenderType"="Opaque"就好了
// 这里最好用"RenderType"="Opaque",因为内置的很多shader,也都是对不透明对象用这个tag标记了的
// 所以自定义的替换绘制,不需要"LightMode"="ShadowCaster"的tag也可以绘制到我们想要的目标纹理
cam.RenderWithShader(shader, "RenderType");
// 临时Camera不需要恢复
恢复渲染目标,等设置
//cam.targetTexture = srcTT;
//cam.clearFlags = srcClearFlags;
//cam.backgroundColor = srcBgColor;
// 设置全局的_CustomDepthTexture纹理
Shader.SetGlobalTexture(depthHash, rt);
}
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
// 显示到destination
Graphics.Blit(null, destination, mat);
}
private void OnDestroy()
{
if (rt != null) RenderTexture.ReleaseTemporary(rt);
Destroy(cam);
}
}
后处理显示Shader
读取_CustomDepthNormalsTexture,解码_CustomDepthNormalsTexture.zw为depth,并显示。
如果我们的自定义的纹理不是深度+法线的编码,只是简单的将ndcZ值写入,那么我们解码就不如要任何处理,直接读取 tex2D(_CustomDepthTexture, screenPos.xy).r
即可,即可得到ndcZ值。
如果要解码的是深度与法线,还可以使用另一个函数:(下面这例子是深度与法线)
inline float3 DecodeViewNormalStereo( float4 enc4 )
{
float kScale = 1.7777;
float3 nn = enc4.xyz*float3(2*kScale,2*kScale,0) + float3(-kScale,-kScale,1);
float g = 2.0 / dot(nn.xyz,nn.xyz);
float3 n;
n.xy = g*nn.xy;
n.z = g-1;
return n;
}
inline void DecodeDepthNormal( float4 enc, out float depth, out float3 normal )
{
depth = DecodeFloatRG (enc.zw);
normal = DecodeViewNormalStereo (enc);
}
下面是我们用到解码的shader
// jave.lin 2020.03.09
Shader "Custom/GetCustomDepthTexture" {
SubShader {
Cull Off ZWrite Off ZTest Always
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _CustomDepthNormalsTexture;
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target {
//return tex2D(_CustomDepthNormalsTexture, i.uv);
float4 depthNormals = SAMPLE_RAW_DEPTH_TEXTURE(_CustomDepthNormalsTexture, i.uv);
return DecodeFloatRG (depthNormals.zw);
}
ENDCG
}
}
}
运行效果
先不解码的内容显示:
fixed4 frag (v2f i) : SV_Target {
return tex2D(_CustomDepthNormalsTexture, i.uv);//先显示未解码的内容
//float4 depthNormals = SAMPLE_RAW_DEPTH_TEXTURE(_CustomDepthNormalsTexture, i.uv);
//return DecodeFloatRG (depthNormals.zw);
}
解码后的显示
fixed4 frag (v2f i) : SV_Target {
//return tex2D(_CustomDepthNormalsTexture, i.uv); //先显示未解码的内容
float4 depthNormals = SAMPLE_RAW_DEPTH_TEXTURE(_CustomDepthNormalsTexture, i.uv);
return DecodeFloatRG (depthNormals.zw);
}
注意:以上是自定义获取与内置的_CameraDepthNormalsTexture数据一样的方式。这个深度+法线编码在一起的纹理。前面说过,里面的depth是经过线性处理的。
如果你要自定义编码深度缓存保存的是ndc中z值的,那就是类似_CameraDepthTexture中的R通道值,但没有线性处理:tex2D(_CameraDepthTexture, screenPos.xy).r == ndcZ
你可以在编码的shader的vert函数拿到clipPos=UnityObjectToClipPos(v.vertex);
,然后frag函数取ndc.z == clip.z / clip.w
...
struct v2f {
float4 vertex : SV_POSITION;
float depth : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target {
return i.vertex.z / i.vertex.w; // ndc.z == clip.z / clip.w
}
...
这样渲染出来的深度纹理,就是非线性的。按需求来使用对应的纹理就好了。
LinearEyeDepth, Linear01Depth
LinearEyeDepth, Linear01Depht得官方文档也有介绍:Using Depth Textures
为了方便理解,亲手用Windows的Paint.exe话了个图,难画。。。(手动哭笑+无语 -_-!)
LinearEyeDepth 作用
上图中,要注意说明,LinearEyeDepth得出来的值,我们通常叫:EyeZ,它的意思是,视图空间(注意不是透视空间)下的z轴的距离值
作用是:将ndc.z值转为view space(视图空间)z,就是上图的EyeZ(距离A点的距离,如,far=1000,near=0.3(上图可能是550),透视空间中的G点的线性z轴值,对应的,就是在视图空间中的F点的z轴值,就是F距离A可能是800,就是说EyeZ=800)。
Linear01Depth 作用
作用是:将ndc.z值转为一个线性比例值(view space(视图空间)z轴值除以far的比例值),就是上图的Linear01Depth = AG/AE,图中AG假设为800,AE(far)=1000,那么Linear01Depth = AG/AE=800/1000=0.8。
下面是两个函数的源码:
// Z buffer to linear 0..1 depth
inline float Linear01Depth( float z )
{
return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
}
// Z buffer to linear depth
inline float LinearEyeDepth( float z )
{
return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
}
...
// Values used to linearize the Z buffer (http://www.humus.name/temp/Linearize%20depth.txt)
// x = 1-far/near
// y = far/near
// z = x/far
// w = y/far
// or in case of a reversed depth buffer (UNITY_REVERSED_Z is 1)
// x = -1+far/near
// y = 1
// z = x/far
// w = 1/far
float4 _ZBufferParams;
Unity中的这两个inline函数都是参考这个总结的:Linearize depth.txt
我就将它的内容copy出来,方便阅读吧:
Some math I did ... (not 100% verified, but looks like it works)
n = near
f = far
z = depth buffer Z-value
EZ = eye Z value
LZ = depth buffer Z-value remapped to a linear [0..1] range (near plane to far plane)
LZ2 = depth buffer Z-value remapped to a linear [0..1] range (eye to far plane)
DX:
EZ = (n * f) / (f - z * (f - n))
LZ = (eyeZ - n) / (f - n) = z / (f - z * (f - n))
LZ2 = eyeZ / f = n / (f - z * (f - n))
GL:
EZ = (2 * n * f) / (f + n - z * (f - n))
LZ = (eyeZ - n) / (f - n) = n * (z + 1.0) / (f + n - z * (f - n))
LZ2 = eyeZ / f = (2 * n) / (f + n - z * (f - n))
LZ2 in two instructions:
LZ2 = 1.0 / (c0 * z + c1)
DX:
c1 = f / n
c0 = 1.0 - c1
GL:
c0 = (1 - f / n) / 2
c1 = (1 + f / n) / 2
-------------------
http://www.humus.ca
快速的获取当前片段的Linear01Depth
来看一段代码:(该源代码在此,是国外一名在暴雪公司工作的女性图形程序员,有兴趣可以到她首页看看一些教程。)
还记得我之前说过的吗?Linear01Depth
其实就是EyeZ/Far
的一个比例值。
代码中vert shader中的output.linearDepth = -(UnityObjectToViewPos(input.vertex).z * _ProjectionParams.w);
中可能大家有疑问:_ProjectionParam.w
是什么东西,为何相乘就可以等于linearDepth了,按照上面Linear01Depth=EyeZ/Far
来推算,只有一种可能,那就是_ProjectionParam.w==1/far
。
-(UnityObjectToViewPos(input.vertex).z
很明显就是==EyeZ的运算了(从函数名就可以看出来,object to view),至于取负数,是因为:Camera下的view space是右手坐标系,X轴,Y轴,Z轴,分别是:X拇指指向右(X轴正方向),食指指向上(Y轴正方向),中指指向你自己(Z轴正方向),所以可以知道,Z轴是指向镜头内为正的方向,与我们世界坐标是反的,因为世界坐标、投影坐标是用左手坐标表示的,所以需要取反。
然后我们打开UnityCG.glslinc和UnityShaderVariables.cginc文件中都有定义、说明:
// x = 1 or -1 (-1 if projection is flipped)
// y = near plane
// z = far plane
// w = 1/far plane // 没错,就是1/far
uniform vec4 _ProjectionParams;
_CameraDepthTexture和_CameraDepthNormalsTexture 存的深度是什么
关于这两纹理也可以先参考官方文档:Camera’s Depth Texture。
- 写入只有深度值的纹理_CameraDepthTexture:在顶点着色器
COMPUTE_EYEDEPTH(ndc.z)
,在片段着色器的取ndc.z==clipPos.z/clipPos.w
就好了。 - 读取只有深度值的纹理_CameraDepthTexture:
tex2D(_CameraDepthTexture).r == ndcZ
。 - 写入深度+法线值的纹理_CameraDepthNormalsTexture:在顶点
o.depthNormals.xyz = COMPUTE_VIEW_NORMAL; o.depthNormals.w = COMPUTE_DEPTH_01;
,这里要留意COMPUTE_DEPTH_01
是有线性处理后的,在片段着色器return EncodeDepthNormal(i.depthNormals.w, i.depthNormals.xyz);
编码即可。 - 读取深度+法线值的纹理_CameraDepthNormalsTexture:在片段着色器
DecodeDepthNormal( tex2D(_CameraDepthNormalsTexture, screenPos.xy), depth, normal)
,这里读取的深度值是有线性处理过的,如果要将线性值,转回ndc.z值,可以反线性处理,意思说我们DecodeDepthNormal( tex2D(_CameraDepthNormalsTexture, screenPos.xy), depth, normal)
出来的depth值0~1范围的,所以我们按Linear01Depth的逆运算,即可求得ndc.z
。 - Linear01Depth的逆运算 Linear01Depth的函数
inline float Linear01Depth( float z ) { return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y); }
,逆运算就是已知Linear01Depth的结果,求参数z。
L i n e a r 01 D e p t h = 1.0 / ( Z B u f f e r P a r a m s . x ∗ z + Z B u f f e r P a r a m s . y ) ; Linear01Depth = 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y); Linear01Depth=1.0/(ZBufferParams.x∗z+ZBufferParams.y);
( Z B u f f e r P a r a m s . x ∗ z + Z B u f f e r P a r a m s . y ) ∗ L i n e a r 01 D e p t h = 1 (_ZBufferParams.x * z + _ZBufferParams.y) * Linear01Depth = 1 (ZBufferParams.x∗z+ZBufferParams.y)∗Linear01Depth=1
( Z B u f f e r P a r a m s . x ∗ z + Z B u f f e r P a r a m s . y ) = 1 / L i n e a r 01 D e p t h (_ZBufferParams.x * z + _ZBufferParams.y) = 1/Linear01Depth (ZBufferParams.x∗z+ZBufferParams.y)=1/Linear01Depth
( Z B u f f e r P a r a m s . x ∗ z ) = 1 / L i n e a r 01 D e p t h − Z B u f f e r P a r a m s . y (_ZBufferParams.x * z) = 1/Linear01Depth - _ZBufferParams.y (ZBufferParams.x∗z)=1/Linear01Depth−ZBufferParams.y
z = ( 1 / L i n e a r 01 D e p t h − Z B u f f e r P a r a m s . y ) / Z B u f f e r P a r a m s . x z = (1/Linear01Depth - _ZBufferParams.y) / _ZBufferParams.x z=(1/Linear01Depth−ZBufferParams.y)/ZBufferParams.x
所以逆运算为:z = (1/Linear01Depth - _ZBufferParams.y) / _ZBufferParams.x
,这个z
就是相当远_CameraDepthTexture的R通道值。 _CameraDepthTexture.r==ndc.z
,那么_CameraDepthNormalsTexture.ba解码出来的线性值
是什么呢?
借用一下puppet_master的博主的一张图:(原文:Unity Shader-深度相关知识总结与效果实现(LinearDepth,Reverse Z,世界坐标重建,软粒子,高度雾,运动模糊,扫描线效果) )
在引用他的原文:
上图中,A为相机位置,G为空间中我们要重建的一点,那么该点的世界坐标为A(worldPos) + 向量AG,我们要做的就是求得向量AG即可。根据三角形相似的原理,三角形AGH相似于三角形AFC,则得到AH / AC = AG / AF。由于三角形相似就是比例关系,所以我们可以把AH / AC看做01区间的比值,那么AC就相当于远裁剪面距离,即为1,AH就是我们深度图采样后变换到01区间的深度值,即Linear01Depth的结果d。那么,AG = AF * d。
如果还不了解,我再补充一下:_CameraDepthNormalsTexture.ba解码出来的线性值
就是depth
,而depth = AG / AF的一个比例值,结果肯定是0~1的。然后你可以用上面讲到的Linear01Depth的逆运算ndcZ = (1/Linear01Depth - _ZBufferParams.y) / _ZBufferParams.x
,将depth
代入Linear01Depth
,就可以求出ndcZ
,或是理解为:Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv))
== DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), out float outLinear01Depth, out float3 normal)
中的outLinear01Depth
值
题外话
如果你要写软渲染器,就要处理如果坐标转换,并将一些坐标写入DepthBuffer深度缓存。可参考下列资料:
- LearnOpenGL-CN:深度值精度
- 坐标转换可参考之前写的一篇:OpenGL Transformation 几何变换的顺序概要(MVP,NDC,Window坐标变换过程)
- Unity Shader-深度相关知识总结与效果实现(LinearDepth,Reverse Z,世界坐标重建,软粒子,高度雾,运动模糊,扫描线效果)
D
e
p
t
h
V
i
e
w
=
>
D
e
p
t
h
S
c
r
e
e
n
Depth_{View} => Depth_{Screen}
DepthView=>DepthScreen
视空间深度 转化到 屏幕空间深度的公式如下:
a
=
F
/
(
F
−
N
)
a = F / (F - N)
a=F/(F−N)
b
=
N
F
/
(
N
−
F
)
b = NF / (N - F)
b=NF/(N−F)
d
e
p
t
h
屏
幕
空
间
=
(
a
Z
+
b
)
/
Z
为
视
空
间
深
度
depth_{屏幕空间} = (aZ + b)/ Z_{为视空间深度}
depth屏幕空间=(aZ+b)/Z为视空间深度
d
e
p
t
h
=
(
a
Z
+
b
)
/
Z
depth = (aZ + b) / Z
depth=(aZ+b)/Z
D
e
p
t
h
S
c
r
e
e
n
=
>
D
e
p
t
h
V
i
e
w
Depth_{Screen} => Depth_{View}
DepthScreen=>DepthView
反推得 屏幕空间深度 转化到 视图空间深度的等于以下公式:
d
e
p
t
h
=
(
a
Z
+
b
)
/
Z
depth = (aZ + b) / Z
depth=(aZ+b)/Z
d
e
p
t
h
=
a
+
b
/
Z
depth = a + b / Z
depth=a+b/Z
d
e
p
t
h
−
a
=
b
/
Z
depth - a = b / Z
depth−a=b/Z
(
d
e
p
t
h
−
a
)
/
b
=
1
/
Z
(depth - a) / b = 1 / Z
(depth−a)/b=1/Z
b
/
(
d
e
p
t
h
−
a
)
=
Z
b / (depth - a) = Z
b/(depth−a)=Z
D
e
p
t
h
S
c
r
e
e
n
=
>
D
e
p
t
h
V
i
e
w
Depth_{Screen} => Depth_{View}
DepthScreen=>DepthView 公式:
b
/
(
d
e
p
t
h
−
a
)
=
Z
b / (depth - a) = Z
b/(depth−a)=Z,代入a,b:
(
N
F
/
(
N
−
F
)
/
(
d
e
p
t
h
−
(
F
/
(
F
−
N
)
)
)
=
Z
(NF / (N - F) / (depth - (F / (F - N))) = Z
(NF/(N−F)/(depth−(F/(F−N)))=Z
假设N=0.3, F=1000,代入N,F:
((0.3 * 1000) / (0.3 - 1000)) / (depth - (1000 / (1000 - 0.3))) = Z
(300 / (-999.7)) / (depth - (1000 / 999.7)) = Z
-0.3000900270081024 / (depth - 1.000300090027008)= Z
假设depth = 0.5,代入depth
-0.3000900270081024 / (0.5 - 1.000300090027008) = Z
-0.3000900270081024 / -0.500300090027008 = Z
0.5998200539838049 = Z
当depth(屏幕空间) = 0.5,Z(Z为视空间深度) = 0.5998200539838049
那么屏幕空间的depth就是要输入深度纹理的值。
另外,还有一篇文章写得非常不错:OpenGL阴影映射
里面有将阴影如何生成,以及如何将片段转换到灯光坐标系空间下来获取深度图,然后判断片段深度与灯光坐标空间下的深度图的深度值做比较的决定是否在阴影中。
里面的深度图,就是将ndc中的z值写入到深度纹理的:tex2D(_CameraDepthTexture, screenPos.xy).r == ndcZ
我借一下他文章中引用的learnopengl中借来的图
注意Unity正交相机中的深度纹理的编码
我虽然没去下载Unity的内置CameraDepthTexture.shader相关的代码。
但是试验后证明,Unity的正交相机的_CameraDepthTexture存着的内容与透视不一样。
- 透视下存储的是
ndc.z
。 - 正交下存储的是线性深度比例值,而且他没有处理:
UNITY_REVERSED_Z
的宏的情况。而透视下的是处理了。所以可以看到我们的shader代码中,透视的是没有处理这个UNITY_REVERSED_Z
的代码的。
先用Profiler>Frame Debugger看看_CameraDepthTexture的内容效果:
可以看到,我镜头距离模型很近时,反而深度值越大,距离模型很远时,深度值越小。
这就与透视的不一样了。从这点大概猜到:随着Unity shader内置代码的升级,官方也不断完善处理。让外部使用透明化,但我现在用的是:unity 2018.3.0f2版本的,正交相机下的,他们漏了处理这块内容,我猜大概是正交相机下比较少人用这块深度功能?用正交的一般都是2D游戏。但现在很多2D,或是固定视角2.5D的也有一部分是3D的。
注意:
// 正交相机下,_CameraDepthTexture存储的是线性值,且:距离镜头远的物体,深度值小,距离镜头近的物体,深度值大,可以使用UNITY_REVERSED_Z宏做处理
// 透视相机下,_CameraDepthTexture存储的是ndc.z值,且:不是线性的。
// _CameraDepthNormalsTexture的纹理,正交相机,与透视相机下都没问题一样这么使用
具体可以参考:Unity Shader - 根据片段深度重建片段的世界坐标。只看在正交相机下的深度纹理重构世界坐标,在里头有详细的试验。
总结
在Unity中,获取深度的方式有多种多样。
理解如何编解码深度,对理解深度纹理存储的是什么内容有帮助。
理解Unity提供的深度纹理存储的是什么,才能知道什么地方,什么时候去使用它。
因为总有某些功能在实现时,是基于深度值来实现。
Project
backup : UnityShader_GetDepthNormalsTextureTesting_2018.03.2f0
- Camera.depthTextureMode方式运行:GetBuiltInDepthTextureScene.unity场景。
- 自定义深度纹理方式运行:GetCustomDepthTextureScene.unity场景。
References
- Using Depth Textures
- Unity Shader-深度相关知识总结与效果实现(LinearDepth,Reverse Z,世界坐标重建,软粒子,高度雾,运动模糊,扫描线效果) - 这篇文章写得很详细,推荐看看。
- 【Unity Shader】浅析Unity shader中RenderType的作用及_CameraDepthNormalsTexture
- Unity Shader 基础(3) 获取深度纹理
- 关于LightMode的pass tag可以查看官方手册或是API的说明。
- Unity3D中的深度纹理和法线纹理
- Unity Camera获取深度数据的几种方式 - 这时我写完文章后,发现另一个写得也不错的,分享一下。