【Unity】Adaptive Probe VolumesでLighting Scenarioをブレンドしてみる
はじめに
こんにちは、クライアントエンジニアのサックーです。
この記事では Unity6 から URP で使用可能になった Adaptive Probe Volumes(以下 APV)において、複数の Lighting Scenario(ライティング状態)をブレンドする方法について紹介します。
出来上がる画は以下のようなものになります。

APV とは簡単に言うと従来の Light Probe の進化系で、場合によっては LightMap へのベイクを置き換えることも可能なクオリティのライティングを提供できるようになったものになります。
今回は LightMap を使わずに、さらに BakedLight のみでライティングを行い、全体の色味をブレンドしながら変えていきます。
なお APV 本体の紹介については割愛しますので、以下のようなソースを参考にしてください。
また、本記事における環境は以下の通りです。
- Unity 6000.0.58f2
- Universal Render Pipeline 17.0.4
さらにシーンについては以下の無料アセットを使用しています。
プロジェクトの設定
まず APV と Scenario のブレンドを行えるようにするための設定をします。
今使っているUniversal Render Pipline AssetのLight Probe Systemの項目を変更していきます。
以下のようにAdaptive Probe Volumesを選択し、配下にあるEnable Lighting ScenariosとEnable Lighting Scenario Blendingにチェックを入れてください。

シーンのセットアップ
次にシーンの設定をしていきます。
前述のアセット付属のシーンを開き、全 MeshRenderer の GI 設定を以下のようにします。t:meshrenderer で検索すると便利です。

Hierarchy で右クリックし、Light > Adaptive Probe Volumeで Adaptive Probe Volume を作成します。
この時 Mode は Scene にしておいてください。

シーンに適当に Light を置いていきます。自分は PointLight を 10 個配置しました。

Window > Rendering > Light Explorerを開き、すべてのライトの Mode をBakedにし、色も一旦白で統一していまいます。
その他パラメータも適当に調整します。

Lighting Scenario の準備
今度はいくつか LightingScenario を作成していきます。
Lighting タブを開き、GPU 搭載 PC の場合はSceneでProgressive GPUを選択しておきます。
隣のAdaptive Probe Volumesに移り、Probe PlacementのMax Probe Spacingを 27m に下げておきます。(シーンが比較的小規模なため)

いよいよベイクを行います。下部にあるGenerate Lightingを押してください。
うまく行くと白いライトで照らされたシーンが現れるはずです。

Lighting Scenariosの所に default という項目があると思うので White にリネームしてしまいましょう。
そしてさらに Red,Blue,Yellow の Scenario を作成してください。

次は赤く照らされた状態を作っていきましょう。
Red を選択した状態で、Light Explorerから全ライトの色を赤に変更します。

そして再度Generate Lightingを行います。
Red の Scenario が Active であることを確認するのを怠らないようにしましょう。

すると今度は赤く照らされたシーンがあるはずです。

同様にして Blue と Yellow も生成してください。


