LoginSignup
32
21

More than 1 year has passed since last update.

UE4 CPU負荷軽減についてのメモ

Last updated at Posted at 2020-02-11

概要

UnrealEngine のCPU負荷軽減について注意すべき項目のメモ書きです。

更新履歴

日付 内容
2020/03/31 socketがついたボーンのLODについての記述を訂正
2020/05/22 ShowMaterialSectionについての追記
2020/06/21 アニメ―ション通知のLODについて追記

環境

Windows10
Visual Studio 2017
UnrealEngine 4.22, 4.24

参考

以下を参考にさせて頂きました、ありがとうございます。

UnrealEngine公式:アクターのTick
UnrealEngine公式:動的解像度
UnrealEngine公式:アニメ―ション最適化
60fpsアクションを実現する秘訣を伝授 基礎編
60fpsアクションを実現する秘訣を伝授 解析編
UE4 BPとC++のパフォーマンス比較検証
【CEDEC 2018】CPUコストを削減して思い通りの演出を! UE4で多数のキャラクターを活かすためのテクニック
UEでゲームスレッドのCPUパフォーマンスをどう改善するか
UE4 LODs for Optimization -Beginner-
[UE4] LODを視覚化するコマンド
背景コリジョン作成においてのベストプラクティス
UE4 LODs for Optimization -Beginner-
【UE4】URO(UpdateRateOptimizations)でアニメーションの最適化

ブループリント

実装コストを無視するなら全てC++で作成したほうがいいです(VM呼び出しが減るため)。

Tick処理

[プロジェクト設定] -> [基本設定] -> [Blueprints]でデフォルトのTickを切ることができます。
Projctsetting_bp.jpg

PrimaryActorTick.bCanEverTick PrimaryComponentTick.bCanEverTick が trueなC++クラスから継承されたBPはTick可能です。
他に親クラスのメタ情報 meta =(ChildCannotTick)meta=(ChildCanTick)でも設定可能なようです。

コンソールコマンド[dumpticks]で確認可能です。(そのままだと見にくいですが)

あとは[Tick Interval]を調整して毎フレーム呼び出しをしないような調整をするのも良いと思われます。

BlueprintNativeEvent

Blueprintからの呼び出しが可能でC++側での実装も持っているメソッド。C++側では_Implementation をつけて実装する必要があります。

.h
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
void Update();
.cpp
void AMyActor::Update_Implementation()
{
// C++側の実装
}

BP側での実装がなくても実装があるかどうかの確認分の処理がかかるため重くなるので、C++のみで済むならUFUNCTIONをなくして実装すると良いそうです。

UFUNCTION,UPROPERTY

UObject数が増えるため、不要なUFUNCTION,UPROPERTYは付けない方が良いみたいです。原因としてはUObject数増加によるGCの負荷がかかります。

シーン中のオブジェクトを調べるには以下コマンドが有用です。(-alphasortはアルファベット順にソートするオプションです)

>obj list -alphasort

モデル - LOD

アニメ―ション関連処理でのCPU負荷を軽減するためのLOD関連の設定です。

デフォルトのオート設定は微妙なので LODSettings にて設定をする必要があります。
Mesh_LODSetting.jpg

LODは処理負荷軽減の効果が高いため、LOD設定は最優先で設定をすべきかと思います。

スタティックメッシュコンポーネント

最小のLODレベルが設定やLODレベルの固定ができます、重要度が低いメッシュには別途設定をすると負荷軽減が期待できます。

BPだと最小のLODレベルの設定メソッドがでませんでした。。
StaticMesh_BP_LOD.jpg

関連する UStaticMeshComponentクラスの設定

StaticMeshComponent.h

// 0の場合、LODレベルを自動選択します。 0より大きい場合、(ForcedLodModel-1)に強制します。
UPROPERTY(EditAnywhere, AdvancedDisplay, BlueprintReadOnly, Category=LOD)
int32 ForcedLodModel;

// このStaticMeshComponentを最後のフレームにレンダリングするために必要なLOD。
UPROPERTY()
int32 PreviousLODLevel;

// このコンポーネントに使用される最小のLODを指定します。
// ForcedLodModelが有効な場合、これは無視されます。
UPROPERTY(EditAnywhere, AdvancedDisplay, BlueprintReadOnly, Category=LOD, meta=(editcondition = "bOverrideMinLOD"))
int32 MinLOD;

// MinLOD設定をオーバーライドするか
UPROPERTY(EditAnywhere, AdvancedDisplay, BlueprintReadOnly, Category=LOD)
uint8 bOverrideMinLOD:1;

スケルタルメッシュコンポーネント

