处理完定向体积光照明后,接下来继续来简单看一下光晕的生成:也是一个很简单的后处理pass,一句话来说就是在屏幕空间生成六张透明贴图叠加即可。
一、Lens Flare(镜头光晕)
Lens Flare 又称为”镜头光晕“。在游戏中镜头指的是经过视椎体做可视裁剪过后将可视物体渲染到RenderTarget中所呈现出来的画面。RenderTarget可以简单理解成当前游戏窗口的画面。光晕则指的是当镜头中存在光源时,光源会在镜头中会产生一种带有颜色的连续”光圈“特效。
从以上描述可以看出来本效果的基本思路就是:叠加六张贴图即可。因此,本效果其实最大程度还是依赖于美工给的光晕贴图。
以下是本部分试用的七张贴图:
当然你也可以试用更多的贴图来实现更好的效果,只要你能介绍带宽及显存的消耗就行。
接下来我们来说一下本算法的主要流程:
1.1 应用程序阶段
本部分就很简单了,后处理添加一个pass来专门处理光晕,在本pass中要记得把图片的混合模式设置正确即可。之后就是承上启下的设置RT皆可,不再赘述。
1.2 着色器处理
主要是在屏幕viewport上做算法处理,见下图即可:
主要分为以下几步:
- 在屏幕空间上创建对应的正方形供贴图采样:因此本部分需要在应用程序记录光源在投影空间位置信息并执行齐次坐标转换供着色器使用;
- 在顶点着色器中根据使用的贴图数量传递对应数量的数据;
- 使用几何着色器根据光源位置创建对应数量的正方形供光晕贴图使用;
- 可选:再添加一个pass专门在光源位置添加大亮度光源贴图优化效果
至此,全部流程处理完毕。
二、代码实现流程
我们主要来看一下shader代码:
2.1 顶点着色器
十分简单,不再赘述
struct VertexOut
{
float4 pos : SV_POSITION;
nointerpolation uint vid : VERTEXID;
};
VertexOut main(uint vid : SV_VERTEXID)
{
VertexOut o = (VertexOut) 0;
o.pos = 0;
o.vid = vid;
return o;
}
2.2 几何着色器
struct Light
{
float4 screen_space_position;
float4 position;
float4 direction;
float4 color;
int casts_shadows;
int use_cascades;
float volumetric_strength;
int screen_space_shadows;
};
cbuffer LightCbuf : register(b0)
{
Light current_light;
}
struct VertexOut
{
float4 pos : SV_POSITION;
nointerpolation uint vid : VERTEXID;
};
struct GeometryOut
{
float4 pos : SV_POSITION;
float3 texPos : TEXCOORD0; // 贴图坐标(xy) + 距光源位置(z)
nointerpolation uint sel : TEXCOORD1; // 记录贴图类型
nointerpolation float4 opa : TEXCOORD2; // 记录透明度用
};
SamplerState point_clamp_sampler : register(s0);
Texture2D lens0 : register(t0);
Texture2D lens1 : register(t1);
Texture2D lens2 : register(t2);
Texture2D lens3 : register(t3);
Texture2D lens4 : register(t4);
Texture2D lens5 : register(t5);
Texture2D lens6 : register(t6);
Texture2D depth_texture : register(t7);
//光源衰减方向(一维线型表达)
static const float mods[] = { 1, 0.55, 0.4, 0.1, -0.1, -0.3, -0.5 };
// 向流输出添加对应光晕贴图位置(都在屏幕空间处理):
inline void append(inout TriangleStream<GeometryOut> triStream, GeometryOut p1, uint selector, float2 posMod, float2 size)
{
//光源在viewport上的位置
float2 pos = (current_light.screen_space_position.xy - 0.5) * float2(2, -2);
//根据posMod偏移对应光晕贴图位置
float2 moddedPos = pos * posMod;
//光晕据光源位置(后续PS做衰减用)
float dis = distance(pos, moddedPos);
//根据中心偏移生成一个正方形
p1.pos.xy = moddedPos + float2(-size.x, -size.y);
p1.texPos.z = dis;
p1.sel = selector;
p1.texPos.xy = float2(0, 0);
triStream.Append(p1);
p1.pos.xy = moddedPos + float2(-size.x, size.y);
p1.texPos.xy = float2(0, 1);
triStream.Append(p1);
p1.pos.xy = moddedPos + float2(size.x, -size.y);
p1.texPos.xy = float2(1, 0);
triStream.Append(p1);
p1.pos.xy = moddedPos + float2(size.x, size.y);
p1.texPos.xy = float2(1, 1);
triStream.Append(p1);
}
[maxvertexcount(4)]
void main(
point VertexOut p[1], inout TriangleStream<GeometryOut> triStream)
{
GeometryOut p1 = (GeometryOut) 0;
// 根据原有纹理尺寸确定光晕耀斑大小,即光晕贴图四边形大小
float2 flareSize = float2(256, 256);
[branch]
switch (p[0].vid)
{
case 0:
lens0.GetDimensions(flareSize.x, flareSize.y);
break;
case 1:
lens1.GetDimensions(flareSize.x, flareSize.y);
break;
case 2:
lens2.GetDimensions(flareSize.x, flareSize.y);
break;
case 3:
lens3.GetDimensions(flareSize.x, flareSize.y);
break;
case 4:
lens4.GetDimensions(flareSize.x, flareSize.y);
break;
case 5:
lens5.GetDimensions(flareSize.x, flareSize.y);
break;
case 6:
lens6.GetDimensions(flareSize.x, flareSize.y);
break;
default:
break;
};
uint width, height, levels;
//从深度缓冲中获取原有视口尺寸
depth_texture.GetDimensions(0, width, height, levels);
float2 ScreenResolution = float2(width, height);
//光晕尺寸归一化
flareSize /= ScreenResolution;
//获取应用程序阶段处理的光源深度值
float referenceDepth = saturate(current_light.screen_space_position.z);
// 确定光晕不透明度:
const float2 step = 1.0f / ScreenResolution;
const float2 range = 10.5f * step; //可自定义10.5f
float samples = 0.0f;
float accdepth = 0.0f;
for (float y = -range.y; y <= range.y; y += step.y)
{
for (float x = -range.x; x <= range.x; x += step.x)
{
samples += 1.0f;
accdepth += depth_texture.SampleLevel(point_clamp_sampler, current_light.screen_space_position.xy + float2(x, y), 0).r >= referenceDepth - 0.001 ? 1 : 0;
}
}
accdepth /= samples;
p1.pos = float4(0, 0, 0, 1);
p1.opa = float4(accdepth, 0, 0, 0);
//根据光晕可见性,决定时候创建对应贴图
[branch]
if (accdepth > 0)
{
append(triStream, p1, p[0].vid, mods[p[0].vid], flareSize);
}
}
在几何着色器处理之后可以生成如下贴图位置:
2.3 片元着色器
struct VertextoPixel
{
float4 pos : SV_POSITION;
float3 texPos : TEXCOORD0;
nointerpolation uint sel : TEXCOORD1;
nointerpolation float4 opa : TEXCOORD2;
};
SamplerState point_clamp_sampler : register(s0);
Texture2D lens0 : register(t0);
Texture2D lens1 : register(t1);
Texture2D lens2 : register(t2);
Texture2D lens3 : register(t3);
Texture2D lens4 : register(t4);
Texture2D lens5 : register(t5);
Texture2D lens6 : register(t6);
Texture2D depth_texture : register(t7);
float4 main(VertextoPixel PSIn) : SV_TARGET
{
float4 color = 0;
[branch]
switch (PSIn.sel)
{
case 0:
color = float4(0.0f, 0.0f, 0.0f, 1.0f);
break;
case 1:
color = lens1.SampleLevel(point_clamp_sampler, PSIn.texPos.xy, 0);
break;
case 2:
color = lens2.SampleLevel(point_clamp_sampler, PSIn.texPos.xy, 0);
break;
case 3:
color = lens3.SampleLevel(point_clamp_sampler, PSIn.texPos.xy, 0);
break;
case 4:
color = lens4.SampleLevel(point_clamp_sampler, PSIn.texPos.xy, 0);
break;
case 5:
color = lens5.SampleLevel(point_clamp_sampler, PSIn.texPos.xy, 0);
break;
case 6:
color = lens6.SampleLevel(point_clamp_sampler, PSIn.texPos.xy, 0);
break;
default:
break;
};
//光晕衰减
color *= 1.1 - saturate(PSIn.texPos.z);
//光晕透明度叠加
color *= PSIn.opa.x;
return color;
}
此pass处理完之后,如下图所示:
之后你也可以选择在添加一个生成光源pass,基本与光晕pass类似,生成后如下:
以上Pass运行后,叠加原有场景便可获得光晕效果如下: