---
title: UnrealEngine5のCPU最適化テクニック
tags:  #ue5 #unreal engine 5  
author: [エピック ゲームズ ジャパン](https://www.docswell.com/user/EpicGamesJapan)
site: [Docswell](https://www.docswell.com/)
thumbnail: https://bcdn.docswell.com/page/GJ5RRL2874.jpg?width=480
description: CPU最適化についてのプレゼン資料です。 AllowbatchTicks、GCの動作、FPrimitiveSceneDescについて解説しています
published: December 09, 24
canonical: https://www.docswell.com/s/EpicGamesJapan/ZN1982-2024-12-09-104909
---
# Page. 1

![Page Image](https://bcdn.docswell.com/page/GJ5RRL2874.jpg)

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


# Page. 2

![Page Image](https://bcdn.docswell.com/page/LE3PPLKGJ5.jpg)

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


# Page. 3

![Page Image](https://bcdn.docswell.com/page/8EDLLN3NJG.jpg)

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


# Page. 4

![Page Image](https://bcdn.docswell.com/page/V7P2254N78.jpg)

Tick自動バッチ
5.5


# Page. 5

![Page Image](https://bcdn.docswell.com/page/2JVDDPXYJQ.jpg)

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


# Page. 6

![Page Image](https://bcdn.docswell.com/page/5EGYYPVWEL.jpg)



# Page. 7

![Page Image](https://bcdn.docswell.com/page/4JQMM26Q7P.jpg)

TaskGraph の動作
Actor
TickFunction
Component
TickFunction


# Page. 8

![Page Image](https://bcdn.docswell.com/page/K74VVL4YJ1.jpg)

TaskGraph の動作
Actor
TaskGraph
TickFunction
Queue
Component
TickFunction


# Page. 9

![Page Image](https://bcdn.docswell.com/page/LJ133L4N7G.jpg)

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


# Page. 10

![Page Image](https://bcdn.docswell.com/page/GJWPPVZM72.jpg)

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


# Page. 11

![Page Image](https://bcdn.docswell.com/page/4EZKK41ME3.jpg)

大量のTickの動作


# Page. 12

![Page Image](https://bcdn.docswell.com/page/Y76QQ3L57V.jpg)

Tickのオーバーヘッド


# Page. 13

![Page Image](https://bcdn.docswell.com/page/G75RRL1GE4.jpg)

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


# Page. 14

![Page Image](https://bcdn.docswell.com/page/9J2DDL1DJR.jpg)

5.5
tick.AllowBatchedTicks


# Page. 15

![Page Image](https://bcdn.docswell.com/page/DEY22QZMEM.jpg)

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


# Page. 16

![Page Image](https://bcdn.docswell.com/page/VJN88K3378.jpg)

効果


# Page. 17

![Page Image](https://bcdn.docswell.com/page/YE9KKV9PE3.jpg)

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


# Page. 18

![Page Image](https://bcdn.docswell.com/page/GE8RRP9XJD.jpg)

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


# Page. 19

![Page Image](https://bcdn.docswell.com/page/LELKK1WNJR.jpg)

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


# Page. 20

![Page Image](https://bcdn.docswell.com/page/4JM22W9QJW.jpg)

ガベージコレクション


# Page. 21

![Page Image](https://bcdn.docswell.com/page/PJRMMYGKE9.jpg)

リリースノートより
●
5.4
UE5.4
○
Added support for incremental Gather Unreachable
Objects (post reachability analysis phase of garbage
collection). It&#039;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&#039;s
now possible to split reachability analysis into multiple
frames with the specified per-frame soft time limit.


# Page. 22

![Page Image](https://bcdn.docswell.com/page/PEXVVGX57X.jpg)

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


# Page. 23

![Page Image](https://bcdn.docswell.com/page/3EKQQ4WRED.jpg)

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


# Page. 24

![Page Image](https://bcdn.docswell.com/page/L73PPL1GE5.jpg)

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


# Page. 25

![Page Image](https://bcdn.docswell.com/page/87DLLNXNEG.jpg)

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


# Page. 26

![Page Image](https://bcdn.docswell.com/page/VJP225PNJ8.jpg)

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


# Page. 27

![Page Image](https://bcdn.docswell.com/page/2EVDDP2YEQ.jpg)

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


# Page. 28

![Page Image](https://bcdn.docswell.com/page/57GYYPRW7L.jpg)

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


# Page. 29

![Page Image](https://bcdn.docswell.com/page/4EQMM2VQJP.jpg)

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


# Page. 30

![Page Image](https://bcdn.docswell.com/page/KJ4VVLMYE1.jpg)

IncrementalGC
ReachabilityAnalysis


# Page. 31

![Page Image](https://bcdn.docswell.com/page/LE133L8NJG.jpg)

IncrementalGC
Gather


# Page. 32

![Page Image](https://bcdn.docswell.com/page/GEWPPVGMJ2.jpg)

IncrementalGC


# Page. 33

![Page Image](https://bcdn.docswell.com/page/47ZKK4LM73.jpg)

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


# Page. 34

![Page Image](https://bcdn.docswell.com/page/YJ6QQ3W5JV.jpg)

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


# Page. 35

![Page Image](https://bcdn.docswell.com/page/GJ5RRLMG74.jpg)

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


# Page. 36

![Page Image](https://bcdn.docswell.com/page/9E2DDL9DER.jpg)

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


# Page. 37

![Page Image](https://bcdn.docswell.com/page/D7Y22Q4M7M.jpg)

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


# Page. 38

![Page Image](https://bcdn.docswell.com/page/VEN88KY3J8.jpg)

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


# Page. 39

![Page Image](https://bcdn.docswell.com/page/Y79KKVPP73.jpg)

FPrimitiveSceneDesc 5.4


# Page. 40

![Page Image](https://bcdn.docswell.com/page/G78RRPDXED.jpg)

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


# Page. 41

![Page Image](https://bcdn.docswell.com/page/L7LKK1MNER.jpg)

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


# Page. 42

![Page Image](https://bcdn.docswell.com/page/4EM22WYQEW.jpg)

しかし


# Page. 43

![Page Image](https://bcdn.docswell.com/page/PERMMY9K79.jpg)



# Page. 44

![Page Image](https://bcdn.docswell.com/page/P7XVVGQ5JX.jpg)

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


# Page. 45

![Page Image](https://bcdn.docswell.com/page/37KQQ49R7D.jpg)

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


# Page. 46

![Page Image](https://bcdn.docswell.com/page/LJ3PPLWG75.jpg)

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


# Page. 47

![Page Image](https://bcdn.docswell.com/page/8JDLLNKN7G.jpg)

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


# Page. 48

![Page Image](https://bcdn.docswell.com/page/VEP225KNE8.jpg)

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


# Page. 49

![Page Image](https://bcdn.docswell.com/page/27VDDPVY7Q.jpg)

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


# Page. 50

![Page Image](https://bcdn.docswell.com/page/5JGYYPLWJL.jpg)

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


# Page. 51

![Page Image](https://bcdn.docswell.com/page/47QMM2YQEP.jpg)

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


# Page. 52

![Page Image](https://bcdn.docswell.com/page/KE4VVLWY71.jpg)

直接SceneProxyを
RenderThreadに渡した
い！


# Page. 53

![Page Image](https://bcdn.docswell.com/page/L7133LYNEG.jpg)

GameThread
SceneProxy直接投入
Renderer
SceneProxy
RenderThread


# Page. 54

![Page Image](https://bcdn.docswell.com/page/G7WPPV1ME2.jpg)

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


# Page. 55

![Page Image](https://bcdn.docswell.com/page/4JZKK4PMJ3.jpg)

FPrimitiveSceneDesc
5.4


# Page. 56

![Page Image](https://bcdn.docswell.com/page/YE6QQ3M5EV.jpg)

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


# Page. 57

![Page Image](https://bcdn.docswell.com/page/GE5RRLZGJ4.jpg)

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&lt;FPrimitiveSceneDesc*&gt; InPrimitives) = 0;
virtual void BatchRemovePrimitives(TArrayView&lt;FPrimitiveSceneDesc*&gt; InPrimitives) = 0;
virtual void BatchReleasePrimitives(TArrayView&lt;FPrimitiveSceneDesc*&gt; InPrimitives) = 0;
virtual void UpdateCustomPrimitiveData(FPrimitiveSceneDesc* Primitive,
const FCustomPrimitiveData&amp; CustomPrimitiveData) = 0;
virtual void UpdatePrimitiveInstances(FInstancedStaticMeshSceneDesc* Primitive) = 0;


# Page. 58

![Page Image](https://bcdn.docswell.com/page/972DDLRD7R.jpg)

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


# Page. 59

![Page Image](https://bcdn.docswell.com/page/DJY22QDMJM.jpg)

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


# Page. 60

![Page Image](https://bcdn.docswell.com/page/V7N88K63E8.jpg)

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


# Page. 61

![Page Image](https://bcdn.docswell.com/page/YJ9KKVLPJ3.jpg)

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


# Page. 62

![Page Image](https://bcdn.docswell.com/page/GJ8RRPXX7D.jpg)

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


# Page. 63

![Page Image](https://bcdn.docswell.com/page/LJLKK18N7R.jpg)

コード例 アクター定義 (2/5)
//StaticMeshと配置情報の配列
USTRUCT(BlueprintType)
struct FStaticMeshInstance
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
UStaticMesh*
StaticMesh;
UPROPERTY()
//重いのでレベルエディタでは編集しない
TArray&lt;FTransform&gt; InstanceTransforms;
};
UCLASS()
class SMSPDESCSAMPLE_API ASMSPDescActor : public AActor
{
GENERATED_BODY()
public:
ASMSPDescActor();
UFUNCTION(BlueprintCallable, CallInEditor, Category = &quot;Default&quot;)
void AddToScene();
UFUNCTION(BlueprintCallable, CallInEditor, Category = &quot;Default&quot;)
void RemoveAllFromScene();
UFUNCTION(BlueprintCallable, CallInEditor, Category = &quot;Default&quot;)
void SetupRandomInstances();
//ランダム配置用
UPROPERTY(Category = &quot;Default&quot;, EditAnywhere )
bool bAutoInstanciate = false;
//メッシュおよび配置情報
UPROPERTY(EditDefaultsOnly, Category = &quot;Default&quot; )
TArray&lt;FStaticMeshInstance&gt;
Descs;
//ランタイムで利用するバッファ
TArray&lt;FStaticMeshSceneProxyDesc&gt;
SMSPDescs;
TArray&lt;FSMSPInstance&gt;
SceneDescs;
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
};


# Page. 64

![Page Image](https://bcdn.docswell.com/page/47M22W6Q7W.jpg)

コード例 アクター定義 (2/5)
//StaticMeshと配置情報の配列
USTRUCT(BlueprintType)
struct FStaticMeshInstance
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
UStaticMesh*
StaticMesh;
UPROPERTY()
//重いのでレベルエディタでは編集しない
TArray&lt;FTransform&gt; InstanceTransforms;
};
FPrimitiveSceneProxyDesc配列
※StaticMesh毎
UCLASS()
class SMSPDESCSAMPLE_API ASMSPDescActor : public AActor
{
GENERATED_BODY()
public:
ASMSPDescActor();
UFUNCTION(BlueprintCallable, CallInEditor, Category = &quot;Default&quot;)
void AddToScene();
UFUNCTION(BlueprintCallable, CallInEditor, Category = &quot;Default&quot;)
void RemoveAllFromScene();
UFUNCTION(BlueprintCallable, CallInEditor, Category = &quot;Default&quot;)
void SetupRandomInstances();
//ランダム配置用
UPROPERTY(Category = &quot;Default&quot;, EditAnywhere )
bool bAutoInstanciate = false;
//メッシュおよび配置情報
UPROPERTY(EditDefaultsOnly, Category = &quot;Default&quot; )
TArray&lt;FStaticMeshInstance&gt;
Descs;
//ランタイムで利用するバッファ
TArray&lt;FStaticMeshSceneProxyDesc&gt;
FPrimitiveSceneDesc配列
※描画インスタンス毎
SMSPDescs;
TArray&lt;FSMSPInstance&gt;
SceneDescs;
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
};


# Page. 65

![Page Image](https://bcdn.docswell.com/page/P7RMMYPKJ9.jpg)

コード例
描画その1 (3/5)
●
FObjectCacheEventSink
○ 分散処理させたときに
ボトルネックになるので
キューイングする
void ASMSPDescActor::AddToScene()
{
TRACE_CPUPROFILER_EVENT_SCOPE(ASMSPDescActor::AddToScene);
//テンプレート用のコンポーネント
UStaticMeshComponent*
TargetComponent = NewObject&lt;UStaticMeshComponent&gt;(this);
TargetComponent-&gt;RegisterComponentWithWorld(GetWorld());
FSceneInterface* Scene = GetWorld()-&gt;Scene;
int32 NumTotalInstances = 0;
FTransform RootTransform = RootComponent-&gt;GetComponentTransform();
//バッファを確保
for (const FStaticMeshInstance&amp; Desc : Descs)
{
NumTotalInstances += Desc.InstanceTransforms.Num();
}
SceneDescs.AddDefaulted(NumTotalInstances);
SMSPDescs.Reserve(Descs.Num());
int32 DestStartIndex = 0;
#if WITH_EDITOR
FObjectCacheEventSink::BeginQueueNotifyEvents();
#endif
・・・次ページにつづく・・・


# Page. 66

![Page Image](https://bcdn.docswell.com/page/PJXVVG35EX.jpg)

・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance&amp; Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent-&gt;SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc&amp; SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&amp;](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray&lt;FPrimitiveSceneDesc*&gt;BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index &lt; StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc&amp; SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform&amp; Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &amp;SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh-&gt;GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData-&gt;SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &amp;SceneDesc );
}
Scene-&gt;BatchAddPrimitives(TArrayView&lt;FPrimitiveSceneDesc*&gt;(
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)


# Page. 67

![Page Image](https://bcdn.docswell.com/page/3JKQQ4YRJD.jpg)

・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance&amp; Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent-&gt;SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc&amp; SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&amp;](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray&lt;FPrimitiveSceneDesc*&gt;BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index &lt; StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc&amp; SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform&amp; Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &amp;SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh-&gt;GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData-&gt;SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &amp;SceneDesc );
}
Scene-&gt;BatchAddPrimitives(TArrayView&lt;FPrimitiveSceneDesc*&gt;(
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を作成


# Page. 68

![Page Image](https://bcdn.docswell.com/page/LE3PPL9GJ5.jpg)

・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance&amp; Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent-&gt;SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc&amp; SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&amp;](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray&lt;FPrimitiveSceneDesc*&gt;BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index &lt; StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc&amp; SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform&amp; Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &amp;SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh-&gt;GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData-&gt;SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &amp;SceneDesc );
}
Scene-&gt;BatchAddPrimitives(TArrayView&lt;FPrimitiveSceneDesc*&gt;(
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による並列化が可能


# Page. 69

![Page Image](https://bcdn.docswell.com/page/8EDLLNGNJG.jpg)

・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance&amp; Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent-&gt;SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc&amp; SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&amp;](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray&lt;FPrimitiveSceneDesc*&gt;BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index &lt; StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc&amp; SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform&amp; Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &amp;SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh-&gt;GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData-&gt;SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &amp;SceneDesc );
}
Scene-&gt;BatchAddPrimitives(TArrayView&lt;FPrimitiveSceneDesc*&gt;(
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は
ワーカスレッドで実行されるタスクが
どのスレッドに属しているかを指示する


# Page. 70

![Page Image](https://bcdn.docswell.com/page/V7P2253N78.jpg)

・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance&amp; Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent-&gt;SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc&amp; SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&amp;](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray&lt;FPrimitiveSceneDesc*&gt;BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index &lt; StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc&amp; SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform&amp; Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &amp;SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh-&gt;GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData-&gt;SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &amp;SceneDesc );
}
Scene-&gt;BatchAddPrimitives(TArrayView&lt;FPrimitiveSceneDesc*&gt;(
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)


# Page. 71

![Page Image](https://bcdn.docswell.com/page/2JVDDP4YJQ.jpg)

・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance&amp; Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent-&gt;SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc&amp; SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&amp;](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray&lt;FPrimitiveSceneDesc*&gt;BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index &lt; StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc&amp; SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform&amp; Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &amp;SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh-&gt;GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData-&gt;SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &amp;SceneDesc );
}
Scene-&gt;BatchAddPrimitives(TArrayView&lt;FPrimitiveSceneDesc*&gt;(
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関数はこのあとに解説


# Page. 72

![Page Image](https://bcdn.docswell.com/page/5EGYYP1WEL.jpg)

・・・AddToScene関数の続き・・・
for (const FStaticMeshInstance&amp; Desc : Descs)
{
//テンプレートにメッシュを設定
TargetComponent-&gt;SetStaticMesh(Desc.StaticMesh);
FStaticMeshSceneProxyDesc&amp; SMSPDesc = SMSPDescs.Emplace_GetRef(TargetComponent);
int32 NumInstances = Desc.InstanceTransforms.Num();
const int32 BatchNum
= 10;
int32 NumLoops
= ( NumInstances + BatchNum - 1) / BatchNum;
ParallelFor(NumLoops, [&amp;](int32 LoopIndex )
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelGameThread);
const int32 StartIndex = LoopIndex * BatchNum;
const int32 NumInstanceInThisBatch = FMath::Min(NumInstances - StartIndex, BatchNum);
TArray&lt;FPrimitiveSceneDesc*&gt;BatchArrayForRegistration;
BatchArrayForRegistration.Reserve(NumInstanceInThisBatch);
for (int32 Index = StartIndex; Index &lt; StartIndex + NumInstanceInThisBatch; Index++)
{
const int32 DestIndexInSceneDescs = DestStartIndex + Index;
FSMSPSceneDesc&amp; SceneDesc
= SceneDescs[DestIndexInSceneDescs];
const FTransform&amp; Transform = Desc.InstanceTransforms[Index];
FTransform InstanceTransform= Transform * RootTransform;
SceneDesc.ProxyDesc
= &amp;SMSPDesc;
SceneDesc.LocalBounds
= Desc.StaticMesh-&gt;GetBounds();
SceneDesc.Bounds
= SceneDesc.LocalBounds.TransformBy(InstanceTransform);
SceneDesc.RenderMatrix
= InstanceTransform.ToMatrixWithScale();
SceneDesc.AttachmentRootPosition
= GetActorLocation();
SceneDesc.PrimitiveSceneData-&gt;SceneProxy = CreateSceneProxy(SMSPDesc);
BatchArrayForRegistration.Emplace( &amp;SceneDesc );
}
Scene-&gt;BatchAddPrimitives(TArrayView&lt;FPrimitiveSceneDesc*&gt;(
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で
複数のプリミティブを一撃で登録


# Page. 73

![Page Image](https://bcdn.docswell.com/page/4JQMM2DQ7P.jpg)

コード例
CreateSceneProxy
FPrimitiveSceneProxy* ASMSPDescActor::CreateSceneProxy( const FStaticMeshSceneProxyDesc&amp; SceneProxyDesc )
{
const UStaticMesh* StaticMesh = SceneProxyDesc.StaticMesh;
if (StaticMesh == nullptr)
{
UE_LOG(LogStaticMesh, Verbose, TEXT(&quot;Skipping CreateSceneProxy for SceneProxyDesc %s (StaticMesh is null)&quot;), *GetFullName());
return nullptr;
}
if (StaticMesh-&gt;IsCompiling())
{
UE_LOG(LogStaticMesh, Verbose, TEXT(&quot;Skipping CreateSceneProxy for SceneProxyDesc %s (StaticMesh is not ready)&quot;), *GetFullName());
return nullptr;
}
if (StaticMesh-&gt;GetRenderData() == nullptr)
{
UE_LOG(LogStaticMesh, Verbose, TEXT(&quot;Skipping CreateSceneProxy for SceneProxyDesc %s (RenderData is null)&quot;), *GetFullName());
return nullptr;
}
if (!StaticMesh-&gt;GetRenderData()-&gt;IsInitialized())
{
UE_LOG(LogStaticMesh, Verbose, TEXT(&quot;Skipping CreateSceneProxy for SceneProxyDesc %s (RenderData is not initialized)&quot;),
*GetFullName());
return nullptr;
}
Nanite::FMaterialAudit NaniteMaterials{};
// Is Nanite supported, and is there built Nanite data for this static mesh?
const bool bUseNanite = SceneProxyDesc.ShouldCreateNaniteProxy(&amp;NaniteMaterials);
if (bUseNanite)
{
return ::new Nanite::FSceneProxy(NaniteMaterials, SceneProxyDesc, false/*Instanced*/);
}
return ::new FStaticMeshSceneProxy(SceneProxyDesc, false/*StaticLighting*/);
};
UStaticMeshComponent::CreateSceneProxy
を参考にして記述


# Page. 74

![Page Image](https://bcdn.docswell.com/page/K74VVLZYJ1.jpg)

動作例


# Page. 75

![Page Image](https://bcdn.docswell.com/page/LJ133LRN7G.jpg)

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


# Page. 76

![Page Image](https://bcdn.docswell.com/page/GJWPPVYM72.jpg)

パフォーマンス比較
SMCx10000


# Page. 77

![Page Image](https://bcdn.docswell.com/page/4EZKK4XME3.jpg)

パフォーマンス比較
SMCx10000


# Page. 78

![Page Image](https://bcdn.docswell.com/page/Y76QQ3457V.jpg)

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


# Page. 79

![Page Image](https://bcdn.docswell.com/page/G75RRLQGE4.jpg)

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


# Page. 80

![Page Image](https://bcdn.docswell.com/page/9J2DDLPDJR.jpg)

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


# Page. 81

![Page Image](https://bcdn.docswell.com/page/DEY22Q5MEM.jpg)

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


# Page. 82

![Page Image](https://bcdn.docswell.com/page/VJN88KN378.jpg)

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


# Page. 83

![Page Image](https://bcdn.docswell.com/page/YE9KKVRPE3.jpg)

その他資料
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


# Page. 84

![Page Image](https://bcdn.docswell.com/page/GE8RRPWXJD.jpg)

Thank you!
— Epic Games Japan
2024


