メインコンテンツへスキップ
Tech Playground
ゲーム開発

UE5.9 Lumen Radiosity Cache動的更新実装|メモリ効率とリアルタイムGI品質両立ガイド

UE5.9 Lumenの最新Radiosity Cache動的更新戦略を完全解説。メモリ効率を保ちながらリアルタイムグローバルイルミネーション品質を最大化する実装パターンとGPU最適化手法を詳解します。

約13分で読めます

Unreal Engine 5.9(2026年4月リリース)では、Lumenのグローバルイルミネーション(GI)システムに重要な改良が加えられました。特にRadiosity Cacheの動的更新メカニズムが大幅に強化され、動的ライト環境でのメモリ効率とレンダリング品質の両立が現実的になりました。

従来のLumen実装では、動的ライトが多いシーンでRadiosity Cacheが肥大化し、メモリオーバーヘッドがボトルネックになる問題がありました。UE5.9の新しいAdaptive Cache Update Strategyは、この課題を解決するために設計されています。

本記事では、UE5.9で導入された最新のRadiosity Cache動的更新実装を実際のプロジェクトに適用する方法、メモリ効率とGI品質のバランスを最適化する具体的な設定パターン、GPU負荷を最小化するための実装テクニックを詳解します。

UE5.9 Radiosity Cacheの動的更新メカニズム

UE5.9のRadiosity Cacheは、階層的な空間分割構造時間的コヒーレンスを活用した新しい更新戦略を採用しています。以下のダイアグラムは、新しいRadiosity Cacheの更新フローを示しています。

flowchart TD
    A["フレーム開始"] --> B["動的ライト変更検出"]
    B --> C{"影響範囲の判定"}
    C -->|局所的| D["Incremental Update"]
    C -->|広範囲| E["Hierarchical Rebuild"]
    D --> F["Cache Entry 選択的更新"]
    E --> G["Spatial Hash 再構築"]
    F --> H["Temporal Filtering"]
    G --> H
    H --> I["GPU Memory Compaction"]
    I --> J["レンダリングパイプラインへ"]

この図は、動的ライトの変更に応じてRadiosity Cacheがどのように更新されるかを示しています。局所的な変更には差分更新を、広範囲な変更には階層的再構築を適用することで、効率的なメモリ管理を実現しています。

従来の課題と改善点

UE5.7までのRadiosity Cacheは、動的ライトの変更ごとにキャッシュ全体を再計算するか、あるいは不正確なキャッシュを使い続けるかの二択でした。この結果、以下の問題が発生していました。

  • メモリ肥大化: 動的ライトが多いシーンでは、キャッシュエントリが際限なく増加
  • 更新遅延: 全体再計算により、動的ライトの変更が反映されるまでに数フレーム必要
  • 品質劣化: 古いキャッシュを使い続けると、間接光が正しく更新されない

UE5.9のAdaptive Cache Updateは、空間的・時間的な局所性を活用して、必要最小限のキャッシュエントリのみを更新します。公式ドキュメントによると、この改良により動的ライトシーンでのメモリ使用量が平均50%削減され、更新レイテンシが3フレームから1フレームに短縮されました。

実装の基本設定

UE5.9プロジェクトでRadiosity Cacheの動的更新を有効化するには、以下の設定をProject Settingsで行います。

// Config/DefaultEngine.ini
[/Script/Engine.RendererSettings]
r.Lumen.RadiosityCache.Enable=1
r.Lumen.RadiosityCache.UpdateMode=2  // 0: Static, 1: Full, 2: Adaptive (新規)
r.Lumen.RadiosityCache.MaxMemoryMB=512
r.Lumen.RadiosityCache.SpatialHashResolution=128
r.Lumen.RadiosityCache.TemporalFilterStrength=0.85

UpdateMode=2(Adaptive)が、UE5.9で新たに導入された動的更新戦略です。この設定により、Lumenは動的ライトの変更を検出し、影響範囲に応じて最適な更新戦略を自動選択します。

C++コードからも制御可能です。

// 動的更新パラメータの設定例
#include "LumenRadiosityCache.h"

void AMyGameMode::ConfigureLumenRadiosityCache()
{
    // Radiosity Cacheの動的更新設定
    auto* RadiosityCache = GetWorld()->Scene->GetLumenRadiosityCache();
    
    // Adaptive Update パラメータ
    RadiosityCache->SetUpdateMode(ELumenRadiosityCacheUpdateMode::Adaptive);
    RadiosityCache->SetMaxMemoryBudgetMB(512);
    RadiosityCache->SetSpatialHashResolution(128);
    
    // 時間的フィルタリング強度(0.0-1.0、高いほど安定、低いほど応答速度速)
    RadiosityCache->SetTemporalFilterStrength(0.85f);
    
    // Incremental Update閾値(影響範囲がこの値以下なら差分更新)
    RadiosityCache->SetIncrementalUpdateThreshold(0.25f);
}

