UnrealEngine5のCPU最適化テクニック

27.8K Views

December 09, 24

スライド概要

CPU最適化についてのプレゼン資料です。
AllowbatchTicks、GCの動作、FPrimitiveSceneDescについて解説しています

profile-image

Unreal Engineを開発・提供しているエピック ゲームズ ジャパンによる公式アカウントです。 勉強会や配信などで行った講演資料を公開しています。 公式サイトはこちら https://www.unrealengine.com/ja/

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

UnrealEngine5 のCPU最適化テクニック EPIC GAMES JAPAN 鈴木 孝司

2.

Tick自動バッチ Table of contents Tickのオーバーヘッドを軽減 ガベージコレクション ガベージコレクション時のヒッチを軽減 FPrimitiveSceneDesc プリミティブの直接描画

3.

Tick自動バッチ Table of contents Tickのオーバーヘッドを軽減 ガベージコレクション ガベージコレクション時のヒッチを軽減 FPrimitiveSceneDesc プリミティブの直接描画

4.

Tick自動バッチ 5.5

5.

Myth-busting “Best Practices” in Unreal Engine | Unreal Fest Seattle 2024 https://www.youtube.com/live/lchh4c9spMw?t=18451s Myth-busting “Best Practices” in Unreal Engine @ Epic Developer Community https://dev.epicgames.com/community/learning/tutorials/l3E0/myth-busting-best-practices-in-unreal-e ngine

7.

TaskGraph の動作 Actor TickFunction Component TickFunction

8.

TaskGraph の動作 Actor TaskGraph TickFunction Queue Component TickFunction

9.

TaskGraph の動作 Actor TaskGraph TickFunction Queue Component Tick Tick Tick Tick GameThread TickFunction Tick Tick Tick Tick Tick Tick Tick Tick WorkerThread WorkerThread

10.

テストプロジェクト // Sets default values AMyTickTestActor::AMyTickTestActor() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; } // 空のTick void AMyTickTestActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); } x1000 x1000

11.

大量のTickの動作

12.

Tickのオーバーヘッド

13.

Tickのオーバーヘッド Tickのオーバーヘッドは非常に小さいが0ではない ※ 0.3~0.5μs @Ryzen-3975wx DevelopmentEditor

14.

5.5 tick.AllowBatchedTicks

15.

tick.AllowBatchedTicks ● 自動でバッチ可能なTickFunctionを 一つのタスクにまとめる ● TaskGraphに詰むタイミングで判定 ● Prerequisiteも考慮される ● 条件 ○ ○ ○ HighPriority = false TickGroupを跨がない bIsBatch = true 5.5

16.

効果

17.

効果 ● 2000個のTickで0.6ms程度負荷削減 ○ おおよそ1tickあたり約0.3μs

18.

利点 注意点 自動で大量のTickを合成して タスクグラフのオーバーヘッドを軽減 ● RunOnAnyThreadが考慮されない ○ ● bIsBatchで除外 デフォルトでOFF

19.

Tick自動バッチ Table of contents Tickのオーバーヘッドを軽減 ガベージコレクション ガベージコレクション時のヒッチを軽減 FPrimitiveSceneDesc プリミティブの直接描画

20.

ガベージコレクション

21.

リリースノートより ● 5.4 UE5.4 ○ Added support for incremental Gather Unreachable Objects (post reachability analysis phase of garbage collection). It's now possible to split this phase into multiple frames to avoid hitches on slow or single-threaded platforms. ○ Added experimental support for incremental reachability analysis (a phase of garbage collection that determines which objects are to be destroyed). It's now possible to split reachability analysis into multiple frames with the specified per-frame soft time limit.

22.

ガベージコレクション ● UObject継承クラスのオブジェクトについて、 参照されなくなったものを自動的に検出してメモリ 解放する仕組み ● 半自動的に様々なタイミングでトリガーされるが しばしばヒッチやフレームレート低下の原因となり 頭を悩ませることが良くあった ● メモリ解放が暗黙的に行われることでGC前後で不 正アクセスを生み出す原因になることもある

23.

ガベージコレクションの流れ Reachability Analysis GC起動! Gather Unhash Destroy

24.

