定向体积光
最近PS5重玩大表哥2,发现他们的体积光有点棒,遂实现一下:
附YYDS大表哥2里的体积光:
一、原理
1.1 体积光散射算法(GPU Gems 3)
此算法属于上古大神的处理方式,全部在屏幕空间处理,具体参照本书13章节体积光散射算法,具体不再赘述,仅放出效果:
1.2 Ray Matching处理
本方法主要是 NVIDIA 2016年开发者大会 发布的Fast, Flexible, Physically-Based Volumetric Light Scattering算法。
总结来说,本算法就是SM的升级版本:SM仅判断物体上的点处理光照,而体积光算法则是,在计算物体上着色的时候,从视点出发,按一定的采样频率逐步进行计算空间点针对光源的可见性,从而计算最终着色。
具体示意见下图:
其中实现方向红色即为体积光作用区域,蓝色为不可见区域。
不要被式子中积分吓唬到了,到具体执行的时候都是各种近似,直接沿步长采样即可。想具体了解的可以参照原版PDF。
二、代码实现
本此算法主要是以Ray Matching的方式处理定向体积光。在场景生成后,多加一个render pass,专门来处理体积光即可,下边主要贴出此pass的shader如下:
VS: 屏幕正方形绘制:
struct VertexOut
{
float4 PosH : SV_POSITION;
float2 Tex : TEX;
};
VertexOut main(uint vI : SV_VERTEXID)
{
int2 texcoord = int2(vI & 1, vI >> 1);
VertexOut vout;
vout.Tex = float2(texcoord);
vout.PosH = float4(2 * (texcoord.x - 0.5f), -2 * (texcoord.y - 0.5f), 0.0, 1);
return vout;
}
PS: Ray Matching处理体积光
Texture2D<float> depthTx : register(t2);
Texture2D shadowDepthMap : register(t4);
struct VertexOut
{
float4 PosH : SV_POSITION;
float2 Tex : TEX;
};
// *************** DITHER 抖动算法 ***************
static const float2x2 BayerMatrix2 =
{
1.0 / 5.0, 3.0 / 5.0,
4.0 / 5.0, 2.0 / 5.0
};
static const float3x3 BayerMatrix3 =
{
3.0 / 10.0, 7.0 / 10.0, 4.0 / 10.0,
6.0 / 10.0, 1.0 / 10.0, 9.0 / 10.0,
2.0 / 10.0, 8.0 / 10.0, 5.0 / 10.0
};
static const float4x4 BayerMatrix4 =
{
1.0 / 17.0, 9.0 / 17.0, 3.0 / 17.0, 11.0 / 17.0,
13.0 / 17.0, 5.0 / 17.0, 15.0 / 17.0, 7.0 / 17.0,
4.0 / 17.0, 12.0 / 17.0, 2.0 / 17.0, 10.0 / 17.0,
16.0 / 17.0, 8.0 / 17.0, 14.0 / 17.0, 6.0 / 17.0
};
static const float BayerMatrix8[8][8] =
{
{ 1.0 / 65.0, 49.0 / 65.0, 13.0 / 65.0, 61.0 / 65.0, 4.0 / 65.0, 52.0 / 65.0, 16.0 / 65.0, 64.0 / 65.0 },
{ 33.0 / 65.0, 17.0 / 65.0, 45.0 / 65.0, 29.0 / 65.0, 36.0 / 65.0, 20.0 / 65.0, 48.0 / 65.0, 32.0 / 65.0 },
{ 9.0 / 65.0, 57.0 / 65.0, 5.0 / 65.0, 53.0 / 65.0, 12.0 / 65.0, 60.0 / 65.0, 8.0 / 65.0, 56.0 / 65.0 },
{ 41.0 / 65.0, 25.0 / 65.0, 37.0 / 65.0, 21.0 / 65.0, 44.0 / 65.0, 28.0 / 65.0, 40.0 / 65.0, 24.0 / 65.0 },
{ 3.0 / 65.0, 51.0 / 65.0, 15.0 / 65.0, 63.0 / 65.0, 2.0 / 65.0, 50.0 / 65.0, 14.0 / 65.0, 62.0 / 65.0 },
{ 35.0 / 65.0, 19.0 / 65.0, 47.0 / 65.0, 31.0 / 65.0, 34.0 / 65.0, 18.0 / 65.0, 46.0 / 65.0, 30.0 / 65.0 },
{ 11.0 / 65.0, 59.0 / 65.0, 7.0 / 65.0, 55.0 / 65.0, 10.0 / 65.0, 58.0 / 65.0, 6.0 / 65.0, 54.0 / 65.0 },
{ 43.0 / 65.0, 27.0 / 65.0, 39.0 / 65.0, 23.0 / 65.0, 42.0 / 65.0, 26.0 / 65.0, 38.0 / 65.0, 22.0 / 65.0 }
};
//bayer dithering
inline float ditherMask2(in float2 pixel)
{
return BayerMatrix2[pixel.x % 2][pixel.y % 2];
}
inline float ditherMask3(in float2 pixel)
{
return BayerMatrix3[pixel.x % 3][pixel.y % 3];
}
inline float ditherMask4(in float2 pixel)
{
return BayerMatrix4[pixel.x % 4][pixel.y % 4];
}
inline float ditherMask8(in float2 pixel)
{
return BayerMatrix8[pixel.x % 8][pixel.y % 8];
}
inline float dither(in float2 pixel)
{
return ditherMask8(pixel);
}
//other
float2 dither(float2 coord, float seed, float2 size)
{
float noiseX = ((frac(1.0 - (coord.x + seed * 1.0) * (size.x / 2.0)) * 0.25) + (frac((coord.y + seed * 2.0) * (size.y / 2.0)) * 0.75)) * 2.0 - 1.0;
float noiseY = ((frac(1.0 - (coord.x + seed * 3.0) * (size.x / 2.0)) * 0.75) + (frac((coord.y + seed * 4.0) * (size.y / 2.0)) * 0.25)) * 2.0 - 1.0;
return float2(noiseX, noiseY);
}
float2 mod_dither(float2 u)
{
float noiseX = fmod(u.x + u.y + fmod(208. + u.x * 3.58, 13. + fmod(u.y * 22.9, 9.)), 7.) * .143;
float noiseY = fmod(u.y + u.x + fmod(203. + u.y * 3.18, 12. + fmod(u.x * 27.4, 8.)), 6.) * .139;
return float2(noiseX, noiseY) * 2.0 - 1.0;
}
// *************** DITHER 抖动算法 ***************
// *************** Saturate算法 ***************
bool IsSaturated(float value)
{
return value == saturate(value);
}
bool IsSaturated(float2 value)
{
return IsSaturated(value.x) && IsSaturated(value.y);
}
bool IsSaturated(float3 value)
{
return IsSaturated(value.x) && IsSaturated(value.y) && IsSaturated(value.z);
}
bool IsSaturated(float4 value)
{
return IsSaturated(value.x) && IsSaturated(value.y) && IsSaturated(value.z) && IsSaturated(value.w);
}
// *************** Saturate算法 ***************
// SM 3x3 PCF
float CalcShadowFactor_PCF3x3(SamplerComparisonState samShadow,
Texture2D shadowMap,
float3 uvd, int smSize, float softness)
{
if (uvd.z > 1.0f)
return 1.0;
float depth = uvd.z;
const float dx = 1.0f / smSize;
float percentLit = 0.0f;
float2 offsets[9] =
{
float2(-dx, -dx), float2(0.0f, -dx), float2(dx, -dx),
float2(-dx, 0.0f), float2(0.0f, 0.0f), float2(dx, 0.0f),
float2(-dx, +dx), float2(0.0f, +dx), float2(dx, +dx)
};
[unroll]
for (int i = 0; i < 9; ++i)
{
offsets[i] = offsets[i] * float2(softness, softness);
percentLit += shadowMap.SampleCmpLevelZero(samShadow,
uvd.xy + offsets[i], depth).r;
}
return percentLit /= 9.0f;
}
//根据可见距离与雾级强度计算雾化指数参数
float ExponentialFog(float dist)
{
float fog_dist = max(dist - fog_start, 0.0);
float fog = exp(-fog_dist * fog_density);
return 1 - fog;
}
float4 main(VertexOut input) : SV_TARGET
{
//是否考虑阴影
if (current_light.casts_shadows == 0)
{
return 0;
}
//当前像素在当前视角下的场景深度
float depth = max(input.PosH.z, depthTx.SampleLevel(linear_clamp_sampler, input.Tex, 2));
//获取视空间下点位置
float3 P = GetPositionVS(input.Tex, depth);
//视空间最远点距离
float3 V = float3(0.0f, 0.0f, 0.0f) - P;
float cameraDistance = length(V);
V /= cameraDistance;
float marchedDistance = 0;
float3 accumulation = 0;
//定向光方向,后续点光源聚光灯等皆需要对应调整
const float3 L = current_light.direction.xyz;
float3 rayEnd = float3(0.0f, 0.0f, 0.0f);
//沿视线方向的采样数,影响体积光的采样质量与耗
const uint sampleCount = 16;
const float stepSize = length(P - rayEnd) / sampleCount;
// 抖动射线法来弥补采样不足:
P = P + V * stepSize * dither(input.PosH.xy);
// 执行ray match,沿着视图光线积分光量:
[loop]
for (uint i = 0; i < sampleCount; ++i)
{
//不同match下的阴影采样点世界系下的位置 shadow_matrix1:当前V的逆矩阵与sm中的VP矩阵结果
float4 posShadowMap = mul(float4(P, 1.0), shadow_matrix1);
float3 UVD = posShadowMap.xyz / posShadowMap.w;
UVD.xy = 0.5 * UVD.xy + 0.5;
UVD.y = 1.0 - UVD.y;
[branch]
if (IsSaturated(UVD.xy))
{
//处理阴影衰减作用
float attenuation = CalcShadowFactor_PCF3x3(shadow_sampler, shadowDepthMap, UVD, shadow_map_size, softness);
//处理雾级衰减作用
attenuation *= ExponentialFog(cameraDistance - marchedDistance);
accumulation += attenuation;
}
//ray match +
marchedDistance += stepSize;
P = P + V * stepSize;
}
accumulation /= sampleCount;
//定向体积光效果
return max(0, float4(accumulation * current_light.color.rgb * current_light.volumetric_strength, 1));
}
具体流程见代码注释即可,运行后可见如下效果:
叠加之前场景后整体渲染效果如下:
至于点光源与聚光灯原理相同,以后有时间了再整吧