メモリ効率最適化:Spatial Hash戦略

UE5.9のRadiosity Cacheは、Sparse Spatial Hash構造を使用してメモリ効率を大幅に向上させています。以下のダイアグラムは、Spatial Hashによるキャッシュエントリの空間管理を示しています。

graph TD
    A["World Space"] --> B["Spatial Hash Grid"]
    B --> C["Hash Bucket 0"]
    B --> D["Hash Bucket 1"]
    B --> E["Hash Bucket N"]
    C --> F["Cache Entry 1<br/>Position, Normal, Irradiance"]
    C --> G["Cache Entry 2"]
    D --> H["Cache Entry 3"]
    E --> I["Cache Entry M"]
    F --> J["GPU Memory Pool"]
    G --> J
    H --> J
    I --> J

Spatial Hashは、ワールド空間を一定サイズのグリッドに分割し、各グリッドセルにキャッシュエントリをハッシュテーブルで管理します。これにより、空間的に近いキャッシュエントリを効率的にクエリでき、動的ライト変更時の影響範囲判定が高速化されます。

Spatial Hash解像度の選択

Spatial Hashの解像度は、メモリ使用量とルックアップ性能のトレードオフを決定します。解像度が高いほど細かい粒度で管理できますが、ハッシュテーブルのオーバーヘッドが増加します。

// Spatial Hash解像度の最適化例
void OptimizeSpatialHashResolution(UWorld* World)
{
    auto* RadiosityCache = World->Scene->GetLumenRadiosityCache();
    
    // シーンのバウンディングボックスサイズに基づいて解像度を決定
    FBox SceneBounds = World->GetWorldSettings()->GetWorldBounds();
    float SceneExtent = SceneBounds.GetExtent().GetMax();
    
    // 目標: 各グリッドセルが約5メートル四方になるように
    int32 OptimalResolution = FMath::CeilToInt(SceneExtent / 500.0f);
    OptimalResolution = FMath::Clamp(OptimalResolution, 64, 256);
    
    RadiosityCache->SetSpatialHashResolution(OptimalResolution);
    
    UE_LOG(LogLumen, Log, TEXT("Radiosity Cache Spatial Hash Resolution: %d (Scene Extent: %.2f m)"),
           OptimalResolution, SceneExtent / 100.0f);
}

公式ドキュメントによると、一般的なオープンワールドシーン(10km四方)では解像度128が推奨され、室内シーン(数百メートル四方)では64で十分です。

メモリバジェット管理

UE5.9では、Radiosity Cacheのメモリ使用量を厳密に制御するためのMemory Budget Systemが導入されました。

// メモリバジェット管理の実装例
struct FLumenRadiosityCacheMemoryBudget
{
    int32 MaxMemoryMB = 512;
    int32 CurrentMemoryMB = 0;
    int32 EvictionThresholdMB = 460;  // 90%
    
    void UpdateMemoryUsage(const FLumenRadiosityCache* Cache)
    {
        CurrentMemoryMB = Cache->GetCurrentMemoryUsageMB();
        
        if (CurrentMemoryMB > EvictionThresholdMB)
        {
            // LRU (Least Recently Used) 戦略でエントリを削除
            int32 TargetFreeMB = MaxMemoryMB - EvictionThresholdMB;
            Cache->EvictLRUEntries(TargetFreeMB);
            
            UE_LOG(LogLumen, Warning, TEXT("Radiosity Cache Memory Budget Exceeded: %d MB / %d MB. Evicted %d MB."),
                   CurrentMemoryMB, MaxMemoryMB, TargetFreeMB);
        }
    }
};

メモリバジェットを超過した場合、最も長い間使用されていないキャッシュエントリから順に削除されます。この戦略により、メモリ使用量を安定させながら、頻繁にアクセスされるキャッシュエントリは保持され続けます。

動的ライト変更の検出とIncremental Update

UE5.9のAdaptive Update戦略の核心は、動的ライトの変更を効率的に検出し、影響範囲を正確に推定することです。

Change Detection メカニズム

Lumenは、各動的ライトのTransform HashLighting Parameters Hashを毎フレーム計算し、前フレームと比較することで変更を検出します。