ReachabilityAnalysis ● GCの起点となるルートオブジェクトから 参照が繋がっているかを走査 接続されているものにフラグを立てていく ● オブジェクト数の増大に応じて負荷が上昇 ● VerifyGCAssumption ○ GCの処理前に前提条件を検証 ○ パッケージビルド かつ 非Shippingビルドで動作 Reachability Analysis Gather Unhash Destroy

25.

Gather Reachability Analysis ● ルートオブジェクトから 到達できないオブジェクトをリストに収集 Gather Unhash Destroy

26.

Unhash Reachability Analysis ● UObject::ConditionalBeginDestroyを 呼び出す ○ Gather RenderResource解放などの Async処理をトリガー Unhash Destroy

27.

Destory Reachability Analysis ● UObject::FinishDestroyを呼び出してメモリ を解放する Gather Unhash Destroy

28.

フルパージ vs インクリメンタルGC ● この4つのフェーズを全て1Tickで処理してしまうも のがフルパージ ○ ● GEngine->ForceGarbageCollection(true) 設定にしたがってGCを中断し複数のフレームに跨っ て処理を継続していくのがインクリメンタルGC ○ GEngine->ForceGarbageCollection(false)

29.

IncrementalGC ● GarbageCollectionが複数フレームに跨って 処理される

30.

IncrementalGC ReachabilityAnalysis

31.

IncrementalGC Gather

32.

IncrementalGC

33.

デフォルトのIncrementalGC動作 Reachability Analysis ● gc.IncrementalGCTimePerFrame 0.002 ● gc.LowMemory.MemoryThresholdMB 0 ● gc.LowMemory.IncrementalGCTimePerFrame 0.002 Gather Unhash Incremental Destroy

34.

IncrementalGCの改善(Experimental) 5.4 ● gc.AllowIncrementalReachability 0 ● gc.IncrementalReachabilityTimeLimit 0.005 ● gc.AllowIncrementalGather 0 ● 注意点 ○ ルートオブジェクトからの走査範囲によっ ては指定時間を大きく超えることがある ○ GCと並列して動作している処理のタイミ ングによっては想定していないメモリ解放 が起こる可能性がある Incremental Reachability Analysis Incremental Gather Unhash Incremental Destroy

35.

トリガー条件について パーシスタントレベル移動 FullPurge 一定時間毎のGC IncrementalGC レベルがストリームアウトした時 ※後述

36.

レベルがストリーミングアウトした時の動作 (非WorldPartition) ● s.ForceGCAfterLevelStreamedOut 1 (Default) ○ レベルがアンロードされるたびにFullPurge ● s.ForceGCAfterLevelStreamedOut 0 ○ 解放待ちのレベルが s.ContinuouslyIncrementalGCWhileLevelsPendingPurge (デフォルト値:1)で設定された値を超える度にIncrementalGC ○ ただし残メモリが少ない場合は設定値に関わらず毎回トリガーされる サブレベルのアンロードが頻繁に起こるレベル構成を用いている場合 これらのコンソール変数の調整が必要

37.

レベルがストリーミングアウトした時の動作 (WorldPartition) ● s.ForceGCAfterLevelStreamedOut が0にセットされる ○ WPセルのアンロード時にIncrementalGCがトリガーされることがある ● wp.Runtime.LevelStreamingContinuouslyIncrementalGCWhileLevelsPendingPurgeForWP ○ ○ デフォルト値 : 64 この値で s.ContinuouslyIncrementalGCWhileLevelsPendingPurgeをオーバーライドする デフォルトではアンロード待ちのWPセルが64個を超えると IncrementalGCがトリガーされる

38.

Tick自動バッチ Table of contents Tickのオーバーヘッドを軽減 ガベージコレクション ガベージコレクション時のヒッチを軽減 FPrimitiveSceneDesc プリミティブの直接描画

39.

FPrimitiveSceneDesc 5.4

40.

How Small Open Doors Can Lead to Better CPU Utilization and Bigger Games | Unreal Fest 2024 https://www.youtube.com/watch?v=JaCf2Qmvy18

41.

いくつかの変更について 既にUE5.4に取り込み済み

42.

しかし

44.

Lightweight Mesh Rendering: Render Static Mesh without Component https://godofpen.notion.site/Lightweight-Mesh-Rendering-Render-Static-Mesh-without-Component-d94 54730ab7749f4b3dfb93dde2f7bc9

