深度分析屏幕空间反射算法以及在URP中的实现(二)
- 作者:admin
- /
- 时间:一小时前
- /
- 浏览:10 次
- /
- 分类:厚积薄发
【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!
前言
湿润的地面,倒映着远方青山,什么样的技术才能造就如此绚丽的效果呢?
本文实现的效果:
使用法线贴图的效果

平滑地面
代码仓库:
GitHub: Screen Space Reflection in URP
Unity版本:2022.3.60f1c1
URP版本:14.0.12
使用延迟渲染
本文承接《深度分析屏幕空间反射算法以及在URP中的实现(一)》。
二分搜索
核心代码实现:
// 屏幕空间RayMarching判断
bool ScreenSpaceRayMarching(inout float2 currPosSS, inout float currInvW, float2 dPosSS, float dInvW,
float rayZ, bool isVertical, out float deltaZ, inout float2 hitUV, float2 endSS, float dir, float stepCount)
{
// 同时考虑方向和大小,用end判断步进结束
float end = endSS.x * dir;
float preZ = rayZ;
// 进行屏幕空间射线步近
UNITY_LOOP
for (int i = 0; i < stepCount && currPosSS.x * dir <= end; i++)
{
// 步近
currPosSS += dPosSS;
currInvW += dInvW;
// 得到步近前后两点的深度
float rayNearZ = preZ;
float rayFarZ = -1 / (dInvW * 0.5 + currInvW);
preZ = rayFarZ;
// 视图空间中相机朝向 -Z 方向
if (rayNearZ < rayFarZ) Swap(rayNearZ, rayFarZ);
// 得到交点uv
hitUV = isVertical ? currPosSS.yx : currPosSS;// 复原
hitUV *= _ScreenSize.zw;
if (any(hitUV < 0.0) || any(hitUV > 1.0)) returnfalse;
float surfaceZ = -LinearEyeDepth(SampleSceneDepth(hitUV), _ZBufferParams);
deltaZ = abs(surfaceZ - rayNearZ);
if (rayFarZ <= surfaceZ) returntrue;
}
returnfalse;
}
// 二分搜索
bool BinarySearchRayMarching(float3 posVS, float3 reflDirVS, float3 normalVS, inout float2 hitUV,
float stepCount, float thickness, float stride, float rayOffset, float maxDistance, float binaryCount)
{
// 视图空间中射线起始位置和结束位置
float3 startVS = posVS;
float endVSZ = posVS.z + reflDirVS.z * maxDistance;
if (endVSZ > -_ProjectionParams.y)
maxDistance = (-_ProjectionParams.y - startVS.z) / reflDirVS.z;
float3 endVS = startVS + reflDirVS * maxDistance;
// inverse w
// w = -Vz , 为了后面方便在屏幕空间插值深度
float startInvW = 0;
float endInvW = 0;
// 屏幕空间中射线起始位置和结束位置
// 其实 startSS = uv * _ScreenSize.xy ([0, 1]^2 -> [0, w-1],[0, h-1])
// 但是为了拿到裁剪空间的w分量, 我们必须使用自定义的 TransformViewToScreen函数
float2 startSS = TransformViewToScreen(startVS, _ScreenSize.xy, startInvW);
float2 endSS = TransformViewToScreen(endVS, _ScreenSize.xy, endInvW);
// DDA算法
float2 deltaSS = endSS - startSS; // 注意方向性!
bool isVertical = false;
// 我们默认的是以横轴为主导向,如果纵轴为主导向,则进行反转,并在之后进行复原
if (abs(deltaSS.x) < abs(deltaSS.y))
{
isVertical = true;
deltaSS = deltaSS.yx;
startSS = startSS.yx;
endSS = endSS.yx;
}
// 计算屏幕坐标、inverse-w的线性增量
float dir = sign(deltaSS.x); // 屏幕空间增量方向
float invdx = dir / deltaSS.x; // 1 / 屏幕空间增量 (绝对值)
float2 dPosSS = float2(dir, invdx * deltaSS.y);
float dInvW = (endInvW - startInvW) * invdx;
// 步长的调整
dPosSS *= stride;
dInvW *= stride;
// 存在自反射,需要加一定的偏移
float3 rayVS = posVS + normalVS * rayOffset;
// 起始数据
float2 currPosSS = startSS;
float currInvW = startInvW;
float deltaZ = 0;
float rayZ = rayVS.z;
UNITY_LOOP
for (int i = 0; i < binaryCount; i++)
{
currPosSS += dPosSS;
currInvW += dInvW;
if (ScreenSpaceRayMarching(currPosSS, currInvW, dPosSS, dInvW, rayZ, isVertical, deltaZ, hitUV, endSS, dir, stepCount))
{
if (deltaZ < thickness) returntrue;
currPosSS -= dPosSS;
currInvW -= dInvW;
rayZ = -1 / currInvW;
dPosSS *= 0.5;
dInvW *= 0.5;
}
else
{
returnfalse;
}
}
returnfalse;
}
二分搜索核心在于回溯并减半步长,当我们在屏幕空间步进到一处恰好被遮挡的地方时,我们进行一次判断,如果这个被遮挡的深度与反射点的深度差值DeltaZ大于我们设定的阈值Thickness,则需要返回上一步,并把我们的步长减半。
if (ScreenSpaceRayMarching(currPosSS, currInvW, dPosSS, dInvW, rayZ, isVertical, deltaZ, hitUV, endSS, dir, stepCount))
{
if (deltaZ < thickness) return true;
currPosSS -= dPosSS;
currInvW -= dInvW;
rayZ = - 1 / currInvW;
dPosSS *= 0.5;
dInvW *= 0.5;
}
参数:

