正文
Radial Blur(径向模糊)效果,是一种从屏幕中心向外呈幅射状的逐渐模糊的后期效果,一般出现在镜头高速运动的时候。
径向模糊的特点是从某个像素为中心向外辐射状扩散,因此需要采样的像素在原像素和中间点像素的连线上,不同连线上的点不会相互影响。 简单的说,就是像素的颜色是由该像素的点与中心点之间连线上进行采样(如图),然后求将这些采样点颜色的加权平均和作为该像素的颜色。[1]
本文以 UE4 的 Third Person 示例工程为例,实现 Radial Blur 效果,并进行效果动态控制和效果剔除的扩展。
后期材质
在工程的 Content 目录新建一个 Post Process 材质 M_RadialBlur,材质节点如下图:
- RadialBlur 后期材质
图中 RadialBlur
结点为 Custom 节点,Code 代码为:
static const int SceneTextureId = 14;
float2 UV = GetDefaultSceneTextureUV(Parameters, SceneTextureId);
float3 Sum = float3(0, 0, 0);
float2 Dir = float2(CenterX, CenterY) - GetViewportUV(Parameters);
for (int it = 0; it < SampleNum; it++)
{
float2 UVOffset = it * Offset * length(Dir) * Dir;
#if SHADING_PATH_MOBILE
Sum += MobileSceneTextureLookup(Parameters, SceneTextureId, UV + UVOffset).rgb;
#else
Sum += SceneTextureLookup(UV + UVOffset, SceneTextureId, false).rgb;
#endif
}
return Sum/SampleNum;
其中:
SceneTextureId
做为SceneTextureLookup
的第二个参数,值 14 表示采样对象为Post Process Input 0
。
注1:UE4 的相关贴图 Index 如下所示
注2:
Post Process Input 0
对象为当前屏幕已绘制内容,相当于 Unity 中后期回调函数void OnRenderImage(RenderTexture source, RenderTexture destination)
中source
的colorBuffer
。
-
UVOffset
用于计算采样偏移量,采样方向Dir
由中心位置减当前像素位置得到,偏移程度由length(Dir)
得到,使离中心点越远,偏移量越大。 -
SampleNum
表示采样次数,多次采样叠加的颜色除以采样次数得到最终的颜色,采样次数越高,最后得到的图像越连续。
自定义 usf 文件
Shader 代码可以直接在 Custom 节点的 Code 文本框编辑,但更合理的做法是自定义一个 usf 文件保存代码,在 Code 文本框内引用该文件,如下图所示:
- 包含 usf 文件的方式
这种写法需要 C++ 代码支持,分下面几步:
- 添加自定义的
GameModule
类,重载StartupModule
函数,添加 Virtual Shader 目录:
class FGameModule : public IModuleInterface
{
public:
virtual void StartupModule() override
{
FString ShaderDirectory = FPaths::Combine(FPaths::ProjectDir(), TEXT("Shaders"));
AddShaderSourceDirectoryMapping("/Project", ShaderDirectory);
}
virtual void ShutdownModule() override
{
ResetAllShaderSourceDirectoryMappings();
}
};
- 用自定义的
FGameModule
类替换掉默认的FDefaultGameModuleImpl
:
IMPLEMENT_PRIMARY_GAME_MODULE(FGameModule, RadialBlurDemo, "RadialBlurDemo");
- 在工程目录新建 Shaders 文件夹,放入 RadialBlur.usf 文件。
如果工程编译链接报错,须在 Build.cs 里添加依赖模块 "RenderCore"。
应用到场景
给场景添加一个 PostProcess Volume,把 RadialBlur
材质添加到 PostProcess Materials 条目下,即可把 RadialBlur 效果应用到场景:
- RadialBlur 材质场景应用效果
动态控制
在实际应用中,一般是由代码动态控制触发和关闭效果,以及后期材质动态加载,PostProcess Volume 初始化和动态设置等。
1. ARadialBlur类
新建一个继承自 AActor
的 ARadialBlur
类,重载 BeginPlay()
函数,用于后期材质动态加载及实例化,PostProcess Volume 初始化:
void ARadialBlur::BeginPlay()
{
Super::BeginPlay();
// load material and create mid
RadialBlurMat = LoadObject<UMaterial>(GetTransientPackage(), TEXT("/Game/M_RadialBlur.M_RadialBlur"));
if (RadialBlurMat != nullptr)
{
RadialBlurMID = UMaterialInstanceDynamic::Create(RadialBlurMat, this, FName("RadialBlurMID"));
}
// find post process volume
int32 num = GetWorld()->PostProcessVolumes.Num();
if (num > 0)
{
PostProcessVolumeActor = (APostProcessVolume *)(World->PostProcessVolumes[0]);
PostProcessVolumeActor->bEnabled = true;
PostProcessVolumeActor->BlendWeight = 1.0f;
PostProcessVolumeActor->bUnbound = true;
}
}
提供效果的触发接口 Trigger()
和关闭接口 Shutdown()
,主要功能是设置 PostProcess Volume 后期材质的 Blend 权重:
void ARadialBlur::Trigger()
{
if (PostProcessVolumeActor != nullptr)
{
PostProcessVolumeActor->AddOrUpdateBlendable(RadialBlurMID, 1);
}
}
void ARadialBlur::Shutdown()
{
if (PostProcessVolumeActor != nullptr)
{
PostProcessVolumeActor->AddOrUpdateBlendable(RadialBlurMID, 0);
}
}
为了能够在接下来的蓝图中调用
Trigger()
和Shutdown()
,需要在函数声明的上方加上UFUNCTION(BlueprintCallable)
。
2. 效果调用
这里用一个蓝图来生成调用 Radial Blur 效果。在工程的 Content 目录新建一个蓝图 BP_CallRaidalBlur ,节点连接如下图:
- RadialBlur 效果调用
其中,EventBeginPlay()
中调用 SpawnActorFromClassSpawn()
生成 RadialBlur ,之后调用其 Trigger()
。
实际项目中效果的触发和关闭一般通过消息机制来传递,或者提供管理类来提供调用接口。
剔除扩展
在实际应用中,一般有让某些场景物体不受后期效果影响的需求,比如全屏压黑的效果不影响主角。本文的示例用 SceneCapture2D
来 剔除主角和火焰粒子特效,使其不受 Radial Blur 的影响。
1. 创建 SceneCapture2D
在 ARadialBlur::BeginPlay()
中添加 SceneCapture2D
的创建:
// create scene capture 2d
SceneCapture2D = World->SpawnActor<ASceneCapture2D>();
FVector2D ViewportSize = FVector2D(1, 1);
if (GEngine && GEngine->GameViewport)
{
GEngine->GameViewport->GetViewportSize(ViewportSize);
}
USceneCaptureComponent2D* SceneCaptureComponent2D = SceneCapture2D->GetCaptureComponent2D();
SceneCaptureComponent2D->PrimitiveRenderMode = ESceneCapturePrimitiveRenderMode::PRM_UseShowOnlyList; ///< 关键
SceneCaptureComponent2D->TextureTarget = NewObject<UTextureRenderTarget2D>(this, TEXT("SceneCaptureTextureTarget"));
SceneCaptureComponent2D->TextureTarget->InitCustomFormat(ViewportSize.X, ViewportSize.Y, PF_A16B16G16R16, false);
SceneCaptureComponent2D->TextureTarget->ClearColor = FLinearColor::Black;
// set material RenderTexture
RadialBlurMID->SetTextureParameterValue(TEXT("RenderTexture"), SceneCaptureComponent2D->TextureTarget);
其中,设置 SceneCapture2D
的 PrimitiveRenderMode
为 PRM_UseShowOnlyList
模式,只绘制 添加进 ShowOnly 队列 的 Actor。
下面 “混合” 章节会理解这么做的原因
TextureTarget
根据当前 Viewport
的大小动态创建,并设置材质实例的 RenderTexture
参数用于最后的混合。
2. 更新 SceneCapture2D
在 ARadialBlur::Tick(float DeltaTime)
中添加:
//update scene capture 2d
APlayerCameraManager* PlayerCameraManager = nullptr;
for (TActorIterator<APlayerController> It(GetWorld()); It; ++It)
{
PlayerCameraManager = (*It)->PlayerCameraManager;
}
if (PlayerCameraManager != nullptr)
{
SceneCapture2D->SetActorLocationAndRotation(PlayerCameraManager->GetCameraLocation(), PlayerCameraManager->GetCameraRotation());
USceneCaptureComponent2D* SceneCaptureComponent2D = SceneCapture2D->GetCaptureComponent2D();
SceneCaptureComponent2D->ProjectionType = PlayerCameraManager->bIsOrthographic ? ECameraProjectionMode::Orthographic : ECameraProjectionMode::Perspective;
SceneCaptureComponent2D->FOVAngle = PlayerCameraManager->GetFOVAngle();
SceneCaptureComponent2D->OrthoWidth = PlayerCameraManager->GetOrthoWidth();
}
其中,用 APlayerController
的 PlayerCameraManager
来使 SceneCapture2D
的位置、朝向和 FOV 等设置跟主相机保持一致。
3. 设置 ShowOnlyActors 队列
在 ARadialBlur::Trigger()
中把 角色 和 粒子特效 添加进 SceneCapture2D
的 ShowOnlyActors
队列:
if (Character)
{
USkeletalMeshComponent* Mesh = Character->GetMesh();
Mesh->bOwnerNoSee = true;
Mesh->MarkRenderStateDirty();
SceneCaptureComponent2D->ShowOnlyActors.Add(Character);
}
if (Emitter)
{
Emitter->SetOwner(Character);
UParticleSystemComponent* psc = Emitter->GetParticleSystemComponent();
psc->bOwnerNoSee = true;
psc->MarkRenderStateDirty();
SceneCaptureComponent2D->ShowOnlyActors.Add(Emitter);
}
其中 ,用 bOwnerNoSee
设置 主角和粒子特效在主相机中不可见,除了效率的考虑外,还有主相机绘制剔除内容的话会影响最后的混合。
即防止做径向模糊效果时穿帮
4. 混合
SceneCapture2D
的 CaptureSource
选择默认的 "SceneColor (HDR) in RGB, Inv Opacity in A",这样 TextureTarget
的 Alpha 通道可以用来 提取出 当前径向模糊的绘制区域。
一般的叠加混合公式为:
FinalColor = SrcAlpha*SrcColor + OneMinusSrcAlpha*DstColor
其中 TextureTarget
的 RGB 通道 预乘 了 Alpha,对应公式中的 SrcAlpha*SrcColor
,TextureTarget
的 Alpha 通道存的值是 “Inv Opacity in A” 表示 Invert 的不透明度,对应 OneMinusSrcAlpha
。
后期材质连接按下图:
- RadialBlur 后期材质剔除版
- 下图左为
TextureTarget
的 Alpha 通道 - 下图右为最终剔除效果
剔除效果在 Unity 中的一般做法是额外加一个相机,设置相机的
CullingMask
单独绘制剔除内容,最后把其 RenderTarget 与主相机绘制内容做混合。UE4 的SceneCapture2D
在具体实现上与 Unity 这种做法类似。