从深度图反推世界坐标:在Unity Shader中实现屏幕空间后处理的完整指南(附代码)

张开发
2026/4/13 21:58:50 15 分钟阅读

分享文章

从深度图反推世界坐标:在Unity Shader中实现屏幕空间后处理的完整指南(附代码)
从深度图反推世界坐标在Unity Shader中实现屏幕空间后处理的完整指南附代码在Unity的屏幕空间后处理效果开发中深度纹理_CameraDepthTexture是一个强大的工具。它不仅仅用于简单的深度检测更可以作为重建三维场景信息的基础。本文将深入探讨如何仅凭深度纹理和相机投影矩阵的逆矩阵在Fragment Shader中精确地重建每个像素在观察空间和世界空间中的三维位置。1. 深度纹理与坐标系统基础Unity中的深度纹理存储的是经过非线性变换后的深度值。要理解如何从这些值反推原始坐标我们需要先了解Unity的渲染管线中坐标系统的转换流程。1.1 坐标系统转换流程在Unity渲染管线中顶点坐标经历以下转换过程模型空间→世界空间通过模型变换矩阵世界空间→观察空间通过视图矩阵观察空间→裁剪空间通过投影矩阵裁剪空间→NDC空间通过透视除法NDC空间→屏幕空间通过视口映射深度纹理中存储的值实际上是NDC空间中的z分量经过非线性映射后的结果。要重建世界坐标我们需要逆向这一过程。1.2 深度值的非线性特性Unity中的深度值是非线性分布的这由透视投影的特性决定// 观察空间z值到深度纹理值的转换 float depth (1.0 / z_eye - 1.0 / near) / (1.0 / far - 1.0 / near);这种非线性分布意味着在近裁剪平面附近有更高的精度这对于深度测试是有利的但也增加了我们反算坐标时的复杂性。2. 从屏幕UV到观察空间坐标2.1 基本重建步骤要从屏幕UV和深度值重建观察空间坐标我们需要将屏幕UV转换为NDC坐标从深度纹理中获取并还原NDC的z分量使用逆投影矩阵将NDC坐标转换回观察空间以下是核心Shader代码实现float4 ReconstructViewPosition(float2 uv, float depth) { // 步骤1将屏幕UV转换为NDC坐标 float4 clipPos; clipPos.xy uv * 2.0 - 1.0; clipPos.z depth * 2.0 - 1.0; clipPos.w 1.0; // 步骤2应用逆投影矩阵 float4 viewPos mul(unity_CameraInvProjection, clipPos); // 透视除法 viewPos / viewPos.w; return viewPos; }2.2 处理正交投影当相机使用正交投影时重建过程略有不同。我们需要检测当前投影类型并做相应处理float4 ReconstructViewPosition(float2 uv, float depth) { float4 clipPos; clipPos.xy uv * 2.0 - 1.0; clipPos.z depth * 2.0 - 1.0; clipPos.w 1.0; float4 viewPos mul(unity_CameraInvProjection, clipPos); // 检测是否为透视投影 if(unity_OrthoParams.w 0) { // 透视投影需要透视除法 viewPos / viewPos.w; } else { // 正交投影不需要透视除法 viewPos.z * -1; } return viewPos; }3. 从观察空间到世界空间一旦我们有了观察空间坐标转换为世界空间就相对简单了float3 ReconstructWorldPosition(float2 uv, float depth) { float4 viewPos ReconstructViewPosition(uv, depth); float4 worldPos mul(unity_CameraToWorld, float4(viewPos.xyz, 1.0)); return worldPos.xyz; }3.1 完整Shader函数实现下面是一个完整的、可直接在Unity后处理效果中使用的函数// 从深度纹理重建世界坐标 float3 ReconstructWorldPositionFromDepth(float2 uv) { // 从深度纹理获取深度值 float depth SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, uv); // 转换为线性深度0-1范围 depth Linear01Depth(depth, _ZBufferParams); // 重建观察空间位置 float4 clipPos float4(uv * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0); float4 viewPos mul(unity_CameraInvProjection, clipPos); viewPos / viewPos.w; // 转换为世界空间 float4 worldPos mul(unity_CameraToWorld, float4(viewPos.xyz, 1.0)); return worldPos.xyz; }4. 实际应用场景与优化4.1 屏幕空间环境光遮蔽(SSAO)重建世界坐标是SSAO效果的核心。有了每个像素的世界坐标和法线信息我们可以计算周围像素的遮挡情况float CalculateOcclusion(float2 uv, float3 worldPos, float3 normal) { float occlusion 0.0; const int samples 16; float radius 0.5; for(int i 0; i samples; i) { // 获取随机偏移 float2 offset GetRandomOffset(i) * radius; float2 sampleUV uv offset; // 重建采样点的世界坐标 float3 samplePos ReconstructWorldPositionFromDepth(sampleUV); // 计算向量和距离 float3 vec samplePos - worldPos; float dist length(vec); vec normalize(vec); // 计算遮挡贡献 float influence max(0.0, dot(normal, vec)); float attenuation 1.0 / (1.0 dist); occlusion influence * attenuation; } return 1.0 - (occlusion / samples); }4.2 性能优化技巧使用计算着色器对于需要大量位置重建的计算考虑使用Compute Shader并行处理降低采样分辨率某些效果可以以半分辨率计算然后上采样缓存计算结果如果多个效果需要相同的位置数据考虑只计算一次// 示例半分辨率处理 float2 halfResUV uv * 0.5; float3 worldPos ReconstructWorldPositionFromDepth(halfResUV);4.3 边缘处理与特殊情况在屏幕边缘或遮挡边界处深度重建可能会出现问题。我们需要添加一些保护性代码float3 SafeReconstructWorldPosition(float2 uv) { // 检查UV是否在有效范围内 if(any(uv 0) || any(uv 1)) return float3(0, 0, 0); float depth SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, uv); // 检查是否为背景深度值为1 if(depth 1.0) return float3(0, 0, 0); return ReconstructWorldPositionFromDepth(uv); }5. 高级应用延迟着色与全局光照在延迟渲染管线中世界位置重建技术尤为重要。我们可以扩展这一技术来实现更复杂的效果。5.1 结合GBuffer的优化实现在延迟渲染中我们可以利用已有的GBuffer数据来优化位置重建// 从GBuffer重建世界位置更精确的版本 float3 ReconstructWorldPosFromGBuffer(float2 uv) { // 获取深度和视图空间位置 float depth SampleDepth(uv); float3 viewPos ReconstructViewPosition(uv, depth); // 直接从GBuffer获取法线视图空间 float3 viewNormal SampleGBufferNormal(uv); // 更精确的世界位置重建考虑法线偏移 float3 worldPos mul(unity_CameraToWorld, float4(viewPos viewNormal * 0.01, 1.0)); return worldPos; }5.2 屏幕空间全局光照(SSGI)结合世界位置重建我们可以实现简单的屏幕空间全局光照float3 CalculateScreenSpaceGI(float2 uv, float3 worldPos, float3 normal) { float3 gi float3(0, 0, 0); const int rays 8; const int steps 16; for(int i 0; i rays; i) { // 生成随机反射方向 float3 rayDir GetReflectionDirection(normal, i); // 步进追踪 float3 currentPos worldPos; for(int j 0; j steps; j) { currentPos rayDir * 0.1; // 将世界坐标投影回屏幕空间 float4 projPos mul(unity_MatrixVP, float4(currentPos, 1.0)); projPos.xy / projPos.w; float2 screenUV projPos.xy * 0.5 0.5; // 检查是否在屏幕内 if(any(screenUV 0) || any(screenUV 1)) break; // 获取该位置的世界坐标 float3 hitPos ReconstructWorldPositionFromDepth(screenUV); // 计算距离并累加光照 float dist distance(currentPos, hitPos); if(dist 0.1) { // 命中表面累加颜色 gi SampleGBufferColor(screenUV) * max(0, dot(normal, rayDir)); break; } } } return gi / rays; }在实际项目中我发现使用半分辨率计算SSGI效果配合适当的时间累积抗噪可以在保持较好视觉效果的同时显著提升性能。重建世界坐标的精度对最终效果影响很大特别是在处理复杂几何体边缘时需要特别注意深度值的准确性和边缘处理。

更多文章