效果:

Debug

效果

没有使用法线贴图

使用法线贴图
1. 对比
相同参数下,对比屏幕空间RayMarching。
步长Stride较大的情况下:


没有二分搜索的屏幕空间RayMarching
二分搜索的屏幕空间RayMarching。
步长Stride较小的情况下:


没有二分搜索的屏幕空间RayMarching

二分搜索的屏幕空间RayMarching
可以看到,当步长在一个很小的范围内的时候,二分搜索的优化是没有那么明显的。但是,二分搜索最关键的点在于准确,就比如说下面两张图:

没有二分搜索的屏幕空间RayMarching

二分搜索的屏幕空间RayMarching
因为屏幕没有储存其背后的信息,所以我们的墙壁上必然不应该有这个反射信息,如果没有二分搜索,就会因为跨步太大导致出现反射。
2. 问题解析
为什么相机在地面向上看的时候会有钳制范围?

原因就是RayOffset太大了,会导致这一部分的自反射和步进不协调。调整到0.01就能解决。但是RayOffset不能太小,太小会导致自反射。
3. 性能分析
参数:

静态基本上0.63ms~0.68ms,可以看出来,为了精准搜索,我们不得不增加一些开销。

那么我们有没有优化的手段呢?
Hierarchical Z-Buffer
1. 理论说明
这个Hierarchical Z-Buffer就是对深度图进行一次MipMap处理,不同的是,Hi-Z不再使用四个像素深度的平均值,而是最小值。整体思想是进行一次一次地保守估计,估计对了就迈进,估计不对就进行回溯。

来自Games 202
这里我自己总结了一下闫老师说的思路,如下图:

我在实践中发现,这个高层级被遮挡之后不是很方便对应子层级中的深度,所以我的思路是高层级被遮挡之后,会先进行一次回溯操作,之后再小步迈进,直到我们回溯到第0层。
2. 配置
首先,为了生成Hierarchical Z-Buffer,我们必须添加一个RenderFeature(RenderPass也写在这一个文件了):
using System;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Serialization;
namespaceScreenSpaceReflection.Render
{
publicclassHiZRenderFeature : ScriptableRendererFeature
{
[Serializable]
publicclassHiZSettings
{
[SerializeField] internal RenderPassEvent RenderPassEvent = RenderPassEvent.BeforeRenderingOpaques;
[SerializeField] [Range(2, 6)] internalint HiZMipCount = 6;
}
[SerializeField] private Shader m_HiZShader;
[SerializeField] private HiZSettings m_Settings = new();
private Material m_HiZMaterial;
private HiZRenderPass m_HiZRenderPass;
public override void Create()
{
m_HiZRenderPass ??= new HiZRenderPass();
m_HiZRenderPass.renderPassEvent = m_Settings.RenderPassEvent;
if(!m_HiZShader)
m_HiZShader = Shader.Find("Hidden/Universal Render Pipeline/Blit");
m_HiZMaterial = CoreUtils.CreateEngineMaterial(m_HiZShader);
m_HiZRenderPass.Create(m_HiZMaterial);
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
renderer.EnqueuePass(m_HiZRenderPass);
}
public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData)
{
m_HiZRenderPass.Setup(ref m_Settings);
}
protected override void Dispose(bool disposing)
{
m_HiZRenderPass?.Dispose();
m_HiZRenderPass?.ClearUp();
m_HiZRenderPass = null;
}
}
staticclassHiZShaderConstants
{
publicstaticreadonlystring HiZTexName = "_HiZTex";
publicstaticreadonlyint HiZMipLevelSizeID =
Shader.PropertyToID("_HiZMipLevelSize");
publicstaticreadonlyint HiZSourceMipLevelID =
Shader.PropertyToID("_HiZSourceMipLevel");
publicstaticreadonlyint HiZDestinationMipLevelID =
Shader.PropertyToID("_HiZDestinationMipLevel");
publicstaticreadonlyint MaxHiZMipLevelID =
Shader.PropertyToID("_MaxHiZMipLevel");
publicstaticreadonlyint HiZTexID =
Shader.PropertyToID("_HiZTex");
publicstaticint[] HiZTexsID;
}
classHiZRenderPass : ScriptableRenderPass
{
privateconststring m_SsrProfilingTag = "HiZRenderPass";
private ProfilingSampler m_ProfilingSampler = new(m_SsrProfilingTag + "_Sampler");
private RenderTextureDescriptor m_Descriptor;
private RenderTextureDescriptor m_HiZDesc;
private RenderTextureDescriptor[] m_HiZDescs;
private RTHandle[] m_HiZRTHandles;
private RTHandle m_HiZRTHandle;
private RTHandle m_CameraDepthTexture;
private Material m_HiZMaterial;
private HiZRenderFeature.HiZSettings m_Settings;
privateconstint m_MaxHiZMipCount = 6;
public void Create(Material material)
{
m_HiZMaterial = material;
HiZShaderConstants.HiZTexsID = newint[m_MaxHiZMipCount];
m_HiZDescs = new RenderTextureDescriptor[m_MaxHiZMipCount];
m_HiZRTHandles = new RTHandle[m_MaxHiZMipCount];
m_HiZRTHandle = RTHandles.Alloc(HiZShaderConstants.HiZTexID, name: HiZShaderConstants.HiZTexName);
for (int i = 0; i < m_MaxHiZMipCount; i++)
{
HiZShaderConstants.HiZTexsID[i] = Shader.PropertyToID(HiZShaderConstants.HiZTexName + i);
m_HiZRTHandles[i] = RTHandles.Alloc(HiZShaderConstants.HiZTexsID[i], name: HiZShaderConstants.HiZTexName + i);
}
}
public void Setup(ref HiZRenderFeature.HiZSettings settings)
{
m_Settings = settings;
}
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
m_CameraDepthTexture = renderingData.cameraData.renderer.cameraDepthTargetHandle;
m_Descriptor = renderingData.cameraData.cameraTargetDescriptor;
m_Descriptor.useMipMap = false;
m_Descriptor.autoGenerateMips = false;
var width = Math.Max((int)Math.Ceiling(Mathf.Log(m_Descriptor.width, 2) - 1.0f), 1);
var height = Math.Max((int)Math.Ceiling(Mathf.Log(m_Descriptor.height, 2) - 1.0f), 1);
width = 1 << width;
height = 1 << height;
m_HiZDesc = GetCompatibleDescriptor(width, height, GraphicsFormat.R32_SFloat);
m_HiZDesc.useMipMap = true;
m_HiZDesc.sRGB = false; // linear
m_HiZDesc.mipCount = m_Settings.HiZMipCount;
RenderingUtils.ReAllocateIfNeeded(ref m_HiZRTHandle, m_HiZDesc, FilterMode.Bilinear,
TextureWrapMode.Clamp, name: HiZShaderConstants.HiZTexName);
for (int i = 0; i < m_Settings.HiZMipCount; i++)
{
m_HiZDescs[i] = GetCompatibleDescriptor(width, height, GraphicsFormat.R32_SFloat);
// m_HiZDescs[i] = new RenderTextureDescriptor(width, height, RenderTextureFormat.RFloat, 0, 1);
// m_HiZDescs[i].msaaSamples = 1;
m_HiZDescs[i].useMipMap = false;
m_HiZDescs[i].sRGB = false; // linear
RenderingUtils.ReAllocateIfNeeded(ref m_HiZRTHandles[i], m_HiZDescs[i],
FilterMode.Bilinear, TextureWrapMode.Clamp, name: HiZShaderConstants.HiZTexName);
// generate mipmap
width = Math.Max(width / 2, 1);
height = Math.Max(height / 2, 1);
}
ConfigureTarget(renderingData.cameraData.renderer.cameraColorTargetHandle);
ConfigureClear(ClearFlag.None, Color.white);
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
CommandBuffer cmd = CommandBufferPool.Get(m_SsrProfilingTag + "_CommandBuffer");
using (new ProfilingScope(cmd, m_ProfilingSampler))
{
// mip 0
Blitter.BlitCameraTexture(cmd, m_CameraDepthTexture, m_HiZRTHandles[0]);
cmd.CopyTexture(m_HiZRTHandles[0], 0, 0, m_HiZRTHandle, 0, 0);
// mip 1~max
for (int i = 1; i < m_Settings.HiZMipCount; i++)
{
cmd.SetGlobalFloat(HiZShaderConstants.HiZSourceMipLevelID, i - 1);
cmd.SetGlobalFloat(HiZShaderConstants.HiZDestinationMipLevelID, i);
cmd.SetGlobalVector(HiZShaderConstants.HiZMipLevelSizeID,
new Vector4(m_HiZDescs[i - 1].width, m_HiZDescs[i - 1].height,
1.0f / m_HiZDescs[i - 1].width, 1.0f / m_HiZDescs[i - 1].height));
Blitter.BlitCameraTexture(cmd, m_HiZRTHandles[i - 1], m_HiZRTHandles[i], m_HiZMaterial, 0);
cmd.CopyTexture(m_HiZRTHandles[i], 0, 0, m_HiZRTHandle, 0, i);
}
cmd.SetGlobalFloat(HiZShaderConstants.MaxHiZMipLevelID, m_Settings.HiZMipCount - 1);
cmd.SetGlobalTexture(HiZShaderConstants.HiZTexName, m_HiZRTHandle);
}
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
CommandBufferPool.Release(cmd);
}
public override void OnCameraCleanup(CommandBuffer cmd)
{
}
public void Dispose()
{
foreach (var handle in m_HiZRTHandles)
handle?.Release();
m_HiZRTHandle?.Release();
}
public void ClearUp()
{
CoreUtils.Destroy(m_HiZMaterial);
}
RenderTextureDescriptor GetCompatibleDescriptor(int width, int height, GraphicsFormat format,
DepthBits depthBufferBits = DepthBits.None)
=> GetCompatibleDescriptor(m_Descriptor, width, height, format, depthBufferBits);
private static RenderTextureDescriptor GetCompatibleDescriptor(RenderTextureDescriptor desc, int width,
int height, GraphicsFormat format, DepthBits depthBufferBits = DepthBits.None)
{
desc.depthBufferBits = (int)depthBufferBits;
desc.msaaSamples = 1;
desc.width = width;
desc.height = height;
desc.graphicsFormat = format;
return desc;
}
}
}
我们HiZShader也是十分简单的:
Shader "Hidden/HiZShader"
{
SubShader
{
Tags { "RenderType" = "Opaque""RenderPipeline" = "UniversalPipeline"}
LOD 100
Pass
{
Name "HiZPass"
ZTest Always
ZWrite Off
Cull Off
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Fragment
#pragma multi_compile_fragment _ _LINEAR_TO_SRGB_CONVERSION
#pragma multi_compile_fragment _ DEBUG_DISPLAY
// Core.hlsl for XR dependencies
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
// --------- Uniform Variable ---------
SAMPLER(sampler_BlitTexture);
float _HiZSourceMipLevel;
float4 _HiZMipLevelSize;
half4 GetSource(half2 uv, float mipLevel = 0.0)
{
return SAMPLE_TEXTURE2D_X_LOD(_BlitTexture, sampler_LinearRepeat, uv, mipLevel);
}
// --------- Shading Stage ---------
half4 Fragment(Varyings input) : SV_Target
{
float2 uv = input.texcoord;
half4 nearest = half4(
GetSource(uv + float2(-1, -1) * _HiZMipLevelSize.zw, _HiZSourceMipLevel).r,
GetSource(uv + float2(-1, 1) * _HiZMipLevelSize.zw, _HiZSourceMipLevel).r,
GetSource(uv + float2( 1, -1) * _HiZMipLevelSize.zw, _HiZSourceMipLevel).r,
GetSource(uv + float2( 1, 1) * _HiZMipLevelSize.zw, _HiZSourceMipLevel).r);
// NDC 空间中, 深度从近到远, 由 1 到 0
return max(max(nearest.r, nearest.g), max(nearest.b, nearest.a));
}
ENDHLSL
}
}
}
值得注意的是,我们为了准确对齐屏幕信息,需要将深度图的Width和Height设置为2的整数次方,这样提高降采样的准确性,后续采样也较为精准地反映出区域的深度。
FrameDebug中看一下:

总共分6 Level

最高层级的Hierarchical Z Buffer

3. 核心实现
既然已经设置好了Hierarchical Z Buffer,那就开始我们的SSR Shader吧:
// HiZ RayMarching
bool HiZRayMarching(float3 posVS, float3 reflDirVS, float3 normalVS, inout float2 hitUV,
float stepCount, float thickness, float stride, float rayOffset, float maxDistance)
{
float3 startVS = posVS;
float endVSZ = posVS.z + reflDirVS.z * maxDistance;
if (endVSZ > -_ProjectionParams.y)
maxDistance = (-_ProjectionParams.y - startVS.z) / reflDirVS.z;
float3 endVS = startVS + reflDirVS * maxDistance;
float startInvW = 0;
float endInvW = 0;
float2 startSS = TransformViewToScreen(startVS, _ScreenSize.xy, startInvW);
float2 endSS = TransformViewToScreen(endVS, _ScreenSize.xy, endInvW);
float2 deltaSS = endSS - startSS;
bool isVertical = false;
if (abs(deltaSS.x) < abs(deltaSS.y))
{
isVertical = true;
deltaSS = deltaSS.yx;
startSS = startSS.yx;
endSS = endSS.yx;
}
float dir = sign(deltaSS.x);
float invdx = dir / deltaSS.x;
float2 dPosSS = float2(dir, invdx * deltaSS.y);
float dInvW = (endInvW - startInvW) * invdx;
dPosSS *= stride;
dInvW *= stride;
float3 rayVS = posVS + normalVS * rayOffset;
float2 currPosSS = startSS;
float currInvW = startInvW;
float preZ = rayVS.z;
float mipLevel = 0.0;
float end = endSS.x * dir;
UNITY_LOOP
for (int i = 0; i < stepCount && currPosSS.x * dir <= end; i++)
{
currPosSS += dPosSS * exp2(mipLevel);
currInvW += dInvW * exp2(mipLevel);
float rayNearZ = preZ;
float rayFarZ = -1 / (dInvW * exp2(mipLevel) * 0.5 + currInvW);
preZ = rayFarZ;
if (rayNearZ < rayFarZ) Swap(rayNearZ, rayFarZ);
hitUV = isVertical ? currPosSS.yx : currPosSS;
hitUV *= _ScreenSize.zw;
if (any(hitUV < 0.0) || any(hitUV > 1.0)) returnfalse;
float mipDepth = SAMPLE_TEXTURE2D_X_LOD(_HiZTex, sampler_HiZTex, hitUV, mipLevel);
float surfaceZ = -LinearEyeDepth(mipDepth, _ZBufferParams);
if (rayFarZ <= surfaceZ)
{
if (mipLevel == 0)
{
if (abs(surfaceZ - rayNearZ) < thickness) returntrue;
}
else
{
currPosSS -= dPosSS * exp2(mipLevel);
currInvW -= dInvW * exp2(mipLevel);
preZ = -1 / currInvW;
mipLevel--;
}
}
else
{
mipLevel = min(mipLevel + 1, _MaxHiZMipLevel);
}
}
returnfalse;
}
值得注意的是,我们步长是以2的指数形式增长,最多增长**2^_MaxHiZMipCount倍,这样做虽然快,但是我们也要考虑准确性,从我们的理论说明部分可以得知,这个方法会在找到大体的点之后进行回溯,会逐步精确到1像素级别**,这样我们就保证了我们的准确性。
参数:

与之前不同的是,我们的步长Stride和阈值Thickness都将使用更高的参数,这是我不断调参才出来的效果,因为如果使用原来的值会出现非常严重的断层,如果太大就会走样。
效果:

帧率一下就上来了
4. 性能分析
基本上在0.36ms~0.42ms之间,性能还是非常友好的。

注意:非全屏
结语
1. 后续可优化方向
1)使用Glossy效果进行Trick,遮挡缺失的信息。
2)使用动态切换分辨率的SSRTexture,优化性能。
3)使用Planar Reflection Probe(平面反射探针)实现全反射。
4)使用Temporal Filter(历史帧过滤)进行降噪处理。
5)法线比较高频时,转动镜头时容易闪烁,可以借助Motion Vector配合时间滤波解决闪烁问题。
6)使用TAA或MSAA等抗锯齿技术。
2. 未来展望
作为廉价的屏幕空间反射,效果上必然比不过光线追踪:

屏幕空间反射

光线追踪
但是,在实时渲染领域,光追一次耗费的性能实在是太大,即使有NVIDIA RTX的支持,我4060勉强能跑个五六十帧(GPU利用率爆到98%都是家常便饭,主要还时常卡顿),这SSR毕竟是类似于后处理的一种手段,跑个一二百帧没问题(大体估计了一下)。

参考:
Efficient GPU Screen-Space Ray Tracing
这是侑虎科技第2051篇文章,感谢作者毅毅供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)
作者主页:https://www.zhihu.com/people/20-85-47-35
再次感谢毅毅的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