// 動的ライト変更検出の実装例
struct FLumenDynamicLightChangeDetector
{
    TMap<ULightComponent*, uint32> PreviousLightHashes;
    
    TArray<ULightComponent*> DetectChangedLights(UWorld* World)
    {
        TArray<ULightComponent*> ChangedLights;
        
        for (TActorIterator<ALight> It(World); It; ++It)
        {
            ULightComponent* Light = (*It)->GetLightComponent();
            if (!Light || !Light->IsMovable()) continue;
            
            // 現在のライト状態のハッシュ計算
            uint32 CurrentHash = ComputeLightHash(Light);
            uint32* PrevHash = PreviousLightHashes.Find(Light);
            
            if (!PrevHash || *PrevHash != CurrentHash)
            {
                ChangedLights.Add(Light);
                PreviousLightHashes.Add(Light, CurrentHash);
            }
        }
        
        return ChangedLights;
    }
    
    uint32 ComputeLightHash(ULightComponent* Light)
    {
        uint32 Hash = GetTypeHash(Light->GetComponentTransform().GetLocation());
        Hash = HashCombine(Hash, GetTypeHash(Light->GetComponentTransform().GetRotation()));
        Hash = HashCombine(Hash, GetTypeHash(Light->Intensity));
        Hash = HashCombine(Hash, GetTypeHash(Light->LightColor));
        return Hash;
    }
};

Incremental Update の実装

変更されたライトが検出されると、Lumenはそのライトの**影響範囲(Affected Region)**を計算します。影響範囲が全シーンの一定割合(デフォルト25%)以下の場合、Incremental Updateが実行されます。

// Incremental Update実装例
void FLumenRadiosityCache::IncrementalUpdate(const TArray<ULightComponent*>& ChangedLights)
{
    for (ULightComponent* Light : ChangedLights)
    {
        // ライトの影響範囲を計算(球体で近似)
        FSphere AffectedRegion(Light->GetComponentLocation(), Light->GetAttenuationRadius());
        
        // Spatial Hashから影響範囲内のキャッシュエントリを取得
        TArray<FRadiosityCacheEntry*> AffectedEntries;
        SpatialHash.QuerySphere(AffectedRegion, AffectedEntries);
        
        // GPU上で選択的に再計算
        DispatchIncrementalUpdateComputeShader(Light, AffectedEntries);
        
        UE_LOG(LogLumen, Verbose, TEXT("Incremental Update: Light '%s', %d entries affected"),
               *Light->GetName(), AffectedEntries.Num());
    }
}

Incremental Updateは、影響を受けるキャッシュエントリのみを再計算するため、Full Rebuildと比較して90%以上の計算量削減が可能です。

Temporal Filtering による品質安定化

動的更新では、キャッシュエントリが頻繁に変更されるため、ちらつき(flickering)が発生しやすくなります。UE5.9では、Temporal Filteringにより時間的に安定した間接光を生成します。

以下のシーケンス図は、Temporal Filteringの処理フローを示しています。

sequenceDiagram
    participant F1 as Frame N-1
    participant F2 as Frame N
    participant Cache as Radiosity Cache
    participant Filter as Temporal Filter
    participant Output as Final Irradiance
    
    F1->>Cache: キャッシュエントリ更新
    Cache->>Filter: 新しいIrradiance値
    F2->>Cache: キャッシュエントリ更新
    Cache->>Filter: 新しいIrradiance値
    Filter->>Filter: 指数移動平均計算<br/>NewValue * (1-α) + OldValue * α
    Filter->>Output: フィルタリング済み値
    Output->>F2: レンダリングに使用

Temporal Filteringは、新しく計算されたIrradiance値と、前フレームの値を指数移動平均(EMA)でブレンドします。

Temporal Filter の実装

// Temporal Filteringの実装例
struct FRadiosityCacheEntry
{
    FVector Position;
    FVector Normal;
    FLinearColor CurrentIrradiance;   // 今フレームの計算結果
    FLinearColor FilteredIrradiance;  // フィルタリング済み(レンダリングに使用)
    float TemporalWeight;             // 時間的信頼度
};

void FLumenRadiosityCache::ApplyTemporalFilter(float FilterStrength)
{
    // FilterStrength: 0.0(即座に更新) ~ 1.0(ほぼ変化なし)
    float Alpha = FMath::Clamp(FilterStrength, 0.0f, 0.95f);
    
    for (FRadiosityCacheEntry& Entry : CacheEntries)
    {
        // 指数移動平均
        Entry.FilteredIrradiance = FMath::Lerp(
            Entry.CurrentIrradiance,
            Entry.FilteredIrradiance,
            Alpha
        );
        
        // 時間的信頼度を更新(新規エントリは低い、安定したエントリは高い)
        Entry.TemporalWeight = FMath::Min(Entry.TemporalWeight + 0.1f, 1.0f);
    }
}

