Unity实现SSGI
摘要Global Illumination(GI) 即全局光照,可以简单理解成 直接光照 + 间接光照。但是一般间接光照的信息比较难计算,因为要准确知道每个观察点的半球方向上会被哪些光线照亮需要一个庞大和复杂的系统,而且光会在物体表面进行无数反弹。在PBR的章节 傻头傻脑亚古兽:Unity的URP实现PBR 中,我们介绍了计算着色点来自四面八方环境光是如何计算的,但是无法计算物体间的光照,本文将在Unity的URP管线下实现SSGI计算物体间的光照反弹。
没有SSGI的效果
开启SSGI的效果
管线
本文用的是URP的Foward前向渲染。为了实现光线无线反弹, 我们在前向lighting的部分,会把我们上一帧计算好间接光应用上,然后这一帧的间接光照通过当前帧lighting的结果来计算,建立一个无限递归的循环,这会造成间接光会有延迟的情况,但一般可以接受。具体管线如下:
lighting(直接光照 + 上一帧的间接光照) ->拷贝深度 ->重构屏幕法线 -> ssgi(计算间接光照)
光照带有延迟
实现
本文SSGI的思路比较简单,我们在着色点的半球面上,随机(每帧不同)发出n条射线,射线数量可以根据性能或者质量来选择,本文中只发出一条射线,如果该射线发生了碰撞,则用碰撞点的颜色作为入射光,最后通过时域和空间的滤波来减少噪点提高质量。
重构法线
因为我们是前向渲染,世界空间法线不能从GBuffer直接获取,所以通过深度图来还原世界空间的法线。但是直接用ddx,ddy还原出的法线,边界上会有错误的问题,所以参考了Unity内SSAO还原法线的方法
。
边缘处会有错误
改善后的效果
#define _HIGHT_QUALITY_NORMAL
half4 NormalFrag(Varyings input) : SV_Target
{
float deviceDepth = SampleSceneDepth(input.uv).r;
float3 posWS = ComputePositionWSFromUV(input.uv, deviceDepth);
#ifdef _HIGHT_QUALITY_NORMAL
float2 delta = _CameraDepthTexture_TexelSize.xy * 2.0;
// Sample the neighbour fragments
float2 lUV = float2(-delta.x, 0.0);
float2 rUV = float2(delta.x, 0.0);
float2 uUV = float2(0.0, delta.y);
float2 dUV = float2(0.0, -delta.y);
float3 l1 = float3(input.uv + lUV, 0.0);
float l1d = SampleSceneDepth(l1.xy).r;
l1.z = RawToLinearDepth(l1d); // Left1
float3 r1 = float3(input.uv + rUV, 0.0);
float r1d = SampleSceneDepth(r1.xy).r;
r1.z = RawToLinearDepth(r1d); // Right1
float3 u1 = float3(input.uv + uUV, 0.0);
float u1d = SampleSceneDepth(u1.xy).r;
u1.z = RawToLinearDepth(u1d); // Up1
float3 d1 = float3(input.uv + dUV, 0.0);
float d1d = SampleSceneDepth(d1.xy).r;
d1.z = RawToLinearDepth(d1d); // Down1
float3 l2 = float3(input.uv + lUV * 2.0, 0.0);
float l2d = SampleSceneDepth(l2.xy).r;
l2.z = RawToLinearDepth(l2d); // Left1
float3 r2 = float3(input.uv + rUV * 2.0, 0.0);
float r2d = SampleSceneDepth(r2.xy).r;
r2.z = RawToLinearDepth(r2d); // Right1
float3 u2 = float3(input.uv + uUV * 2.0, 0.0);
float u2d = SampleSceneDepth(u2.xy).r;
u2.z = RawToLinearDepth(u2d); // Up1
float3 d2 = float3(input.uv + dUV * 2.0, 0.0);
float d2d = SampleSceneDepth(d2.xy).r;
d2.z = RawToLinearDepth(d2d); // Down1
float depth = RawToLinearDepth(deviceDepth);
const uint closest_horizontal = abs((2.0 * l1.z - l2.z) - depth) < abs((2.0 * r1.z - r2.z) - depth) ? 0 : 1;
const uint closest_vertical = abs((2.0 * d1.z - d2.z) - depth) < abs((2.0 * u1.z - u2.z) - depth) ? 0 : 1;
float3 P1;
float3 P2;
if (closest_vertical == 0)
{
P1 = closest_horizontal == 0 ? float3(l1.xy, l1d) : float3(d1.xy, d1d);
P2 = closest_horizontal == 0 ? float3(d1.xy, d1d) : float3(r1.xy, r1d);
}
else
{
P1 = closest_horizontal == 0 ? float3(u1.xy, u1d) : float3(r1.xy, r1d); // u1: r1;
P2 = closest_horizontal == 0 ? float3(l1.xy, l1d) : float3(u1.xy, u1d); // l1: u1;
}
P1 = ComputePositionWSFromUV(P1.xy, P1.z);
P2 = ComputePositionWSFromUV(P2.xy, P2.z);
returnfloat4(normalize(cross(P2 - posWS, P1 - posWS)) * 0.5 + 0.5, 1);
#else
half3 T = ddy(posWS);
half3 B = ddx(posWS);
half3 N = cross(T, B);
N = normalize(N);
N = N * 0.5 + 0.5;
returnfloat4(N , 1);
#endif
}捕获入射光线
有了法线信息,现在我们从半球上随机发射一个射线,我们通过生成的随机数,然后用SampleHemisphereCosine做半球的cosine weighted重要性采样。关于随机数的生成,HDRP的SSGI使用的是A Low-Discrepancy Sampler that Distributes Monte Carlo Errors as a Blue Noise in Screen Space,这种蓝噪声能减少噪声在视觉上的影响。而为了方便,本文将使用另一种较简单方式作为来生成随机数。步进的时候可以使用HZB的方法加速,这里使用简单的方式。
float2 HashRandom(float2 p,float frameCount)
{
float3 p3 = frac(float3(p.xyx) * float3(.1031, .1030, .0973));
p3 += dot(p3, p3.yzx + 33.33);
float3 frameMagicScale = float3(2.083f, 4.867f,8.65);
p3 += frameCount * frameMagicScale;
return frac((p3.xx + p3.yz) * p3.zy);
}
蓝噪声
本文的随机函数
half4 TrackFrag(Varyings input) : SV_Target
{
float4 sceneColor = SAMPLE_TEXTURE2D(_CameraColorTexture,sampler_CameraColorTexture,input.uv);
float deviceDepth = SampleSceneDepth(input.uv).r;
float3 normalWS = SampleSceneNormals(input.uv);
float2 newSample;
uint fameIndex = _GIFrameIndex;
/*
* HDRP的随机方式
newSample.x = GetBNDSequenceSample(input.positionCS.xy, fameIndex, 0);
newSample.y = GetBNDSequenceSample(input.positionCS.xy, fameIndex, 1);
*/
newSample = HashRandom(input.positionCS.xy, fameIndex);
float3 sampleDir = SampleHemisphereCosine(newSample.x, newSample.y, normalWS);
float3 posWS = ComputePositionWSFromUV(input.uv, deviceDepth);
int MaxStep = _MaxStep;
float4 trackColor = 0;
bool isHit = false;
for (float i = 1; i < float(MaxStep); i += 1.0)
{
float3 samplePosWS = posWS +sampleDir * 0.005 * i * i * _StepScale;
float3 samplePosVS = mul(GetWorldToViewMatrix(), float4(samplePosWS, 1.0)).xyz;
float4 samplePosCS = mul(_GIProjectMatrix, float4(samplePosVS, 1.0));
samplePosCS.xyz /= samplePosCS.w;
float eyeDepth = samplePosCS.w;
float2 sampleUV = samplePosCS.xy * 0.5 + 0.5;
float sampleDepth = SampleSceneDepth(sampleUV).r;
float sampleEyeDepth = LinearEyeDepth(sampleDepth, _ZBufferParams);
float diff = (eyeDepth - sampleEyeDepth );
if (diff > 0 && abs(diff) < 0.5)
{
float3 sampleColor = SAMPLE_TEXTURE2D(_CameraColorTexture, sampler_CameraColorTexture, sampleUV.xy);
float3 sampleNormalWS = SampleSceneNormals(sampleUV.xy);
float valid = dot(normalWS, sampleNormalWS) ;
valid = valid > 0.0 ? 0.0 : 1.0;//不考虑同向的法线
half ndotl = max(0.00, dot(normalWS, sampleDir));
trackColor = float4(sampleColor, 1.0) * ndotl * valid;
isHit = true;
break;
}
}
if (isHit == false)
{
//检测不到碰撞时,使用环境光SH
trackColor =float4(SampleSH9(_AmbientSH, sampleDir), 1.0);
}
//时域上混合
#ifdef _GI_REPROJECT
float4 prePosCS = mul(_PreWorld2Project, float4(posWS, 1.0));
prePosCS.xyz /= prePosCS.w;
prePosCS.xy = prePosCS.xy * 0.5 + 0.5;
prePosCS.y = 1.0 - prePosCS.y;
float4 preTrack = SAMPLE_TEXTURE2D(_PreIrradianceMap, sampler_PreIrradianceMap, prePosCS.xy);
float zDiff = abs(LinearEyeDepth(preTrack.a, _ZBufferParams) - LinearEyeDepth(deviceDepth, _ZBufferParams));
//深度差距很大的时候减少历史帧的权重
zDiff = exp(-1.0 * zDiff);
trackColor = lerp(trackColor, preTrack, min(max(zDiff,0.00),0.95));
#endif
trackColor.a = deviceDepth;
return trackColor;
}下面是C#获取Unity环境光SH的方法
在检测不到碰撞的时候,我们直接采样环境光的SH来作间接光照,通过重投影累计多帧的结果如下
然后我们在空间上做n次滤波,我们需要考虑深度差和法线的夹角差来减少权重值。
half4 FilterFrag(Varyings input) : SV_Target
{
#ifdef _HIGHT_QUALITY
#define FILTER_COUNT 9
const float2 offsets = { float2(0.0, 0.0), float2(-1, 0.0), float2(1, 0.0), float2(0.0, -1), float2(0.0, 1),
float2(1, 1), float2(1, -1), float2(-1, 1), float2(-1, -1) };
#else
#define FILTER_COUNT 5
const float2 offsets = { float2(1.0, 0), float2(0, -1.0), float2(0,1.0), float2(-1.0,0), float2(0.0,0) };
#endif
float4 color = 0;
float w = 0;
float depth = SampleSceneDepth(input.uv).r;
float3 normalWS = SampleSceneNormals(input.uv);
for (int i = 0; i < FILTER_COUNT; i++)
{
float2 offset = _FilterTexture_TexelSize.xy * offsets * _GIFilterSize;//每次滤波使用越来越大的_GIFilterSize
float4 textureColor = SAMPLE_TEXTURE2D(_FilterTexture, sampler_FilterTexture, input.uv + offset.xy);
float deviceDepth = SampleSceneDepth(input.uv + offset.xy).r;
float3 sampleNormalWS = SampleSceneNormals(input.uv + offset.xy);
float weight =1.0 / float(FILTER_COUNT);
float diffDepth = (LinearEyeDepth(depth, _ZBufferParams)- LinearEyeDepth(textureColor.a, _ZBufferParams) );
diffDepth = abs(diffDepth) * float(-5.0) ;
diffDepth =saturate(exp(diffDepth));
float normalDiff = max(0.0,dot(normalWS, sampleNormalWS));
weight = weight * diffDepth * (normalDiff);
color.xyz += textureColor.xyz * weight;
w += weight;
}
return float4(color.xyz / max(0.001,w),depth);
}滤波后我们得出了比较干净的结果
间接光照结果
采样间接光
采样部分我们需要修改Lighting.hlsl函数,把原来采样环境SH的部分改成采样我们SSGI的结果,需要注意,我们的SSGI是在上一帧进行计算的,我们需要重投影到上一帧的屏幕uv来采样,所以我们需要把上一帧的VP矩阵也存下来。
之后我们得出了这样的结果
最终效果
复杂场景测试
无间接光照
仅环境光SH
开启SSGI
仅环境光SH
开启SSGI
总结
SSGI移动时也会很不稳定,因为SSGI只能获取屏幕空间内的信息,所以光照会随着相机移动而变化,在只有少量直接光照在屏幕内时,结果会变得不可信。但是某些情况的确可以得出不错的结果,可以考虑用在特定的场景。
本文仅供参考,很多地方需要完善。
页:
[1]