本文只是對深度的一些整理和個人理解,如果有不對的地方,請一定要告訴我,演示基于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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/251704.html
標籤:其他