FilterStrengthパラメータは、応答速度と安定性のトレードオフを決定します。

  • 0.5-0.7: 動的ライトが頻繁に変化するシーン(アクションゲーム、VFX多用)
  • 0.8-0.9: 動的ライトの変化が緩やかなシーン(アドベンチャー、戦略ゲーム)
  • 0.95: 最高品質、ちらつき完全抑制(シネマティック、建築ビジュアライゼーション)

動的ライトシーンでの最適化パターン

実際のゲーム開発では、シーンの特性に応じてTemporal Filterのパラメータを動的に調整することが推奨されます。

// シーンの動的ライト変化率に応じた自動調整
void AdjustTemporalFilterDynamically(UWorld* World, FLumenRadiosityCache* Cache)
{
    // 過去10フレームの動的ライト変更回数を計測
    static TArray<int32> RecentChangeCount;
    RecentChangeCount.Add(Cache->GetFrameChangeCount());
    if (RecentChangeCount.Num() > 10) RecentChangeCount.RemoveAt(0);
    
    // 平均変更回数を計算
    int32 AvgChanges = 0;
    for (int32 Count : RecentChangeCount) AvgChanges += Count;
    AvgChanges /= FMath::Max(RecentChangeCount.Num(), 1);
    
    // 変化が激しいほど FilterStrength を下げる(応答速度優先)
    float TargetFilterStrength = FMath::GetMappedRangeValueClamped(
        FVector2D(0.0f, 20.0f),     // 入力範囲(変更回数/フレーム)
        FVector2D(0.85f, 0.5f),     // 出力範囲(FilterStrength)
        AvgChanges
    );
    
    Cache->SetTemporalFilterStrength(TargetFilterStrength);
    
    UE_LOG(LogLumen, Verbose, TEXT("Temporal Filter auto-adjusted: %.2f (Avg Changes: %d)"),
           TargetFilterStrength, AvgChanges);
}

GPU最適化:Compute Shader実装

Radiosity Cacheの更新はGPU Compute Shaderで実行されます。UE5.9では、Wave Intrinsicsを活用した最適化が施されています。

// Radiosity Cache更新 Compute Shader(簡略版)
[numthreads(64, 1, 1)]
void UpdateRadiosityCacheCS(uint3 DispatchThreadId : SV_DispatchThreadID)
{
    uint EntryIndex = DispatchThreadId.x;
    if (EntryIndex >= NumCacheEntries) return;
    
    // キャッシュエントリを読み込み
    FRadiosityCacheEntry Entry = CacheEntries[EntryIndex];
    
    // 動的ライトからの直接照明を計算
    float3 DirectIrradiance = 0;
    for (uint LightIndex = 0; LightIndex < NumDynamicLights; LightIndex++)
    {
        FDynamicLight Light = DynamicLights[LightIndex];
        DirectIrradiance += ComputeDirectLighting(Entry.Position, Entry.Normal, Light);
    }
    
    // 近傍のキャッシュエントリから間接光を補間(Radiosity)
    float3 IndirectIrradiance = 0;
    for (uint NeighborIndex = 0; NeighborIndex < 8; NeighborIndex++)
    {
        uint NIdx = Entry.NeighborIndices[NeighborIndex];
        if (NIdx != INVALID_INDEX)
        {
            FRadiosityCacheEntry Neighbor = CacheEntries[NIdx];
            float Weight = ComputeInterpolationWeight(Entry, Neighbor);
            IndirectIrradiance += Neighbor.FilteredIrradiance.rgb * Weight;
        }
    }
    
    // 直接光 + 間接光
    Entry.CurrentIrradiance = float4(DirectIrradiance + IndirectIrradiance, 1.0);
    
    // Temporal Filtering
    Entry.FilteredIrradiance = lerp(
        Entry.CurrentIrradiance,
        Entry.FilteredIrradiance,
        TemporalFilterStrength
    );
    
    // 結果を書き戻し
    CacheEntries[EntryIndex] = Entry;
}

この実装では、各キャッシュエントリが独立して更新されるため、GPU並列性を最大限に活用できます。

Wave Intrinsicsによる最適化

UE5.9のShader Model 6.6では、Wave Intrinsicsを使用して近傍エントリの間接光計算を高速化できます。

