VolumeLighting的本质就是模拟丁达尔效应,即光线在空气中的散射效果。对于游戏渲染,远景可以直接使用大气散射实现,而近景则使用RayMatching的VolumeLighting实现。
具体可以看我做的Unity实现 https://gitee.com/alienity/srp-volume-lighting,有兴趣的同学可以看每次的提交记录,基本都记录了我每次提交的改进
实现原理
VolumeLighting原理很简单,就是使用RayMatching对每一小段的光照进行积分,最后把积分的结果加到最终渲染的纹理上。
我们做的完善的点
- 做了半分辨率的VolumeLighting纹理的RayMatching积分
- 用Dither对RayMatching的起点做偏移,可以达到空间上对阶梯型的纹理做一次滤波
- 使用BilateralUpSampling基于Depth做了上采样
- 做RayMatching的时候使用3DTexture累计每一步得到的Volume积分强度,最后在绘制Transparent物体的时候直接从这张3DTexture中采样
废话不多说,我们看看代码实现
float4 RayMarch(float2 screenUV, float3 rayStart, float3 rayDir, float rayLength)
{
int stepCount = _SampleCount;
float stepDifference = 1.0f / stepCount;
float stepSize = rayLength * stepDifference;
float3 step = rayDir * stepSize;
int accVolumeWidth;
int accVolumeHeight;
int accVolumeDepth;
_3DVolumeAccumulateTexture.GetDimensions(accVolumeWidth, accVolumeHeight, accVolumeDepth);
int2 relativeXY = int2(accVolumeWidth, accVolumeHeight) * screenUV;
// 因为这里使用了半分辨率,所以计算dither的时候也手动使用半分辨率计算
uint2 screenPos = uint2(screenUV * _ScreenParams.xy * 0.5f);
uint2 ditherPos = fmod(screenPos, 4);
float offset = _dither4x4[ditherPos.x][ditherPos.y] * 0.0625f;
rayStart += step * offset;
float3 volColor = float3(0, 0, 0);
[loop]
for (int i = 0; i < stepCount; ++i)
{
float3 currPositionWS = rayStart + step * i;
// 转换到主光源空间
float4 shadowCoord = TransformWorldToShadowCoord(currPositionWS);
Light mainLight = GetMainLight(shadowCoord);
volColor += 0.1 * mainLight.color * mainLight.shadowAttenuation * mainLight.shadowAttenuation;
#ifdef _ADDITIONAL_LIGHTS
// uint pixelLightCount = GetAdditionalLightsCount();
uint pixelLightCount = _AdditionalLightsCount.x;
for (uint lightIndex = 0u; lightIndex < pixelLightCount; ++lightIndex)
{
Light light = GetAdditionalLight(lightIndex, currPositionWS, half4(1, 1, 1, 1));
volColor += 0.1 * light.color * light.distanceAttenuation * light.shadowAttenuation * light.shadowAttenuation;
}
#endif
// 计算在视椎体中的相对深度位置,使用cell向上取整
int relativeDepth = round(saturate(stepDifference*(i+1)) * accVolumeDepth);
_3DVolumeAccumulateTexture[int3(relativeXY, relativeDepth)] = volColor * stepDifference;
}
return float4(volColor * stepDifference, 1);
}
我们使用的是光栅化的方法做了RayMathcing,先把绘制的分辨率减半,
我们对每个像素都做固定次数的RayMatching,实际效果看起来也还行,毕竟Volume信息都是低频信息,同时为了避免产生阶梯的效果,我们对每个RayMatching的起点都做了Dither的偏移。通过以上两张图的对比,添加了Dither之后的效果会好很多。
half4 frag(Varyings input) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(input);
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
half2 uv = input.uv;
uint2 depthUV = uv * _ScreenParams.xy;
half deviceDepth00 = saturate(LoadSceneDepth(depthUV)+0.00001f);
half deviceDepth01 = saturate(LoadSceneDepth(depthUV+uint2(0,1))+0.00001f);
half deviceDepth10 = saturate(LoadSceneDepth(depthUV+uint2(1,0))+0.00001f);
half deviceDepth11 = saturate(LoadSceneDepth(depthUV+uint2(1,1))+0.00001f);
half2 _screenSizeFrag = _ScreenSize.zw;
half4 volumeCol00 = SAMPLE_TEXTURE2D(_HalfVolumeTex, sampler_HalfVolumeTex, uv);
half4 volumeCol01 = SAMPLE_TEXTURE2D(_HalfVolumeTex, sampler_HalfVolumeTex, uv+float2(_screenSizeFrag.x, 0));
half4 volumeCol10 = SAMPLE_TEXTURE2D(_HalfVolumeTex, sampler_HalfVolumeTex, uv+float2(0, _screenSizeFrag.y));
half4 volumeCol11 = SAMPLE_TEXTURE2D(_HalfVolumeTex, sampler_HalfVolumeTex, uv+_screenSizeFrag.xy);
half4 color = (volumeCol00 * deviceDepth00
+ volumeCol10 * deviceDepth01
+ volumeCol10 * deviceDepth10
+ volumeCol11 * deviceDepth11)
/(deviceDepth00 + deviceDepth01 + deviceDepth10 + deviceDepth11);
return color;
}
因为我们是使用半分辨率做了RayMatching,所以这里使用了BilateralUpSampling做上采样,依赖的额外信息就是Depth,因为depth的值在边界的时候差别会非常的大,有可能为0,所以为了避免上采样之后的volume值也变成0,在每个depth的值上加了一个非常小的bias。
绘制不透明物理的时候,我们只需要把Volume的纹理直接叠加到场景上就好了
#if defined(_VOLUMEFUSE_ON)
float3 positionNDC = input.positionNDC.xyz / input.positionNDC.w;
float deviceDepth = SampleSceneDepth(positionNDC.xy);
#if UNITY_REVERSED_Z
deviceDepth = 1 - deviceDepth;
#endif
float3 samplePos = float3(positionNDC.xy, deviceDepth);
float3 accVolumeColor = SAMPLE_TEXTURE3D(_3DVolumeAccumulateTexture, sampler_3DVolumeAccumulateTexture, samplePos);
color.rgb += accVolumeColor.rgb;
#endif
最后为了让透明物体绘制的时候也受到Volume的影响,我们把Volume的每一步都累积到了一张3DTexture上,最后我们需要在绘制不透明物体的时候根据深度从3DTexture中采样,就可以得到相机到当前位置累计的Volume的强度值。
引用
[1] https://github.com/SlightlyMad/VolumetricLights
[2] https://book.douban.com/subject/25738978/