最小のLODレベルやLODレベルの固定が設定できます、重要度が低いメッシュには別途設定をすると負荷軽減が期待できます。

BPでも指定可能です。
SkeletalMesh_BP_LOD.jpg

関連する USkinnedMeshComponentクラスの設定

SkinnedMeshComponent.h

// 0の場合、LODレベルを自動選択します。 0より大きい場合、(ForcedLodModel-1)に強制します。
UPROPERTY(EditAnywhere, AdvancedDisplay, BlueprintReadOnly, Category=LOD)
int32 ForcedLodModel;

// このコンポーネントが使用する最小LODレベル (例えば、2に設定した場合、2 + LODモデルのみが使用されます。)これは、一定の距離にあることが知られていて、ズームインするとより良いLODレベルが必要なメッシュに使用します。
UPROPERTY(EditAnywhere, AdvancedDisplay, BlueprintReadOnly, Category=LOD)
int32 MinLodModel;

// UpdateSkelPoseによって「予測」された最高のLODレベルです。これは、ボーンが基づいて更新されたものであるため、これよりも優れたLODでのレンダリングは許可されません。
int32 PredictedLODLevel;


// メッシュのMinLODではなく、このコンポーネントの MinLodModelで指定されたMinLODの値を使用するか
UPROPERTY(EditAnywhere, AdvancedDisplay, BlueprintReadOnly, Category = LOD)
uint8 bOverrideMinLod:1;

ソケット設定における注意1

ソケットが設定されたボーンツリーはLODの削減対象にならないので、ソケットを使用する際は注意が必要です。

▼訂正
ソケットを選択 -> [Socket Parameters] -> [Force Always Animated] のチェックを外すとソケットが設定されていてもLODに影響はありませんでした。
ForceAlwaysAnimated.png
デフォルトではチェックが入っているようなので、設定に注意!

ソケット設定における注意2

ソケットや物理アセットで使われているボーンのトランスフォームはボーンLODに関係なく計算されているらしいので、多数使用する場合は注意が必要です。

メッシュのマテリアル単位で表示を切り替える

メッシュのマテリアル単位で表示 ShowMaterialSection で切り替えることができます。

showmaterialsection.jpg

C++だと以下のようなコードになります。

.cpp
int32 _MatIndex = SkeletalMeshComponent->GetMaterialIndex(TEXT("head00"));
int32 _SectionIndex = 1;
int32 _LODIndex = 0;

SkeletalMeshComponent->ShowMaterialSection(_MatIndex, _SectionIndex, false, _LODIndex);

セクションとはスケルタルメッシュビュワーで見た時の以下の項目になります。
Section.jpg

ShowMaterialSection のコードは以下の様になっています。

Components/SkinnedMeshComponent.cpp
void USkinnedMeshComponent::ShowMaterialSection(int32 MaterialID, int32 SectionIndex, bool bShow, int32 LODIndex)
{
	if (!SkeletalMesh)
	{
		// スケルタルメッシュがないので何もしない 
		return;
	}
	// このコンポーネントのLOD情報が初期化されていることを確認してください
	InitLODInfos();
	if (LODInfo.IsValidIndex(LODIndex))
	{
		const FSkeletalMeshLODInfo& SkelLODInfo = *SkeletalMesh->GetLODInfo(LODIndex);
		FSkelMeshComponentLODInfo& SkelCompLODInfo = LODInfo[LODIndex];
		TArray<bool>& HiddenMaterials = SkelCompLODInfo.HiddenMaterials;
	
		// まだ割り当てられていない場合は割り当てる
		if ( HiddenMaterials.Num() != SkeletalMesh->Materials.Num() )
		{
			// Materials.Num()は<= SkeletalMesh-> Materials.Num()でなければならないため、skeletalmeshコンポーネントを使用します
			HiddenMaterials.Empty(SkeletalMesh->Materials.Num());
			HiddenMaterials.AddZeroed(SkeletalMesh->Materials.Num());
		}
		// 有効なLODInfo LODMaterialMapがある場合は、マテリアルインデックスをルーティングします。
		int32 UseMaterialIndex = MaterialID;			
		if(SkelLODInfo.LODMaterialMap.IsValidIndex(SectionIndex) && SkelLODInfo.LODMaterialMap[SectionIndex] != INDEX_NONE)
		{
			UseMaterialIndex = SkelLODInfo.LODMaterialMap[SectionIndex];
			UseMaterialIndex = FMath::Clamp( UseMaterialIndex, 0, HiddenMaterials.Num() );
		}
		// マップされたセクションマテリアルエントリを表示/非表示としてマーク
		if (HiddenMaterials.IsValidIndex(UseMaterialIndex))
		{
			HiddenMaterials[UseMaterialIndex] = !bShow;
		}

		if ( MeshObject )
		{
			// 更新された非表示セクションのレンダリングスレッドを送信する必要があります
			FSkeletalMeshObject* InMeshObject = MeshObject;
			ENQUEUE_RENDER_COMMAND(FUpdateHiddenSectionCommand)(
				[InMeshObject, HiddenMaterials, LODIndex](FRHICommandListImmediate& RHICmdList)
			{
				InMeshObject->SetHiddenMaterials(LODIndex, HiddenMaterials);
			});
		}
	}
}

