Global Illumination_Directional Volumetric Light (定向体积光)

定向体积光

最近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));
}

具体流程见代码注释即可,运行后可见如下效果:

在这里插入图片描述

叠加之前场景后整体渲染效果如下:

在这里插入图片描述

至于点光源与聚光灯原理相同,以后有时间了再整吧

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值