Unity 深度 depth (URP)

本文基于Unity URP,对深度相关知识进行整理。介绍了Eye Depth,即物体相对摄像机平面的距离,还提及ComputeScreenPos的作用;阐述深度图,包括其存储及采样方法;最后讲述根据深度重建世界坐标的多种方法,如逆推、射线法等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文只是对深度的一些整理和个人理解,如果有不对的地方,请一定要告诉我。演示基于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

### Unity URP中的深度问题及其解决方案 #### 深度图配置与使用 在Unity的通用渲染管线(URP)中,为了正确处理场景中的物体遮挡关系以及实现诸如阴影、反射等效果,深度图的应用至关重要。对于不透明对象而言,在Shader中启用深度写入功能是必要的;而对于半透明对象,默认情况下不会参与深度测试也不更新深度缓冲区,这意味着如果希望某些特殊材质能够影响或响应其他几何体,则需特别设置。 当涉及到具体的Shader编写时,确保通过`DepthStencilState`来控制如何对待深度值是非常重要的[^2]: ```csharp Tags { "RenderType"="Opaque" } // 或者针对特定需求调整此标签 Pass { ZWrite On // 开启深度写入 } ``` 此外,在片段着色器阶段读取来自先前绘制通道所存储的距离信息前,先声明并初始化好相应的纹理资源和采样状态变量: ```hlsl TEXTURE2D(_CameraDepthTexture); SAMPLER(sampler_CameraDepthTexture); float rawZ = SAMPLE_TEXTURE2D(_CameraDepthTexture, sampler_CameraDepthTexture, i.uv).r; ``` 以上操作允许后续逻辑基于这些数据执行进一步计算,比如转换为视空间位置或是作为环境光散射强度衰减因子的一部分[^1]。 #### 解决SRP Batcher兼容性挑战 值得注意的是,在利用URP特性的同时还可能遇到性能优化工具——如SRP Batcher带来的新难题。由于该机制依赖于静态批处理技术减少Draw Call次数从而提高效率,因此任何引入额外关键字组合的情况都可能导致其失效。具体表现为含有条件编译指令(`#pragma multi_compile`)或其他动态变化属性(例如自定义光照模型参数) 的Material可能会被排除在外[^3]。 对此类情形的一个有效应对策略是在不影响核心视觉表现的前提下尽可能精简可选配置项数量,并考虑采用预烘焙方式代替运行时期望实时改变的部分内容。同时也要留意官方文档和技术社区内关于最新版本修复进展的消息,以便及时跟进最佳实践建议。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值