これを読むと、SectionIndexMaterialID を探す場合に使用しているようです。MaterialIDLODIndex が確定している場合は -1 で問題ないようです。

上記を利用して、ボーンのアニメ―ションを止めて(ボーンを減らして)メッシュでの切り替えで表現すると処理軽減になるケースがあるかもしれません。
(この場合、切り替え用の通知などをアニメ―ションシーケンスに仕込んだりの手間がかかりますが)

アニメ―ション

プロファイラでの[AnimGameThreadTime]はアニメ―ション関連処理の時間のようです。

BPでの毎フレーム呼び出し処理のC++への移行

BPでの処理は呼び出しコストが高いので毎フレーム呼び出すような処理はC++への移行を検討した方が良いです。

BP_UpdateAnimation.jpg

MyAnimInstance.cpp
void UMyAnimInstance::NativeUpdateAnimation(float DeltaTimeX)
{
	Super::NativeUpdateAnimation(DeltaTimeX);

	// [イベントBlueprint Update Animation]の処理をここに置く
}

マルチスレッドでのアニメ―ション更新

アニメ―ションBPの[クラス設定] -> [Optimization] の [Use Multi Threaded Animation Update]をチェックするとゲームスレッド外での処理になるようです。デフォルトではプロジェクト設定にあります。

AnimBP_Optimaization.jpg

ただしルートモーションを使用する場合はマルチスレッド更新ではなくなるので要注意です。

Visibility Based Anim Tick Option

アニメーションのTickとボーン更新頻度の設定です。
スケルタルメッシュコンポーネント の [Optimization] -> [Visibility Based Anim Tick Option] の設定を [Only Tick Pose when Rendered] にすると画面外でのボーン計算がなくなるため負荷が下がります。
ただしアニメ―ションの計算がなくなることでのアクターの挙動に影響がないようなつくりにする必要があります。(アニメーションに通知を埋め込んでいる場合など)

SkeletalMesh_Optimization.jpg

Always Tick Pose and Refresh Bones

常にアニメーションのtick、ボーンも更新。

Always Tick Pose

常にアニメーションのtick、レンダリング時のみボーンを更新。

Only Tick Montages when Not Rendered

レンダリング時のみ、アニメーションのtickしてボーンを更新。
レンダリングされない場合、モンタージュのみ更新。

OnlyTick Pose when Rendered

レンダリング時のみ、アニメーションのtickしてボーンを更新。

Enable Update Rate Optimization

[Enable Update Rate Optimization] を有効にすると距離に応じた最適化がかかります。(同時に[Display Debug Update Rate Optimizations]を有効にするとアクターのバウンディングボックスの色が変わってチェックができます)

SkeletalMesh_Optimization2.jpg

このようにバウンディングボックスの色でチェックができます。
DebugURO.jpg

C++は以下のメンバ変数で変更可能です。

SkinnedMeshComponent.h
	// Update Rate
	/** TRUEの場合、オーナーはアニメ―ションの更新および評価頻度を判断します。 AnimUpdateRateTick()を参照。 
	 * パフォーマンスに対し、フレームスキップを有効にします。(例えばスクリーンのビジビリティとサイズに基づいて) */
	UPROPERTY(EditAnywhere, AdvancedDisplay, BlueprintReadWrite, Category=Optimization)
	uint8 bEnableUpdateRateOptimizations:1;

	/** 更新レート最適化のオンスクリーンデバッグ作業を有効にします。 
	 * 赤 = 0フレームをスキップ、緑 = 1フレームをスキップ、
	 * 青 = 2フレームをスキップ、黒 = 2フレーム以上をスキップを意味します。
	 */
	UPROPERTY(EditAnywhere, AdvancedDisplay, BlueprintReadWrite, Category=Optimization)
	uint8 bDisplayDebugUpdateRateOptimizations:1;

コンソールコマンドでのスキップフレームの変更も可能です。

