【转载】在 UE4 中实现 Radial Blur 效果(修订版)

1,564 阅读6分钟

原文链接:在UE4中实现Radial Blur效果 | YOung

正文

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;

其中:

  1. SceneTextureId 做为 SceneTextureLookup 的第二个参数,值 14 表示采样对象为 Post Process Input 0。

注1:UE4 的相关贴图 Index 如下所示

image.png

注2:Post Process Input 0 对象为当前屏幕已绘制内容,相当于 Unity 中后期回调函数 void OnRenderImage(RenderTexture source, RenderTexture destination) 中 source 的 colorBuffer 。

  1. UVOffset 用于计算采样偏移量,采样方向 Dir 由中心位置减当前像素位置得到,偏移程度由 length(Dir) 得到,使离中心点越远,偏移量越大。

  2. SampleNum 表示采样次数,多次采样叠加的颜色除以采样次数得到最终的颜色,采样次数越高,最后得到的图像越连续。

自定义 usf 文件

Shader 代码可以直接在 Custom 节点的 Code 文本框编辑,但更合理的做法是自定义一个 usf 文件保存代码,在 Code 文本框内引用该文件,如下图所示:

  • 包含 usf 文件的方式

这种写法需要 C++ 代码支持,分下面几步:

  1. 添加自定义的 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();
	}
};
  1. 用自定义的 FGameModule 类替换掉默认的FDefaultGameModuleImpl:
IMPLEMENT_PRIMARY_GAME_MODULE(FGameModule, RadialBlurDemo, "RadialBlurDemo");
  1. 在工程目录新建 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 这种做法类似。

完整工程代码(UE4 版本 4.22)

UE4_RadialBlurDemo

参考文献