これで Lighting Scenario の準備は完了です。
それぞれの Scenario を選択すると、そのライティング状態のシーンが出現することを確認できるはずです。
Scenario Blending を行う
以下のようなコードを用意します。
using UnityEngine;
using UnityEngine.Rendering;
public class LightingScenarioBlender : MonoBehaviour
{
//---シナリオ設定---
[SerializeField, Header("シナリオ設定")]
private string[] scenarios;
//---ブレンド設定---
[SerializeField, Header("各シナリオ間のブレンド時間(秒)")]
private float transitionDuration = 5f;
[SerializeField, Header("各シナリオを保持する時間(秒)")]
private float holdDuration = 3f;
[SerializeField, Header("ブレンドセル数/秒")][Min(1)]
private int blendCell = 10;
[SerializeField, Header("最後のシナリオから最初に戻るか")]
private bool loop = true;
[SerializeField, Header("自動再生するか")]
private bool autoPlay = true;
private ProbeReferenceVolume probeVolume;
private int currentScenarioIndex = 0;
private int nextScenarioIndex = 1;
private float currentBlendFactor = 0f;
private float timer = 0f;
private bool isTransitioning = false;
void Start()
{
// シングルトンインスタンスの取得
probeVolume = ProbeReferenceVolume.instance;
// 配列が有効かチェック
if (scenarios == null || scenarios.Length < 2)
{
Debug.LogError("ProbeVolumeScenarioBlender: 少なくとも2つのシナリオが必要です");
enabled = false;
return;
}
// ブレンドセル数を設定
probeVolume.numberOfCellsBlendedPerFrame = blendCell;
// 最初のシナリオを適用
currentScenarioIndex = 0;
nextScenarioIndex = 1;
ApplyCurrentScenario();
}
void Update()
{
if (!autoPlay || scenarios == null || scenarios.Length < 2)
return;
timer += Time.deltaTime;
if (isTransitioning)
{
// ブレンド中
currentBlendFactor = Mathf.Clamp01(timer / transitionDuration);
BlendToNextScenario(currentBlendFactor);
if (timer >= transitionDuration)
{
// ブレンド完了
isTransitioning = false;
currentScenarioIndex = nextScenarioIndex;
ApplyCurrentScenario();
timer = 0f;
Debug.Log($"シナリオ切り替え完了: {scenarios[currentScenarioIndex]}");
}
}
else
{
// 保持期間
if (timer >= holdDuration)
{
// 次のシナリオへ移行開始
MoveToNextScenario();
}
}
}
private void MoveToNextScenario()
{
nextScenarioIndex = currentScenarioIndex + 1;
if (nextScenarioIndex >= scenarios.Length)
{
if (loop)
{
nextScenarioIndex = 0;
}
else
{
// ループしない場合は停止
autoPlay = false;
Debug.Log("全シナリオの再生が完了しました");
return;
}
}
isTransitioning = true;
timer = 0f;
Debug.Log($"シナリオ遷移開始: {scenarios[currentScenarioIndex]} → {scenarios[nextScenarioIndex]}");
}
private void ApplyCurrentScenario()
{
if (scenarios == null || scenarios.Length == 0)
return;
// 現在のシナリオを完全に適用
probeVolume.lightingScenario = scenarios[currentScenarioIndex];
Debug.Log($"シナリオ完全適用: {scenarios[currentScenarioIndex]}");
}
private void BlendToNextScenario(float blend)
{
if (scenarios == null || nextScenarioIndex >= scenarios.Length)
return;
// 次のシナリオへブレンド
probeVolume.BlendLightingScenario(scenarios[nextScenarioIndex], blend);
}
}
基本的には現在の Scenario の切り替えと Blend 用メソッドの使用で実現します。
現在の Scenario を変更するには
ProbeReferenceVolume.lightingScenario
に Scenario 名を入れてあげるだけです。
Scenario の Blend は
ProbeReferenceVolume.BlendLightingScenario(string otherScenario, float blendingFactor)
を使用します。
混ぜたい Scenario 名とその割合を入れ、割合を時間経過で適切に変化させることでなめらかな Scenario の変更を実現しています。ちなみに混ぜることができるのは 2Scenario のみで、それ以上は混ぜられません。
上記コンポーネントを適当な GameObject にアタッチし、シナリオ設定の所に作成した Scenario 名をすべて登録してください。

この状態でシーンを再生すると以下のように各 Lighting Scenario がブレンドされながら切り替わっていくはずです。

おわりに
ここまでで Adaptive Probe Volumes を使った Lighting Scenario のブレンドする実装についてみてきました。
リアルタイムライトではなくシーンのライティングを切り替えられるのは面白いなと感じています。
一方で負荷やルックについて、リアルタイムライトや LightMap を用いた場合との比較が気になる方もいると思います。
そこについても後日投稿する予定なのでお待ちいただければと思います。
Discussion