45.

GameThread アクターがレンダリングされるまでの流れ RenderThread

46.

GameThread アクターがレンダリングされるまでの流れ Sp SpawnActor ee ria liz e n D Level Actor aw Component RenderThread

47.

World SpawnActor Level GameThread アクターがレンダリングされるまでの流れ Actor AddActor Component RegisterComponent RenderThread

48.

World SpawnActor GameThread アクターがレンダリングされるまでの流れ Renderer Actor CreateSceneProxy Level SceneProxy Component RenderThread

49.

World SpawnActor Level GameThread アクターがレンダリングされるまでの流れ Renderer Actor SceneProxy Component GPUへ RenderThread

50.

GameThread コンポーネントが変化した時 Renderer Actor SendAllEndOfFrameUpdates (更新 or 再作成) SceneProxy Component RenderThread

51.

World SpawnActor Level GameThread ただ描画したいだけなら・・・ Renderer Actor SceneProxy Component RenderThread

52.

直接SceneProxyを RenderThreadに渡した い!

53.

GameThread SceneProxy直接投入 Renderer SceneProxy RenderThread

54.

GameThread SceneProxy直接投入 Renderer StaticMeshCompoentを 代替するなにか SceneProxy RenderThread

55.

FPrimitiveSceneDesc 5.4

56.

関係クラス /構造体 FStaticMeshSceneProxyDesc UStaticMeshComponentからレンダリングに必要な情報だけを取り出した構造体 UStaticMeshComponentをコンストラクタに与えて生成できるほか、 独自の方法で設定してもよい FPrimitiveSceneDesc FStaticMeshSceneProxyDescへの参照をメンバに持ち さらに個々のインスタンスについてランタイムで必要な姿勢行列やバウンズなどを 保持する FPrimitiveSceneProxy いわゆるシーンプロキシーの基底クラス。 様々なコンポーネントに対応した構造体が実装されている。 FScene FPrimitiveSceneDescを受け取って処理を行うAPIを持っている UWorldを経由して取得する

57.

FSceneの関連メンバ関数 // FPrimtiveDesc version for primitive/light scene interactions virtual void AddPrimitive(FPrimitiveSceneDesc* Primitive) = 0; virtual void RemovePrimitive(FPrimitiveSceneDesc* Primitive) = 0; virtual void ReleasePrimitive(FPrimitiveSceneDesc* Primitive) = 0; virtual void UpdatePrimitiveTransform(FPrimitiveSceneDesc* Primitive) = 0; virtual void BatchAddPrimitives(TArrayView<FPrimitiveSceneDesc*> InPrimitives) = 0; virtual void BatchRemovePrimitives(TArrayView<FPrimitiveSceneDesc*> InPrimitives) = 0; virtual void BatchReleasePrimitives(TArrayView<FPrimitiveSceneDesc*> InPrimitives) = 0; virtual void UpdateCustomPrimitiveData(FPrimitiveSceneDesc* Primitive, const FCustomPrimitiveData& CustomPrimitiveData) = 0; virtual void UpdatePrimitiveInstances(FInstancedStaticMeshSceneDesc* Primitive) = 0;

58.

UStaticMeshComponent 独自のデータセット GameThread FPrimitveSceneDescを使った描画の流れ FStaticMeshSceneProxyDesc RenderThread

59.

独自のデータセット FStaticMeshSceneProxyDesc Cr ea te S ce ne P ro xy UStaticMeshComponent GameThread StaticMeshSceneProxyDesc SceneProxy RenderThread FPrimitiveSceneDesc

60.

UStaticMeshComponent GameThread StaticMeshSceneProxyDesc 独自のデータセット Renderer UWorld FStaticMeshSceneProxyDesc SceneProxy r im itive FScene FPrimitiveSceneDesc RenderThread Add P SceneProxy

61.

コンポーネントを使わない 描画コード

62.

コード例 (1/5) ● FPrimitiveSceneInfoData ○ レンダラーが書き込む変数群 ○ 描画インスタンス毎に必要 struct FSMSPSceneDesc : public FPrimitiveSceneDesc { public: FSMSPSceneDesc() { PrimitiveSceneData = &SceneInfoData; } FPrimitiveSceneInfoData SceneInfoData; };

