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

821 阅读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)sourcecolorBuffer

  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类

新建一个继承自 AActorARadialBlur 类,重载 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);

其中,设置 SceneCapture2DPrimitiveRenderModePRM_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();
}	

其中,用 APlayerControllerPlayerCameraManager 来使 SceneCapture2D 的位置、朝向和 FOV 等设置跟主相机保持一致。

3. 设置 ShowOnlyActors 队列

ARadialBlur::Trigger() 中把 角色粒子特效 添加进 SceneCapture2DShowOnlyActors 队列:

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. 混合

SceneCapture2DCaptureSource 选择默认的 "SceneColor (HDR) in RGB, Inv Opacity in A",这样 TextureTargetAlpha 通道可以用来 提取出 当前径向模糊的绘制区域

一般的叠加混合公式为:

FinalColor = SrcAlpha*SrcColor + OneMinusSrcAlpha*DstColor

其中 TextureTargetRGB 通道 预乘 了 Alpha,对应公式中的 SrcAlpha*SrcColorTextureTargetAlpha 通道存的值是 “Inv Opacity in A” 表示 Invert 的不透明度,对应 OneMinusSrcAlpha

后期材质连接按下图:

  • RadialBlur 后期材质剔除版

  • 下图左为 TextureTargetAlpha 通道
  • 下图右为最终剔除效果

剔除效果在 Unity 中的一般做法是额外加一个相机,设置相机的 CullingMask 单独绘制剔除内容,最后把其 RenderTarget 与主相机绘制内容做混合。UE4 的 SceneCapture2D 在具体实现上与 Unity 这种做法类似。

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

UE4_RadialBlurDemo

参考文献