
【Unity】Shader GraphでStylizedなSkyboxシェーダを実装する【Advent Calendar 12/5】
はじめに
この記事は Colorful Palette アドベントカレンダー 12/5 の記事です。
株式会社Colorful Paletteでクライアントエンジニアをしているとぐちです。普段は主にグラフィックス関連やライブコンテンツ関連の業務を行っています。
3Dゲームの構成要素には様々なものがありますが、その中でも「空」は画面の広い面積を占めることが多いです。今回の記事では空の表現に着目し、UnityのShader Graphで行ったSkyboxシェーダの実装を解説します。

概要
Unityにはデフォルトで Skybox/Procedual というプロシージャルなSkyboxシェーダが含まれています。このシェーダは一般的に物理的な空の表現を行うために使われるレイリー散乱・ミー散乱などの光学的な計算をシミュレートしているものです。
こういったシェーダはある程度物理的に正しい挙動が得られる一方、アーティストによる細かい色調整が難しいという点があります。
そこで、今回は物理的な正しさは無視する代わりにアーティストによる細かい色調整を可能にすることを目指してシェーダを実装します。
開発環境
●Unity 2022.1f7
●Universal RP 13.1.8
●Shader Graph 13.1.8
キャプチャについて
この記事に添付しているスクリーンショットではポストエフェクトをBloomのみ適用した状態でキャプチャしています。

使用アセット
デモシーンの背景モデルとして以下のアセットをお借りしています。
ここからは、実際のShader Graphのキャプチャとともに詳細な実装を解説していきます。
カラー制御
まずは空全体のカラーの制御について解説します。以下がカラー制御の処理の全体像になります。

World PositionをNormalizeしたベクトルのY要素を使い、以下の2つの処理を行っています。
地平線の高さを基準としてSkyboxをGround/非Groundに分ける
TopColor, BottomColorの2つのカラーをブレンドする
光学的なシミュレーションなどは行っていないため、かなりシンプルな実装になっているかと思います。

太陽の描画
次に、Skybox上への太陽の描画について解説します。以下が全体像です。

ここでは、
太陽そのもの
太陽の周辺光(フレア)
の2つを描画するための計算を行っています。

太陽は、原点(0, 0, 0)半径1の球上に指定したある点に対するSkyboxの各点の距離をもとに、stepノードやsmoothstepノードで円のサイズを制御しています。

雲の描画
次に、雲の描画について解説します。今回は雲用のメッシュなどは用意せず、Skyboxシェーダ上で雲の描画もまとめて行う方式で実装を行っています。以下が全体像となりますが、いくつかに処理を分割して解説します。

雲の座標計算
今回の実装ではSkyboxシェーダで雲の実装を行っているため、Skybox上に擬似的に高さをつけた雲を描画する必要があります。具体的には以下の画像のようなイメージです。

そこで、Skyboxから計算される視線方向と雲の描画平面の高さから、Skyboxの各点における雲の座標を計算します。

ノイズのサンプリング
雲の平面の座標が取得できたら、その座標のXZ座標をUVとしてノイズテクスチャをサンプリングします。ここでは雲の形状や移動を制御するためにUVのタイリングやオフセットの計算を行っています。

ノイズのエッジ調整
上節でサンプリングしたノイズそのままでもある程度雲っぽさは出ますが、さらに雲に近い形状を目指すために、エッジの調整を行います。

smoothstepノードを使い、ノイズの強さを絞りエッジを適切な見た目に調整します。

雲の距離フェード
ここまでの実装を反映した結果が以下の画像となります。

かなり雲っぽい雰囲気が出せてきたかと思いますが、無限遠まで雲の描画を行ってしまっているため、地平線に近い部分の表示が汚くなってしまっています。
この問題に対処するため、カメラから雲への距離に応じて雲の強度をフェードさせるようにします。

カメラの座標と雲の平面上の座標との距離を計算し、それをsmoothstepノードに接続することで距離に応じたフェードを実現しています。
この実装により地平線近くで雲がフェードして消えるようになり、以下のような見た目となりました。

時間経過の表現
ここまでの実装により、空・太陽・雲と大体の構成要素を揃えることができました。この項ではそれらのパーツのカラー等を時間経過に合わせて変化させるための実装について解説します。
ここからはShader GraphではなくC#のスクリプトで制御を行うため、 SkyController.cs というSkyboxの制御用のクラスを作成しました。
カラーの制御
SkyControllerには、0 - 1で24時間を表現するTime変数と、その0 - 1に対応するカラーを制御するためのGradientを定義しています。

実装は非常にシンプルで、Skyboxの構成要素に対応する各カラーのGradientをTime変数で評価し、Skybox用のマテリアルのプロパティにセットしています。
private void UpdateSky()
{
var topColor = _topColorGradient.Evaluate(_time) * _skyExposure;
var bottomColor = _bottomColorGradient.Evaluate(_time) * _skyExposure;
_skyMaterial.SetColor(TopColor, topColor);
_skyMaterial.SetColor(BottomColor, bottomColor);
}
また、SkyboxだけでなくシーンのFogのカラーも同じくGradientで定義することで、全体の空気感も合わせて制御しています。
private void UpdateFog()
{
RenderSettings.fogColor = _fogColorGradient.Evaluate(_time);
}
太陽の制御
前述した通り、今回の実装では太陽の位置を原点(0, 0, 0)半径1の球上の点として定義しています。
そこで、まずはTime変数からDirectional Lightの角度を計算します。Directional Light(太陽)の方向ベクトルの逆ベクトルが原点から太陽への方向ベクトルとなるため、その値をSkyboxのマテリアルにセットします。
これにより、Directional Lightの方向とSkybox上の太陽の位置を合致させることができました。
private void UpdateSun()
{
var sunTransform = _sunLight.transform;
// 日の出を0とする
var rotation = Quaternion.Euler(_time * 360f, -90f, -90f);
sunTransform.rotation = rotation;
var sunPosition = -sunTransform.forward.normalized;
_skyMaterial.SetVector(SunPosition, sunPosition);
}
最終出力
これらの実装に加え、雲に簡易的なUVスクロールを入れたりなどを行い、最終的に以下のような挙動を作ることができました。

おわりに
今回の記事では、なるべくシンプルな仕組みで細かい制御ができるSkyboxシェーダを実装することを目指しました。
パフォーマンス的にもある程度軽量なものになっているかと思いますので、特にモバイルゲームなどで空の表現が必要になった際にはご参考にいただけると幸いです。
次回12/6の記事は同じくクライアントエンジニアのふぁいたーさんが担当します。UnityのEntitiesパッケージの入門記事とのことですが、非常にタイムリーな技術なので気になりますね!
Colorful Palette アドベントカレンダー は12/25までの平日+α更新となっております。ぜひ引き続きご覧ください!