63.
[beta]
コード例 アクター定義 (2/5)
//StaticMeshと配置情報の配列
USTRUCT(BlueprintType)
struct FStaticMeshInstance
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
UStaticMesh*
StaticMesh;
UPROPERTY()
//重いのでレベルエディタでは編集しない
TArray<FTransform> InstanceTransforms;
};

UCLASS()
class SMSPDESCSAMPLE_API ASMSPDescActor : public AActor
{
GENERATED_BODY()
public:
ASMSPDescActor();
UFUNCTION(BlueprintCallable, CallInEditor, Category = "Default")
void AddToScene();
UFUNCTION(BlueprintCallable, CallInEditor, Category = "Default")
void RemoveAllFromScene();
UFUNCTION(BlueprintCallable, CallInEditor, Category = "Default")
void SetupRandomInstances();
//ランダム配置用
UPROPERTY(Category = "Default", EditAnywhere )
bool bAutoInstanciate = false;
//メッシュおよび配置情報
UPROPERTY(EditDefaultsOnly, Category = "Default" )
TArray<FStaticMeshInstance>
Descs;
//ランタイムで利用するバッファ
TArray<FStaticMeshSceneProxyDesc>

SMSPDescs;

TArray<FSMSPInstance>
SceneDescs;
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
};

64.
[beta]
コード例 アクター定義 (2/5)
//StaticMeshと配置情報の配列
USTRUCT(BlueprintType)
struct FStaticMeshInstance
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
UStaticMesh*
StaticMesh;
UPROPERTY()
//重いのでレベルエディタでは編集しない
TArray<FTransform> InstanceTransforms;
};

FPrimitiveSceneProxyDesc配列
※StaticMesh毎

UCLASS()
class SMSPDESCSAMPLE_API ASMSPDescActor : public AActor
{
GENERATED_BODY()
public:
ASMSPDescActor();
UFUNCTION(BlueprintCallable, CallInEditor, Category = "Default")
void AddToScene();
UFUNCTION(BlueprintCallable, CallInEditor, Category = "Default")
void RemoveAllFromScene();
UFUNCTION(BlueprintCallable, CallInEditor, Category = "Default")
void SetupRandomInstances();
//ランダム配置用
UPROPERTY(Category = "Default", EditAnywhere )
bool bAutoInstanciate = false;
//メッシュおよび配置情報
UPROPERTY(EditDefaultsOnly, Category = "Default" )
TArray<FStaticMeshInstance>
Descs;
//ランタイムで利用するバッファ
TArray<FStaticMeshSceneProxyDesc>

FPrimitiveSceneDesc配列
※描画インスタンス毎

SMSPDescs;

TArray<FSMSPInstance>
SceneDescs;
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
};

65.
[beta]
コード例
描画その1 (3/5)
●

FObjectCacheEventSink
○ 分散処理させたときに
ボトルネックになるので
キューイングする

void ASMSPDescActor::AddToScene()
{
TRACE_CPUPROFILER_EVENT_SCOPE(ASMSPDescActor::AddToScene);
//テンプレート用のコンポーネント
UStaticMeshComponent*
TargetComponent = NewObject<UStaticMeshComponent>(this);
TargetComponent->RegisterComponentWithWorld(GetWorld());
FSceneInterface* Scene = GetWorld()->Scene;
int32 NumTotalInstances = 0;
FTransform RootTransform = RootComponent->GetComponentTransform();
//バッファを確保
for (const FStaticMeshInstance& Desc : Descs)
{
NumTotalInstances += Desc.InstanceTransforms.Num();
}
SceneDescs.AddDefaulted(NumTotalInstances);
SMSPDescs.Reserve(Descs.Num());
int32 DestStartIndex = 0;
#if WITH_EDITOR
FObjectCacheEventSink::BeginQueueNotifyEvents();
#endif

・・・次ページにつづく・・・

66.
[beta]
・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance& Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent->SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc& SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray<FPrimitiveSceneDesc*>BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index < StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc& SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform& Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh->GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData->SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &SceneDesc );
}
Scene->BatchAddPrimitives(TArrayView<FPrimitiveSceneDesc*>(
BatchArrayForRegistration.GetData(), BatchArrayForRegistration.Num() ));
});
DestStartIndex += Desc.InstanceTransforms.Num();
}
#if WITH_EDITOR
// Any remaining events will be flushed with this call
FObjectCacheEventSink::EndQueueNotifyEvents();
#endif

