本文只是对深度的一些整理和个人理解,如果有不对的地方,请一定要告诉我。演示基于Unity URP, shader用shader graph 或者HLSL,build-in自行根据对照表更改
1. Eye Depth(观察空间)
Eye Depth是物体相对于摄像机所在平面的距离,因为是相对,所以Z是相反的,Eye Depth的0就是摄像机,1就是一个单位,10就是10个单位,所以有的人会把他称为“World” space Depth,这个depth在所有平台上都是一样的,而且不区分正交透视,因为他在投影之前。在shader graph 中如何得到片段的Eye Depth呢
正交或者投影
只适用于透视
HLSL
第一种
// 顶点
float3 positionWS = TransformObjectToWorld(IN.positionOS.xyz);
float3 positionVS = TransformWorldToView(positionWS);
OUT.positionVS = positionVS;
// 片段
float fragmentEyeDepth = -IN.positionVS.z;
第二种
//裁剪空间坐标
float4 positionCS = TransformWorldToHClip(input.positionOS.xyz);
OUT.positionCS = positionCS;
//求屏幕坐标
//一种方法是
float4 positionScr = positionCS * 0.5f;
positionScr.xy = float2(positionScr.x, positionScr.y * _ProjectionParams.x) + positionScr.w;
positionScr.zw = positionCS.zw;
OUT.scrPos= positionScr;
//或者直接用URP封装的
OUT.scrPos= ComputeScreenPos(positionCS);//[-w,w]->[0,w]
//片段
float fragmentEyeDepth = IN.scrPos.w;
这边扯开了,先讲下ComputeScreenPos,他的作用是把裁剪空间齐次坐标转换到屏幕空间的齐次坐标 ,或者换句话说就是把xy取值范围从[-w,w] 转到 [0,w],然后再做透视除法(齐次除法),取值范围变成[0,1],就可以用来提取屏幕纹理了
注意 透视除法应该放在片段着色器里执行,不然会造成UV扭曲,因为片段着色器之前的光栅化会对几何数据做差值,而做差值前做除法会导致结果不准确
这个方法得到了屏幕齐次坐标,那么再经过透视除法,可以的到NDC,再走一步可以直接得到Z Buffer Depth,也就是深度纹理中的像素值
float3 ndcPos = IN.scrPos.xyz / IN.scrPos.w;//[0-1] D3D
float2 screenUV = ndcPos.xy;
float zdepth = ndcPos.z;
对于D3D,NDC z取值范围已经是[0,1],所以可以直接相等,而openGL z在[-1,1],需要
z* 0.5 + 0.5
所以我常用的是第二种方法,因为可以顺便得到更多的信息
PS:
NDC=,但是如果你想得到NDC Z/Depth
2.深度图
在渲染opaque and transparent 之间,URP复制了depth buffer,然后储存在了一张贴图中,后面的Transparent就可以获得这些值,并用来计算,比如
护盾俯视角高度雾
物体浸入水中
水的泡沫
未完待续。。。。。
继续!
如何对深度图采样呢
方法一
TEXTURE2D(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);
//片段,用之前求到的scrPos
float sceneRawDepth=SAMPLE_TEXTURE2D(_CameraDepthTexture, sampler_CameraDepthTexture, IN.scrPos.xy / IN.scrPos.w)
或者直接用URP封装的
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
//片段
float sceneRawDepth = SampleSceneDepth(IN.scrPos.xy / IN.scrPos.w);
float sceneEyeDepth = LinearEyeDepth(sceneRawDepth, _ZBufferParams);
float scene01Depth = Linear01Depth(sceneRawDepth, _ZBufferParams);
注意这个时候得到的raw depth在view space是非线性的,但是在NDC/Screen space 是线性的,简单来说是为了在D3D平台获得更高精度,而线性的深度则正好反过来
可以看这篇文章https://developer.nvidia.com/content/depth-precision-visualized
和这篇http://www.humus.name/index.php?ID=255
frac(sceneRawDepth*1000)
frac(scene01Depth *1000);
对于正交相机,rawDepth 已经是线性的了,和linerdepth相等,或者one minus(根据flip),要求EyeDepth的话就需要对远平面,近平面求差值,t=linerdepth
shader graph就很简单了,记得选Transparent
3.Reconstruct World Space Position from Depth (根据深度重建世界坐标)
未完待续。。。。
继续!
首先为了验证我们求到的世界坐标没问题,先创建一些物体,贴近摄像机
,然后用shader显示自己的世界坐标
//v
OUT.wPos = TransformObjectToWorld(IN.positionOS.xyz);
//f
return half4(IN.wPos,1);
,然后切回普通材质
第一种方法 最无脑的 把片段的屏幕坐标逆推到view space,然后用逆矩阵转回world space
//VP
float2 p11_22 = float2(unity_CameraProjection._11, unity_CameraProjection._22);
float3 vpos = float3((IN.uv * 2 - 1) / p11_22, -1) *eyeDepth;
float4 wposVP = mul(_InverseView, float4(vpos, 1));
_InverseView就是cameraToWorldMatrix,需要用C#传递
见证奇迹的时刻到了,打开后处理
WTF???原来是忘记判断天空盒了,加入depth01 < 1
发现屏幕完全一致,说明结果是正确的
第二种方法 NDC逆推
//V
float4 ndcPos = (OUT.screenPos / OUT.screenPos.w) * 2 - 1;
float far = _ProjectionParams.z;
float4 clipVec = float4(ndcPos.x, ndcPos.y,1.0, 1.0)*far;
OUT.viewVec = mul(unity_CameraInvProjection, clipVec).xyz;
//F
float3 vPos = IN.viewVec*depth01;
float3 wPos = mul(_InverseView, float4(vPos, 1.0)).xyz;
首先通过NDC对应的远平面的点转到clip space中的远平面的点,然后用逆投影矩阵得到view space中的远平面的点
然后乘以线性深度,得到屏幕深度像素中储存的view space点,最后把这个点用逆矩阵转到world space,打开后处理(注意了第四个值=1就是点,0就是向量,你可以简单的想象平行光和点光源)
结果依然正确
第三章方法 也是NDC逆推
float4 GetWorldPositionFromDepth(float2 uv_depth)
{
float depth = SampleSceneDepth(uv_depth);
#if defined(SHADER_API_OPENGL)
depth = depth * 2.0 - 1.0;
#endif
float2 projectedXY = float2(uv_depth) * 2 - 1;//[-1,1]screen coordinates
float4 H = float4(projectedXY, depth, 1.0);//NDC
float4 D = mul(_ViewProjInv, H);
return D / D.w;
}
//f
float4 wPos = GetWorldPositionFromDepth(IN.uv);
c#中
void VPI()
{
var viewMat = Camera.main.worldToCameraMatrix;
var projMat = GL.GetGPUProjectionMatrix(Camera.main.projectionMatrix, false);
var viewProjMat = (projMat * viewMat);
Shader.SetGlobalMatrix("_ViewProjInv", viewProjMat.inverse);
}
结果也是一样
第四种方法 射线法
我们已知相机的位置,那如果能知道像素点相对于相机的偏移,不就能直接得到世界坐标吗
也就是wPos=ray * eyeDepth + _WorldSpaceCameraPos.
怎么求射线呢。
1.求摄像机到近平面的四个角的射线
2.后处理中,这四个点就是一个全屏Quad
3.上面说过了顶点到片元会自动差值,所以可以直接得到我们要的射线
c#求射线,具体代码的意义 其实就是相似三角形 可以去翻《入门精要》
float height = cam.nearClipPlane * Mathf.Tan(Mathf.Deg2Rad * cam.fieldOfView * 0.5f);
Vector3 up = cam.transform.up * height;
Vector3 right = cam.transform.right * height * cam.aspect;
Vector3 forward = cam.transform.forward * cam.nearClipPlane;
Vector3 ButtomLeft = forward - right - up;
float scale = ButtomLeft.magnitude / cam.nearClipPlane;
ButtomLeft.Normalize();
ButtomLeft *= scale;
Vector3 ButtomRight = forward + right - up;
ButtomRight.Normalize();
ButtomRight *= scale;
Vector3 TopRight = forward + right + up;
TopRight.Normalize();
TopRight *= scale;
Vector3 TopLeft = forward - right + up;
TopLeft.Normalize();
TopLeft *= scale;
Matrix4x4 MATRIX = new Matrix4x4();
MATRIX.SetRow(0, ButtomLeft);
MATRIX.SetRow(1, ButtomRight);
MATRIX.SetRow(2, TopRight);
MATRIX.SetRow(3, TopLeft);
mat.SetMatrix("Matrix", MATRIX);
那么怎么在顶点中判断哪条射线对应哪个角呢,你也可以像书里一样用if判断,或者。。。
我们已经知道UV和index的关系可以求出一个隐函数,F(x, y) = abs (3 * y-x)
//v
int t = 0;
t=abs(3 * i.texcoord.y - i.texcoord.x);
o.Dirction = Matrix[t].xyz;
//f
float3 wsPos =_WorldSpaceCameraPos + depth01 * i.Dirction * _ProjectionParams.z;
输出一下,锵锵,,结果正确
第五种 shader graph
Function中其实就是一个得到矩阵的方法
uniform float4x4 _InverseView;
void GetInverseView_float(out float4x4 Out){
Out = _InverseView;
}
聪明的小伙伴已经发现了 这其实就是 第二种方法的shader graph 版本
知道了世界坐标就可以为所欲为,为所欲为,为所欲为
未完待续 漏了一个_CameraDepthNormalsTexture