コンソールコマンド
強制的にUpdateRateOpitimaization を ON にする
> a.URO.Enable

スキップさせたいフレーム数の設定をする
> a.URO.ForceAnimRate [数字]

重要ではないメッシュのアニメ―ションを指定してスキップすると良いと思われます。

アニメ―ション更新のレートコントロール関連のC++コードは以下にあります。

SkinnedMeshComponent.h
	/** Delegate when AnimUpdateRateParams is created, to override its default settings. */
	FOnAnimUpdateRateParamsCreated OnAnimUpdateRateParamsCreated;

	/** Animation Update Rate optimization parameters. */
	struct FAnimUpdateRateParameters* AnimUpdateRateParams;

FAnimUpdateRateParameters は "Engine/Source/Runtime/Engine/Classes/Engine/EngineType.h" に定義されています。

EngineType.h
/*
デフォルト設定
	BaseVisibleDistanceFactorThesholds.Add(0.4f)
	BaseVisibleDistanceFactorThesholds.Add(0.2f)
意味:
0フレームスキップ:MaxDistanceFactor > 0.4f
1フレームスキップ:MaxDistanceFactor > 0.2f
2フレームスキップ:MaxDistanceFactor > 0.0f
*/

となっています。これを自前で設定する場合のコード例が以下になります。
スケルタルメッシュを取得して直接パラメータを書き換えています。

以下のコード例では距離でのフレームスキップが4段階で、非表示のスキップ数を10にしています。

.cpp
// UROを有効にする
GetSkeletalMeshComponent()->bEnableUpdateRateOptimizations = true;
// アニメ―ション更新パラメータを書き換える
auto _Params = GetSkeletalMeshComponent()->AnimUpdateRateParams;
_Params->BaseVisibleDistanceFactorThesholds.Empty();
_Params->BaseVisibleDistanceFactorThesholds.Add(0.1f);
_Params->BaseVisibleDistanceFactorThesholds.Add(0.01f);
_Params->BaseVisibleDistanceFactorThesholds.Add(0.001f);
_Params->MaxEvalRateForInterpolation = 4;	// 補間がかかるフレームスキップ数
_Params->BaseNonRenderedUpdateRate = 10;	// 非表示時のフレームスキップ数

更新レートを見る場合は以下のように取得できます。

.cpp
int32 _UpdateRate = GetSkeletalMeshComponent()->AnimUpdateRateParams->UpdateRate;

bDisplayDebugUpdateRateOptimizations での更新レートの数値を取るものです。

アニメ―ション通知(AnimationNotify)のLOD設定

アニメ―ション通知系にもLOD設定ができます。
[詳細] -> [Trigger Settings] -> [Notify Filter Type] を LOD にして、[Notify Filter LOD] にスケルタルメッシュLODレベルを指定します。指定されたレベルの数値以上で通知が発行されません。

▼LOD2から通知が発行されない設定例(LOD0, LOD1なら通常動作になります)
Notify_LOD.jpg

アニメ―ションノードのLOD設定

ボーン毎のレイヤーブレンドなどのノードにもLOD設定ができます。
[詳細] -> [Performance] -> [LOD Threshold] にLODレベルを設定します。

AnimNode_LOD.jpg

アニメ―ション通知と設定の仕方が違うので注意が必要です。

コリジョン

プロファイラの[UpdateOverlaps Time] はコリジョンチェックにかかる処理時間のようです。
コリジョンの複雑度がそのまま処理にかかるようなので、表示用メッシュをそのままコリジョンとして使う[Use Complex Collision As Simple]は要注意。
基本は「シンプルに(凸包)」「数を少なく」のようです。

また、Overlapイベントを発生させるフラグの bGenerateOverlapEvents は不要な所でもtrueになっていると負荷が増えるので要注意です。

UMG

BPがやはり重くなりがちなので処理部分はC++で書くと良いです。(実装コストが大変ですが)
あとはそもそもの表示量を減らすことや、ネイティブ化をしてみるのも検討に値するかと思います。

RetainerBox

RenderTargetを使用して描画タイミングを調整できる機能です。
[Render Rules]の[Phase]が更新タイミング、[Phase Count]更新間隔です。
これを増やすことで描画間隔を減らすことができます。(例:Phase Count = 3 なら描画フレームの3回に1回描画される)

UMG_RetainerBox.jpg

更新タイミング[Phase]を分けないと負荷が減らないことに注意。

InvalidationBox

ジオメトリをキャッシュして更新をなくす機能です。
通常では描画の更新が行われないため、任意のタイミングで更新を制御する必要があります。また、[Preformance]->[IsVolatile] のチェックを入れた Widget はキャッシュ対象から外されます。
以下のノードを使って更新を制御できます。