コード例
描画その2 (4/5)

67.
[beta]
・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance& Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent->SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc& SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray<FPrimitiveSceneDesc*>BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index < StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc& SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform& Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh->GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData->SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &SceneDesc );
}
Scene->BatchAddPrimitives(TArrayView<FPrimitiveSceneDesc*>(
BatchArrayForRegistration.GetData(), BatchArrayForRegistration.Num() ));
});
DestStartIndex += Desc.InstanceTransforms.Num();
}
#if WITH_EDITOR
// Any remaining events will be flushed with this call
FObjectCacheEventSink::EndQueueNotifyEvents();
#endif

コード例
描画その2 (4/5)

テンプレート用コンポーネントに
StaticMeshをセットし、そのテンプレートから
StaticMeshSceneProxyDescを作成

68.
[beta]
・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance& Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent->SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc& SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray<FPrimitiveSceneDesc*>BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index < StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc& SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform& Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh->GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData->SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &SceneDesc );
}
Scene->BatchAddPrimitives(TArrayView<FPrimitiveSceneDesc*>(
BatchArrayForRegistration.GetData(), BatchArrayForRegistration.Num() ));
});
DestStartIndex += Desc.InstanceTransforms.Num();
}
#if WITH_EDITOR
// Any remaining events will be flushed with this call
FObjectCacheEventSink::EndQueueNotifyEvents();
#endif

コード例
描画その2 (4/5)

ParallelForによる並列化が可能

69.
[beta]
・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance& Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent->SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc& SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray<FPrimitiveSceneDesc*>BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index < StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc& SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform& Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh->GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData->SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &SceneDesc );
}
Scene->BatchAddPrimitives(TArrayView<FPrimitiveSceneDesc*>(
BatchArrayForRegistration.GetData(), BatchArrayForRegistration.Num() ));
});
DestStartIndex += Desc.InstanceTransforms.Num();
}
#if WITH_EDITOR
// Any remaining events will be flushed with this call
FObjectCacheEventSink::EndQueueNotifyEvents();
#endif

コード例
描画その2 (4/5)

FOptionalTaskTagScoepは
ワーカスレッドで実行されるタスクが
どのスレッドに属しているかを指示する

70.
[beta]
・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance& Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent->SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc& SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray<FPrimitiveSceneDesc*>BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index < StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc& SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform& Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh->GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData->SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &SceneDesc );
}
Scene->BatchAddPrimitives(TArrayView<FPrimitiveSceneDesc*>(
BatchArrayForRegistration.GetData(), BatchArrayForRegistration.Num() ));
});
DestStartIndex += Desc.InstanceTransforms.Num();
}
#if WITH_EDITOR
// Any remaining events will be flushed with this call
FObjectCacheEventSink::EndQueueNotifyEvents();
#endif

コード例
描画その2 (4/5)

71.
[beta]
・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance& Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent->SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc& SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray<FPrimitiveSceneDesc*>BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index < StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc& SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform& Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh->GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData->SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &SceneDesc );
}
Scene->BatchAddPrimitives(TArrayView<FPrimitiveSceneDesc*>(
BatchArrayForRegistration.GetData(), BatchArrayForRegistration.Num() ));
});
DestStartIndex += Desc.InstanceTransforms.Num();
}
#if WITH_EDITOR
// Any remaining events will be flushed with this call
FObjectCacheEventSink::EndQueueNotifyEvents();
#endif

コード例
描画その2 (4/5)

ここでSceneProxyを作成している
CreateSceneProxy関数はこのあとに解説

72.
[beta]
・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance& Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent->SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc& SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray<FPrimitiveSceneDesc*>BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index < StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc& SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform& Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh->GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData->SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &SceneDesc );
}
Scene->BatchAddPrimitives(TArrayView<FPrimitiveSceneDesc*>(
BatchArrayForRegistration.GetData(), BatchArrayForRegistration.Num() ));
});
DestStartIndex += Desc.InstanceTransforms.Num();
}
#if WITH_EDITOR
// Any remaining events will be flushed with this call
FObjectCacheEventSink::EndQueueNotifyEvents();
#endif

