『あんさんぶるスターズ!!Music』における3D演出開発の工夫 〜U.S.A.のプロジェクションマッピング〜
はじめに
Happy Elements株式会社 カカリアスタジオで『あんさんぶるスターズ!!Music』(以下『あんスタ!!Music』)のゲームエンジニアをしているH.K.です。
現在は主にあんスタ!!Musicの3DライブMVの開発・運用を担当しています。MV制作といっても社内外含め非常に多くの人が関わっておりまして、その中でも社内のエンジニアは主に以下の部分を担当しています。
- 背景の3Dデータの Unity への入れ込み、キャラクターのデータと合わせて再生できるようにする準備
- 背景の演出機能の新規開発、改善
- キャラクター入れ替え時の体型変化への対応やIK、その他のさまざまな制御をタイムラインから行う機能の開発、改善
- ゲームからMVを再生し、SPPへの切り替えなどを行う仕組みの開発、改善
- MV公開前の各種データの整理、設定の精査、AssetBundle の準備
また、実際に背景演出やキャラクターの設定を行うのはエンジニアではなくアーティストとなりますが、エンジニアはアーティストと密に連携をとりMVの品質にこだわりを持って業務に臨んでいます。最近ではMVの演出がますます凝ってきていることもあり、MV内容の大枠を検討する段階でも実現可能性やエンジニア視点で演出を盛りやすいポイントのアドバイスを行うようになってきています。
あんスタ!!Music カバーソングシリーズ第1弾『U.S.A.』のMVでは、地面や建物にさまざまな図形のプロジェクションマッピングを行う演出がありました。
この記事では、プロジェクションマッピングの演出を実装した際の品質面・パフォーマンス面・制作効率面での工夫を紹介します。かなり細かな話題が多くなりますが、MV制作において弊社エンジニアが新規演出開発にどのように取り組んでいるかの雰囲気だけでも伝われば幸いです。
プロジェクションマッピングの仕組み
U.S.A. のプロジェクションマッピングは、サブカメラで投影内容の図形を RenderTexture に描画し、その結果を地面などのオブジェクトに投影するような仕組みになっています。
投影の仕組みは以前から実装済みで、Unity 標準の Projector と似たようなものを あんスタ!!Music のライブ向けに独自実装したようなものとなっています。大まかには
- 投影先のオブジェクトに専用のシェーダーをセットする
- 制御用のスクリプトから投影内容のテクスチャと投影角度などを表す行列を投影先に渡す
- 投影先のシェーダーでは、上記の行列から投影される映像の位置を計算してテクスチャをサンプリングし、ライトに加算して描画する
という仕組みになっていますが、この記事の本題から外れるため詳細は割愛します。ここから先は、投影内容をどのように作るかの話となります。
RenderTexture の解像度の問題
今回のプロジェクションマッピングでは、サブカメラの RenderTexture の解像度が懸念点の一つでした。これまでに使われた投影では
- 投影内容が固定の静止画または動画で、 RenderTexture を必要としない
- 複数のスポットライトを床に投影するため RenderTexture が必要となるが、解像度が低くても品質的には全く問題ない
というケースが多かったのですが、今回は投影内容を動的に生成する必要があり、なおかつ細かい図形からなるモーショングラフィックということで、比較的高い解像度が必要となりました。投影範囲は地面の限られた範囲に絞り、建物への投影時は別系統の制御に分けるなどしていますが、それでも3Dリッチでは 1024x1024 ピクセル程度は必要ということになりました。
RenderTexture の解像度が高いと何が問題かというと、主にメモリ負荷です。通常のテクスチャは PVRTC や ASTC など圧縮された形で保持できるのに対し、 RenderTexture は(描画先にもなるということから)そのような圧縮形式は存在しません。それだけで通常のテクスチャより数倍大きくなるのは避けられませんし、さらに設定を雑に行うと爆発的に容量が大きくなる可能性があります。
- MSAA (Multi-Sample AntiAliasing): 容量はサンプル数倍に増えます。ポリゴンのエッジをなめらかにすることができますが、今回は図形をそのままの形のポリゴンで表しているわけではない(フラグメントシェーダーで図形の形状を計算している)ので不要となりました。詳しくは次節で説明します。
- Format: RGBAが扱えて標準的なフォーマットは ARGB32(下の画像のR8G8B8A8_UNorm)ですが、HDRが必要な場合は ARGBHalf(R16G16B16A16_SFloat)などを使う必要があります。プロジェクションマッピングの用途では ARGB32 で問題なさそうです(十分明るい光を投影したい場合、投影機能側に全体の明るさを調整できるパラメータを追加する必要がありますが)。
- Enable Mip Maps: 容量は約1.3倍に増えます。ミップマップは高解像度のテクスチャが画面に小さく映った時にきれいに低負荷で描画するためのものです。今回は粗が目立たないギリギリに解像度を落としているため、ミップマップの必要性は低いですが、地面はカメラに対して急な角度で映ることが多いので検討しても良いかもしれません。
- Depth Buffer: 追加で容量が必要になります(MSAAの影響あり)。半透明を加算していくプロジェクションマッピングの用途では不要そうです。
これらの設定によっては、ちょっとした違いで容量が10倍以上大きくなり、解像度 1024x1024 の場合数十MBに達する可能性があります。これはMVのキャラモーション数人分[1] に匹敵する容量です。そのため、RenderTexture の設定をチューニングして容量を抑えることや、なるべく低い解像度で品質を担保できるようにシェーダーを工夫することが必要となります。
シェーダーによる図形の表現
多角形・直線・円などの単純な図形を描画する方法として、図形を含む長方形の板ポリにシェーダーの計算によって図形を表現する方法を採用しました。この方法は、アニメーションのために制御するパラメータさえ整備できればすべての計算をシェーダーで完結できることや、図形をきれいに描画できる、後述するグロー効果やアンチエイリアスも軽く対応できるといった利点があります。
これ以外にも以下のような方法も考えられますが、今回のようなアニメーションを柔軟に作成する目的には合っていないと思われます。
- 図形の形そのままのメッシュを作成する方法:C#側でアニメーションの処理まで行えば自由度は高いですが、コードを変更するとアプリの更新が必要になるため柔軟性に難があります。アニメーションを頂点シェーダーで行ってC#コードの更新を減らすことも可能ではあります。ただ、図形の形のメッシュを作る段階でそれなりに面倒です。
- 図形を画像で表す方法:拡大縮小・移動・回転・色乗算程度なら問題ありませんが、それ以上のアニメーションは無理があります。連番画像にすると容量がものすごいことになりそうです。ただ、シェーダーもがっつり開発する前提であればSDF[2] のような形で画像を利用するアプローチは十分にあり得ます。
フラグメントシェーダーのイメージ
板ポリ上にシェーダーで図形を表現する方法について軽く説明します。詳しく説明すると長くなり本題から外れてしまうので、ある程度の前提知識が必要となることはご容赦ください。
フラグメントシェーダーでの計算は、与えられた点が図形の内部にあるか外部にあるかを判定して色を決定するというのが主な内容になります。例えば円(塗りつぶし)の場合は次のようなコードになります。
float _Radius;
struct v2f
{
float4 pos : SV_POSITION;
float4 color : COLOR0;
float2 uv : TEXCOORD0;
};
// フラグメントシェーダー
half4 frag(v2f i) : SV_Target
{
half4 color = i.color;
// i.uv は標準のQuadメッシュのUV (0, 0)〜(1, 1) がそのまま格納されているとします
// 円の中心からの距離
half r = length(i.uv * 2 - 1);
// 図形の境界からの符号付き距離(内部なら d > 0, 外部なら d < 0)
half d = _Radius - r;
// d < 0 のとき結果が 0 になる
color = color * step(0, d);
return color;
}
途中で図形の境界からの符号付き距離というのをわざわざ明示的に計算しています。他の図形はUV座標から符号付き距離を計算する方法だけ差し替えれば済むため、プログラムがわかりやすくなるでしょう。また、アンチエイリアスやグロー効果を実装するのにも役立ちます。
アンチエイリアス
このMVでは RenderTexture の解像度をできるだけ小さくしようとしているため、ジャギの有無は見栄えに大きく影響します。ジャギが消えて輪郭が滑らかになるように、シェーダー内の計算を変更してアンチエイリアスを行うようにしてみましょう。いくつか方法が考えられますが、 fwidth
を使うのが手軽です。
half4 frag(v2f i) : SV_Target
{
// (符号付き距離の計算まで同じ)
color = color * saturate(0.5 + d / max(0.0001, fwidth(d)));
return color;
}
fwidth
( fwidth - Win32 apps | Microsoft Learn )は隣接するピクセルの値を用いて微分に関する計算ができる関数です。上記のプログラムでちょうど良い滑らかさの輪郭が得られる理由は以下のように説明できます。
- 例えば
saturate(0.5 + d / 2)
という式の値は、厳密に図形の境界と一致する点(d = 0
)を中心として、d = 1
からd = -1
のところまで値が 1〜0 で連続的に変化します。そのため、ぼやけた輪郭の図形が得られます。(ぼかし幅は距離の尺度に依存します。) -
fwidth(d)
はabs(ddx(d)) + abs(ddy(d))
に等しく、d
の値がその周辺で1ピクセルあたりどれくらい変化するかを表しています。 - 最初の式の
d / 2
をd / fwidth(d)
に置き換えることで、輪郭のぼかし幅が1ピクセル程度となり、ちょうど良い滑らかさの輪郭が得られます。 -
max(0.0001, )
はゼロ除算回避のために追加してあります。
フラグメントシェーダーでの図形描画にアンチエイリアスを適用する方法として、 fwidth
を使う以外には以下のような方法も考えられます。実際に試してはいませんが、単純な図形であれば負荷・クオリティの面であまり大きな違いはないと思われます。
-
fwidth(d)
の代わりに解析的な方法で(隣接するピクセルの値を使わず、その点のUV座標だけから)勾配を計算する方法。 - スーパーサンプリング、すなわちUV座標を少しだけずらした複数の点で内外判定を行い、その結果をブレンドする方法。
以上で述べているアンチエイリアスはフラグメントシェーダー内で完結するものですので、MSAAの設定とは関係なく行うことができます。ただし、図形が 半透明 の物体として描画されることを想定しています。不透明な物体に用い、境界付近も深度バッファにいい感じに書き込まれるようにしたい場合には、MSAAを有効にして AlphaToMask On
を使う必要があります。[3]
グロー効果
図形の外側に光が漏れ出すグロー効果を実装しました。これはプログラミングよりアート的な工夫となりますが、効果は非常に大きかったです。また、アンチエイリアスと合わせてエッジをなめらかにする効果にも貢献しているようです。以下はアンチエイリアスとグロー効果のオンオフ組み合わせの比較画像です。
▼アンチエイリアス無し・グロー無し
▼アンチエイリアスあり・グロー無し
▼アンチエイリアス無し・グローあり
▼アンチエイリアスあり・グローあり
グロー効果のメリットは以下のように説明できると思います。
- 細かな凹凸のある地面に当たった光が拡散されてその周囲を照らしているように見え、投影っぽさがアップする
- 図形のシャープな輪郭と平べったい塗りだけでなく、滑らかな明るさの変化の要素もあった方が情報量が増えてリッチに見える(特にライトに加算されるという性質上、地面のテクスチャと掛け合わされるので効果大)
実装面では、大まかには符号付き距離を使って d < 0
の領域にも色が加わるようにしています。ただしいくつか細かな工夫を行いました。
- グローの描画範囲が収まるように頂点シェーダーで板ポリを拡大する
- グローの強さ・幅には線の太さなども考慮に入れ、消える間際の線からはほとんどグローが発生しないようにする
- 図形の形状によっては境界からの符号付き距離をそのまま使うのではなく、形状を考慮した補正を行う(四角形の角付近は弱めるなど)
グリッド制御による制作効率化およびドローコール削減、アニメーション
このMVには多数の図形が同じような挙動をする箇所がいくつかあります。図形1つずつ個別に制御していては大変ですし、ドローコール数も嵩んでしまいます。そのため、図形の集団を一括で制御できるような仕組みを作りました。
その1つは ParticleSystem 対応版で、位置・大きさ・回転・色の制御を ParticleSystem で行えるようにしたものです。シェーダーで図形の計算を行う都合上、パーティクルの中心点などの情報を Custom Vertex Streams でシェーダーに渡す必要があります。
▼ParticleSystem を使っている箇所の例
▼Custom Vertex Streams の設定例
もう1つの仕組みは、前節のスクリーンショットのハニカム模様のように複数の図形のタイミングをずらしてアニメーションさせ、一括で描画する機能です。この記事ではグリッド制御と呼ぶことにします。アニメーションさせる値である線の太さや回転角などはメンバーごとに異なるので、マテリアルプロパティとして直接指定することはできず、頂点シェーダーでアニメーション時刻から計算しています。一括制御する図形たちはメッシュが1つに結合されているのですが、結合処理時に各図形の頂点にグリッドID(UVに格納)が割り当てられており、グリッドIDに応じた時間だけアニメーションを遅延させています。
▼結合されたメッシュとアニメーション中の状態
グリッド制御アニメーションの計算をどのように行うのが良いかは少し悩みました。というのも AnimationCurve のようなアニメーションの仕方を表す情報をシェーダーに渡す必要があるにもかかわらず AnimationCurve そのものは渡すことはできず、マテリアルプロパティで曲線を再現する必要があったためです。AnimationCurve からテクスチャにベイクしたり配列プロパティに格納するというのも大げさに感じられます。結果かなりシンプルに、 float4
型のプロパティで時刻0と1での値と変化率を指定して3次関数でアニメーションさせるようにしてみました。キーフレーム2個だけで制御しているようなものなので少々癖ありですが、あまり複雑なアニメーションが必要なかったのと、インスペクタで制御できて手軽だったのでこの形に落ち着きました。
▼マテリアルのエディタ拡張で曲線のプレビューを表示しています
まとめ
U.S.A. のプロジェクションマッピング演出の仕組みと工夫した点をまとめると以下の通りです。
- プロジェクションマッピングの仕組みは、RenderTexture に図形を描画し、 Projector のような機能で投影しています。
- RenderTexture はメモリ負荷が高くなりやすいため、解像度だけでなく各種設定をチューニングする必要があります。
- シェーダーで図形を表現しています。図形の形にポリゴンを作るのは柔軟性や開発効率に難があり、図形を画像で表すのはアニメーションの自由度、あるいは容量的な観点で無理があるという判断でこのような形になりました。
- 解像度削減のため必須となるアンチエイリアスはシェーダー内の計算で対応しています。半透明を重ねていくだけなのでMSAAを利用する必要もありません。
- グローは見栄えを良くするために非常に効果大。アンチエイリアスと合わせてエッジをなめらかにする効果にも貢献しています。
- 同一種類の多数の図形をタイミングをずらしてアニメーションさせるため、線の太さや回転などの値を頂点シェーダーで計算しています。マテリアルのプロパティでは簡易的な方法でアニメーションパラメータを指定しています。
あんスタ!!MusicのMVでは、今後も次々と新しい演出に挑戦していく予定ですので、よろしくお願いいたします。
-
あんスタ!!Music のライブではキャラモーションがメモリ負荷の結構な割合を占めています。 ↩︎
-
Signed Distance Field, 符号付き距離フィールド ↩︎
-
参考: Anti-aliased Alpha Test: The Esoteric Alpha To Coverage | by Ben Golus | Medium ↩︎
Discussion