InvalidateCache キャッシュを破棄して再度キャッシュする
SetCanCache キャッシュの有効/無効を制御
GetCanCache キャッシュ有効/無効状態を取得

UMG_InvalidationBox.jpg

performance設定

Tick Frequency

[詳細] -> [Performance] -> [Tick Frequency] はデフォルトで[Auto]になっているのを[Never]にすると更新処理が止まり負荷が軽減します。
更新がなくても呼び出し負荷がかかるため、更新不要なUMGは設定すべきです。

UMG_TickFrequency.jpg

このメンバは privateで、変更するメソッドが見当たらないため、C++からの制御はできないようです。

UserWidget.h
private:
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Performance", meta=(AllowPrivateAccess="true"))
	EWidgetTickFrequency TickFrequency;

Is Volatile

UMG_performance.jpg

動的で変化が大きいUIの場合はここにチェックを入れてキャッシュをなくし、キャッシュのコストを抑えると有効な場合があります。

Widget.h
protected:
	/* trueの場合、ウィジェットまたはその子のジオメトリまたはレイアウト情報はキャッシュされません。
	このウィジェットがすべてのフレームを変更するが、無効化パネル内に残したい場合は、フレームごとに無効化するのではなく、
	揮発性にする必要があります。これにより、無効化パネルが実際に何もキャッシュしなくなります。*/
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Performance")
	uint8 bIsVolatile:1;

public:
	/** Sets the forced volatility of the widget. */
	UFUNCTION(BlueprintCallable, Category="Widget")
	void ForceVolatile(bool bForce);

制御用メソッド ForceVolatile があります。

メモリアロケート

Binned Allocator3 というアロケーターを使用しているようで、2に比べメモリ消費の最適化が行われていますが、CPU負荷が少しあがっているようです。
2に切り替えるには以下の設定を追加することで可能なようです。

*.Target.cs
GlobalDefinitions.Add("USE_MALLOC_BINNED3=0");

詳細はエンジンソースの
"Engine\Source\Runtime\Core\Private\Windows\WindowsPlatformMemory.cpp"
辺りを参照すると良いようです。
外部ライブラリのメモリアロケータ―を使うことも可能なようですが、プラットフォーム毎の修正が必要となります。
特定のプラットフォームだけにする場合は以下のように処理ができます。

*.Target.cs
if(Target.Platform == UnrealTargetPlatform.??? ){
 // 特定プラットフォームだけ
}

TArray

UEでの動的配列です。動的なメモリ確保をなるべくなくすことが重要です。

Slack1 のリサイズをしない

配列の要素数の上限が分かる場合は、Reserve で確保し、配列を消す場合も Empty ではなく Reset を使うと Slackのリサイズが行われないため余計なメモリ確保が起こりません。

TInlineAllocator を使う

TArray<UStaticMeshComponent*, TInlineAllocator<10>> _StaticMeshComponent;

指定サイズでの静的領域を確保します、ただし指定サイズより大きくなるとコストが増えるようなので使い方に注意が必要です。

ローカル参照時はautoで参照しない

配列データの参照を返す以下のようなメソッドがある場合、

.h
const TArray<FMiscData>& GetMiscData() const;

以下のようなローカル変数で参照をする時、

.cpp

void Exec()
{
	// autoで受ける
	auto _MiscData = GetMiscData();
	// 型を明示して受ける
	const TArray<FMiscData>& _MiscData2 = GetMiscData();
}

ローカルで受ける場合、autoではなく、型を明示したほうが処理が速いです。
プロファイラで見た所、autoで受ける際に内部で slackの変更がかかっているようです。

プラグイン

パフォーマンス改善につながりそうなプラグインです。
どれもプロジェクト毎にカスタムする必要があります。

Animation Budget Allocator

スケルタルメッシュコンポーネントのTickを動的にスキップする管理をするプラグインです。複数メッシュに予算(バジェット)を設定してその範囲を超えないように管理できます。
Animation Budget Allocator

Significance Manager

パフォーマンス管理のためのオブジェクト単位の重要度を調整/管理するプラグインです。
Significance Manager

プロファイル誘導最適化(Profile Guided Optimization : PGO)

事前準備と計測用パッケージが必要。
自動プレイ等がないとツラい?

まとめ

CPU負荷を減らすのは事前の設計と機能把握が重要であると思われます。
方法はたくさんありますが、1つ1つこまめにやっていかないと全体の処理負荷軽減にはなりません。

  1. TArray配列の内部バッファをUE用語でslackと呼ぶようです。

32
21
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
21