コード例
描画その2 (4/5)

BatchAddPrimitiveで
複数のプリミティブを一撃で登録

73.
[beta]
コード例

CreateSceneProxy

FPrimitiveSceneProxy* ASMSPDescActor::CreateSceneProxy( const FStaticMeshSceneProxyDesc& SceneProxyDesc )
{
const UStaticMesh* StaticMesh = SceneProxyDesc.StaticMesh;
if (StaticMesh == nullptr)
{
UE_LOG(LogStaticMesh, Verbose, TEXT("Skipping CreateSceneProxy for SceneProxyDesc %s (StaticMesh is null)"), *GetFullName());
return nullptr;
}
if (StaticMesh->IsCompiling())
{
UE_LOG(LogStaticMesh, Verbose, TEXT("Skipping CreateSceneProxy for SceneProxyDesc %s (StaticMesh is not ready)"), *GetFullName());
return nullptr;
}
if (StaticMesh->GetRenderData() == nullptr)
{
UE_LOG(LogStaticMesh, Verbose, TEXT("Skipping CreateSceneProxy for SceneProxyDesc %s (RenderData is null)"), *GetFullName());
return nullptr;
}
if (!StaticMesh->GetRenderData()->IsInitialized())
{
UE_LOG(LogStaticMesh, Verbose, TEXT("Skipping CreateSceneProxy for SceneProxyDesc %s (RenderData is not initialized)"),
*GetFullName());
return nullptr;
}
Nanite::FMaterialAudit NaniteMaterials{};
// Is Nanite supported, and is there built Nanite data for this static mesh?
const bool bUseNanite = SceneProxyDesc.ShouldCreateNaniteProxy(&NaniteMaterials);
if (bUseNanite)
{
return ::new Nanite::FSceneProxy(NaniteMaterials, SceneProxyDesc, false/*Instanced*/);
}
return ::new FStaticMeshSceneProxy(SceneProxyDesc, false/*StaticLighting*/);
};

UStaticMeshComponent::CreateSceneProxy
を参考にして記述

74.

動作例

75.

比較用サンプルプロジェクト ● PCGFrameworkで配置してUnlinkしたレベル ○ StaticMeshComponentを持ったアクター x10000 ○ ISM未使用なことに注意 ● PrimitiveSceneDescで直接描画するレベル ○ 10000インスタンス

76.

パフォーマンス比較 SMCx10000

77.

パフォーマンス比較 SMCx10000

78.

パフォーマンス比較 直接描画

79.

パフォーマンス比較 直接レンダリング

80.

パフォーマンス比較 StaticMeshComponent x10000 直接描画 ロード&生成 89ms+(AsyncLoading) 37ms+(PostLoad@GT) 2.1ms (AsyncLoading) RegisterComponent 34ms+ AddPrimitive 30ms+ 12.5ms+ メモリ (Actor+Component)x10000 Actor+配置情報

81.

コリジョン 物理界には投入されないので独自にコリジョン界に投 入するか、別にコリジョンアクターを配置して代替す る HLOD 懸案事項 HLODビルダーはStaticMeshComponentを収集する のでカスタムのHLODビルダーを実装して FPrimitiveSceneDescを収集する必要がある ライティング Movable扱い ライトベイク時にも考慮されない InstancedStaticMeshComponent ISMはすでに高度に最適化されているが よりプロジェクト特化なデータ構造などを実現できる 可能性がありそう

82.

まとめ ● PrimitiveSceneDescによる直接描画はコンポーネントの処理を迂回できる ○ 省メモリ化 ○ レベルロード時のオーバーヘッドの軽減 ○ カスタマイズ ● 制限や運用時に注意しなければいけない点が増えるため、どのようなケースでも利用できるわけではな いことに注意 ○ ワークフローの構築にはかなりの実装コストを見積もる必要がある

83.

その他資料 Unreal Fest Seattle 2024 | Livestream 1, Day 1 https://www.youtube.com/watch?v=s1qdbJtjUI0 Optimizing the Game Thread | Unreal Fest 2024 https://www.youtube.com/watch?v=KxREK-DYu70

84.

Thank you! — Epic Games Japan 2024