// Wave Intrinsics活用例(SM 6.6)
float3 ComputeIndirectIrradianceWithWave(FRadiosityCacheEntry Entry)
{
    // Wave内のスレッドで近傍エントリをシェア
    uint WaveLaneCount = WaveGetLaneCount();
    float3 IndirectSum = 0;
    float WeightSum = 0;
    
    // 各Waveレーンが異なる近傍エントリを処理
    for (uint NeighborIndex = 0; NeighborIndex < 8; NeighborIndex++)
    {
        uint NIdx = Entry.NeighborIndices[NeighborIndex];
        if (NIdx != INVALID_INDEX)
        {
            FRadiosityCacheEntry Neighbor = CacheEntries[NIdx];
            float Weight = ComputeInterpolationWeight(Entry, Neighbor);
            
            // Wave内でIrradianceを集約
            float3 NeighborIrradiance = Neighbor.FilteredIrradiance.rgb;
            IndirectSum += WaveActiveSum(NeighborIrradiance * Weight);
            WeightSum += WaveActiveSum(Weight);
        }
    }
    
    return IndirectSum / max(WeightSum, 0.001);
}

Wave Intrinsicsにより、近傍エントリの間接光計算がSIMD並列化され、30-40%の性能向上が期待できます。

実践的な設定パターン

最後に、異なるゲームタイプ・シーン特性に応じた推奨設定パターンを示します。

パターン1: オープンワールドゲーム(大規模、動的ライト中程度)

[/Script/Engine.RendererSettings]
r.Lumen.RadiosityCache.UpdateMode=2
r.Lumen.RadiosityCache.MaxMemoryMB=768
r.Lumen.RadiosityCache.SpatialHashResolution=128
r.Lumen.RadiosityCache.TemporalFilterStrength=0.8
r.Lumen.RadiosityCache.IncrementalUpdateThreshold=0.3
  • メモリバジェット大きめ(768MB):広大なワールドをカバー
  • Temporal Filter強め(0.8):安定した間接光、遠景のちらつき抑制
  • Incremental閾値高め(0.3):局所的な変更は差分更新で対応

パターン2: アクションゲーム(室内、動的ライト多数)

[/Script/Engine.RendererSettings]
r.Lumen.RadiosityCache.UpdateMode=2
r.Lumen.RadiosityCache.MaxMemoryMB=512
r.Lumen.RadiosityCache.SpatialHashResolution=64
r.Lumen.RadiosityCache.TemporalFilterStrength=0.6
r.Lumen.RadiosityCache.IncrementalUpdateThreshold=0.2
  • メモリバジェット標準(512MB):室内シーンは範囲限定的
  • Temporal Filter弱め(0.6):動的ライトの変化に素早く追従
  • Incremental閾値低め(0.2):頻繁な変更でも差分更新を優先

パターン3: シネマティック・建築ビジュアライゼーション(最高品質)

[/Script/Engine.RendererSettings]
r.Lumen.RadiosityCache.UpdateMode=2
r.Lumen.RadiosityCache.MaxMemoryMB=1024
r.Lumen.RadiosityCache.SpatialHashResolution=256
r.Lumen.RadiosityCache.TemporalFilterStrength=0.95
r.Lumen.RadiosityCache.IncrementalUpdateThreshold=0.4
  • メモリバジェット最大(1024MB):品質最優先
  • Temporal Filter最強(0.95):ちらつき完全排除
  • Spatial Hash高解像度(256):細かい間接光の表現

まとめ

UE5.9のLumen Radiosity Cache動的更新実装により、以下が実現可能になりました。

  • メモリ効率50%改善: Adaptive UpdateとSpatial Hashによる効率的なキャッシュ管理
  • 更新レイテンシ削減: 3フレーム→1フレームへの短縮(Incremental Update使用時)
  • 品質安定化: Temporal Filteringによるちらつき抑制とリアルタイムGI品質の両立
  • GPU最適化: Wave Intrinsicsを活用した30-40%の性能向上

実際のプロジェクトでは、シーン特性(オープンワールド/室内、動的ライト頻度)に応じて、メモリバジェット・Temporal FilterStrength・Incremental閾値を調整することが重要です。

UE5.9の新しいRadiosity Cache実装は、次世代のリアルタイムグローバルイルミネーションを実現する強力な基盤となります。動的ライトが多用される現代のゲーム開発において、メモリ効率と品質を両立する実践的なソリューションです。

参考リンク

#Unreal Engine 5 #Lumen #グローバルイルミネーション #GPU最適化 #リアルタイムレンダリング
シェア: