原文链接
原文写得很好,但是主题格式我不太喜欢,所以我对文章的格式和部分错误的地方进行了补充校正,方便自己日后阅读
正文
在某些特定应用场景,比如说 屏幕空间反射,会要求我们从深度缓冲中重建像素点的世界空间位置。本文介绍在 Unity 中如何从深度缓冲中重建世界空间位置。
深度缓冲
首先先来看看在 Unity 中怎么计算深度。
- UnityCG.cginc
#define COMPUTE_EYEDEPTH(o) o = -mul( UNITY_MATRIX_MV, v.vertex ).z
#define COMPUTE_DEPTH_01 -(mul( UNITY_MATRIX_MV, v.vertex ).z * _ProjectionParams.w)
其中,_ProjectionParams.w 是 1 / FarPlane 1。
符号取反 的原因是在 Unity 的 观察空间(View space)中 z 轴翻转了,摄像机的前向量就是
z轴的正方向 2。这是和 OpenGL 中不一样的一点。
从上式可知,Unity 中的
- 观察线性深度(Eye depth)就是顶点在观察空间(View space)中的
z分量, - 01 线性深度(01 depth)就是观察线性深度通过除以 摄像机远平面 重新映射到
[0,1]区间所得到的值。
我们可以从深度缓冲中采样得到深度值,并使用 Unity 中内置的功能函数将原始数据转换成 线性深度。
- UnityCG.cginc【具体详解见这篇文章】
// Z buffer to linear 0..1 depth (0 at eye, 1 at far plane)
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);
}
我的知识串联 —— 实际应用
- 护盾效果
- 水池边缘相交效果
一、从 NDC 空间中重建
第一种方法 是通过齐次坐标系下的屏幕坐标位置来计算。
首先将齐次坐标系下的屏幕坐标(未做透视除法)转换到 NDC 空间中。
float4 ndcPos = (o.screenPos / o.screenPos.w) * 2 - 1;
然后将(NDC 空间下的)屏幕像素对应在摄像机 远平面(Far plane)的点转换到 裁剪空间(Clip space)。因为在 NDC 空间 中远平面上的点的 z 分量为 1,所以可以直接乘以摄像机的 far 值来将其转换到 裁剪空间
我的补充:实际就是 视锥体深度范围 的逆变换 。
float far = _ProjectionParams.z;
float3 clipVec = float3(ndcPos.x, ndcPos.y, 1.0) * far;
接着通过 逆投影矩阵(Inverse Projection Matrix)将点转换到 观察空间(View space)。
float3 o.viewVec = mul(unity_CameraInvProjection, clipVec.xyzz).xyz;
已知在 观察空间 中摄像机的位置 一定 为 (0, 0, 0),所以从摄像机指向远平面上的点的 viewVec 就是其在 观察空间 中的方向。
将 viewVec 乘以 线性深度值,得到在深度缓冲中储存的值的 观察空间 位置。
float depth = UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture, i.screenPos));
float3 viewPos = i.viewVec * Linear01Depth(depth);
最后将 观察空间 中的位置变换到 世界空间 中。
float3 worldPos = mul(UNITY_MATRIX_I_V, float4(viewPos, 1.0)).xyz;
附上在 Shader Graph 中的实现。这里 Unity 有 bug 导致如果使用 Transformation Matrix 节点的 Inverse Projection 会报错,所以这里使用了一个 Custom Function 节点输出一个 4x4 矩阵 unity_CameraInvProjection。理论上效果是一样的。
- Shader Graph(Custom Function 输出的是
unity_CameraInvProjection)
我的知识串联 —— 针对作者 CJT 说 Unity 有 bug
即使用 Transformation Matrix 节点的
Inverse Projection会报错。知识串联_1:
URP_BlitRenderFeature 作者大大的方法,思路和本文作者 CJT 有点类似,也是用了自定义节点(
unity_CameraInvProjection),只不过他基于的是 物体空间,深度选择的是Eye模式知识串联_2:
但是我看国外某大佬制作 Decal 效果 —— 同样使用深度(
Raw模式)信息结合 Screen Position(center模式 —— 即 NDC 空间 + 居中变换)重构 世界空间 的位置他采用的是
Inverse View Projection选项,是可以正常使用的。
- ShaderGraph 如下:
二、在世界空间中重建(有争议)
第二种方法 是利用 WorldSpaceViewDir 得到 世界空间 中的从物体顶点指向摄像机的方向向量来计算。
首先构造在 世界空间 中 (输入为物体空间的)顶点指向摄像机 的向量。
o.worldSpaceDir = WorldSpaceViewDir(v.vertex);
我的补充:
// Computes world space view direction inline float3 WorldSpaceViewDir( in float4 v ) { return _WorldSpaceCameraPos.xyz - mul(_Object2World, v).xyz; }我认为原文这里其实这里应该加一个 负号,将它转为 摄像机指向物体顶点 的方向向量
将 世界空间 向量转换到 观察空间,存储其 z 分量的值。
注意 向量和位置的空间转换是不同的,当
w分量为 0 的时候 Unity 会将其视为向量,而当w分量为 1 的时候 Unity 将其视为位置。
o.viewSpaceZ = mul(UNITY_MATRIX_V, float4(o.worldSpaceDir, 0.0)).z;
接着,在深度缓冲中采样。
这里使用
tex2Dproj而不是tex2D的原因是:screenPos是用 ComputeScreenPos 来计算得到的,用tex2Dproj可以刚好帮我们做透视除法。
float eyeDepth = UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture, i.screenPos));
eyeDepth = LinearEyeDepth(eyeDepth);
因为深度缓存采样出并经过 LinearEyeDepth 变换后的 观察线性深度 就是其在 观察空间 中的 z 分量(但是由于在 Unity 使用的视角空间中,摄像机正向对应的 z 值均为负值,所以 eyeDepth 应该取负),所以根据向量的 z 分量计算其缩放因子,将向量缩放到实际的长度。
i.worldSpaceDir *= -eyeDepth / i.viewSpaceZ;
最后以摄像机为 起点,缩放后的向量为指向向量,得到像素点在世界空间中位置。
float3 worldPos = _WorldSpaceCameraPos + i.worldSpaceDir;
附上在 Shader Graph 中的实现。
- Shader Graph
三、正交摄像机的情况(有争议)
unity_OrthoParams各个分量的含义
x=width,y=height,z没有定义w=1.0(该摄像机是 正交 摄像机)w=0.0(该摄像机是 透视 摄像机)
如果摄像机不是透视而是 正交 的,做法上就有些不同。计算 观察空间(View space)中顶点的 xy 分量。
float4 ndcPos = (o.screenPos / o.screenPos.w) * 2 - 1;
o.viewVec = float3(unity_OrthoParams.xy * ndcPos.xy, 0);
如果是正交摄像机,则会对深度缓冲中采样出的 rawDepth 进行特别的处理,即以下代码中的 ortho/far 作为 depth。
该方法来自:Unity 下无视 摄像机模式 和 裁剪平面 都能得到正确线性深度的方法 3。
// Sample the depth texture to get the linear depth
float rawDepth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.screenPos));
float ortho = (far - near) * (1 - rawDepth) + near;
float depth = lerp(LinearEyeDepth(rawDepth), ortho, unity_OrthoParams.w) / far;
rawDepth=1的时候ortho=near,depth=near/far
rawDepth=0的时候ortho=far,depth=1
接着再根据上一步得到的深度值 depth 在作线性插值,就得到顶点在 观察空间 中的 z 分量(注意取负值)。
// Linear interpolate between near plane and far plane by depth value
float z = -lerp(near, far, depth);
最后将 观察空间 中的位置转换到 世界空间。
float3 worldPos = mul(UNITY_MATRIX_I_V, float4(viewPos, 1)).xyz;
// Wrong depth
// float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.screenPos));
// Correct Depth
float rawDepth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.screenPos));
float ortho = (far - near) * (1 - rawDepth) + near;
float depth = lerp(LinearEyeDepth(rawDepth), ortho, unity_OrthoParams.w) / far;
附上在 Shader Graph 中的实现。其中 Custom Function 节点为:
float ortho = (_ProjectionParams.z - _ProjectionParams.y) * (1 - rawDepth)
+ _ProjectionParams.y;
depth = lerp(eyeDepth, ortho, unity_OrthoParams.w) / _ProjectionParams.z;
- Shader Graph
注意:
该方法我没有验证,但是从原文的评论来看,还是有点争议的
最后(完整代码)
以上方法可以根据实际渲染的对象是在世界中物体(贴花)还是屏幕大小的四边形(后处理)来灵活使用。
附上keijiro 大神写的代码作为参考,这个项目通过后处理将 深度缓冲 转换成 世界空间 位置并可视化。
我的补充:
大神的源码已经 删除 了第二种方法(
世界空间)并 修改 了第三种(正交相机)
1. NDC 方法
与大神仓库的源码略有不同
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float4 vertex : SV_POSITION;
float4 screenPos : TEXCOORD0;
float3 viewVec : TEXCOORD1;
};
v2f vert(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// Compute texture coordinate
o.screenPos = ComputeScreenPos(o.vertex);
// NDC position
float4 ndcPos = (o.screenPos / o.screenPos.w) * 2 - 1;
// Camera parameter
float far = _ProjectionParams.z;
// View space vector pointing to the far plane
float3 clipVec = float3(ndcPos.x, ndcPos.y, 1.0) * far;
o.viewVec = mul(unity_CameraInvProjection, clipVec.xyzz).xyz;
return o;
}
sampler2D _CameraDepthTexture;
half4 frag(v2f i) : SV_Target
{
// Sample the depth texture to get the linear 01 depth
float depth = UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture, i.screenPos));
depth = Linear01Depth(depth);
// View space position
float3 viewPos = i.viewVec * depth;
// Pixel world position
float3 worldPos = mul(UNITY_MATRIX_I_V, float4(viewPos, 1)).xyz;
return float4(worldPos, 1.0);
}
2. 世界空间方法
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float4 vertex : SV_POSITION;
float4 screenPos : TEXCOORD0;
float3 worldSpaceDir : TEXCOORD1;
float viewSpaceZ : TEXCOORD2;
};
v2f vert(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// World space vector from camera to the vertex position
o.worldSpaceDir = WorldSpaceViewDir(v.vertex);
// Z value of the vector in view space
o.viewSpaceZ = mul(UNITY_MATRIX_V, float4(o.worldSpaceDir, 0.0)).z;
// Compute texture coordinate
o.screenPos = ComputeScreenPos(o.vertex);
return o;
}
sampler2D _CameraDepthTexture;
half4 frag(v2f i) : SV_Target
{
// Sample the depth texture to get the linear eye depth
float eyeDepth = UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture, i.screenPos));
eyeDepth = LinearEyeDepth(eyeDepth);
// Rescale the vector
i.worldSpaceDir *= -eyeDepth / i.viewSpaceZ;
// Pixel world position
float3 worldPos = _WorldSpaceCameraPos + i.worldSpaceDir;
return float4(worldPos, 1.0);
}
3. 正交摄像机的情况
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float4 vertex : SV_POSITION;
float4 screenPos : TEXCOORD0;
float3 viewVec : TEXCOORD1;
};
v2f vert(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// Compute texture coordinate
o.screenPos = ComputeScreenPos(o.vertex);
// NDC position
float4 ndcPos = (o.screenPos / o.screenPos.w) * 2 - 1;
// View space vector from near plane pointing to far plane
o.viewVec = float3(unity_OrthoParams.xy * ndcPos.xy, 0);
return o;
}
sampler2D _CameraDepthTexture;
half4 frag(v2f i) : SV_Target
{
// Camera parameters
float near = _ProjectionParams.y;
float far = _ProjectionParams.z;
// Sample the depth texture to get the linear depth
float rawDepth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.screenPos));
float ortho = (far - near) * (1 - rawDepth) + near;
float depth = lerp(LinearEyeDepth(rawDepth), ortho, unity_OrthoParams.w) / far;
// Linear interpolate between near plane and far plane by depth value
float z = -lerp(near, far, depth);
// View space position
float3 viewPos = float3(i.viewVec.xy, z);
// Pixel world position
float3 worldPos = mul(UNITY_MATRIX_I_V, float4(viewPos, 1)).xyz;
return float4(worldPos, 1.0);
}