🎥

[Unity]3D映像表現のマルチアスペクト対応

2024/11/22に公開

Happy Elements株式会社 カカリアスタジオで『あんさんぶるスターズ!!Music』のゲームエンジニアをしているI.Y.です。この記事では、Unityで作成した3Dの映像表現を、様々な解像度・アスペクト比の端末で違和感なく表示するために行っていることについて、簡単にご紹介いたします。また、関連する話題として、空間に色を加えるためのポストエフェクトの実装について少し説明いたします。

前提

  • モバイルアプリ「あんさんぶるスターズ!!Music」の音楽ゲームで表示されるMVで使われているものです。
  • Unity2022.3系、Built-in Render Pipeline で開発を行っていますが、他のバージョン・パイプラインにも共通する話だと思います。

実現したいこと

あんスタのMVはYouTube等で公開される動画を16:9のアスペクト比で制作しています。16:9というのは様々な媒体で採用されている標準的な比率で、多くのプロジェクトではこのアスペクト比で綺麗に見えるように制作を行っていると思います。例えば、以下の画像は2024年10月に公開された「Fairy Tale Library」のワンカットです。

一方、アプリ側では様々なアスペクト比の端末があることを考慮して、もう少し広い範囲まで見えるようになっています。以下の画像のように、16:9より縦長の端末では縦方向が広く表示され、横長の端末では横方向が広く表示されます。こうすることで16:9の範囲が常に画面の中央に収まることになり、公開動画と比べた際の印象があまり変わらないようにしています。

Physical Camera の Sensor Size と Gate Fit を使ったマルチアスペクト対応

Unityのカメラには Orthographic と Perspective という2つのプロジェクションモードがあります。このうち、Perspective には Physical Camera という機能があり、より詳細な設定を行えるようになっています。

今回実現したい内容に関係があるのは Sensor Size と Gate Fit です。これらの項目の意味や挙動については詳しく解説されている方がいらっしゃるので、そちらを参照していただくと良さそうです。

それぞれ以下のように設定します。

  • Sensor Type: Custom
  • Sensor Size: xとyの比率を16:9にする
  • Gate Fit: Overscan

これにより、カメラの描画範囲(視錐台の形状)は、画面のアスペクト比が16:9より縦長の場合は縦方向に引き伸ばされ、横長の場合は横方向に引き伸ばされるようになります。例えば GameView のアスペクト比をそれぞれ4:3と19.5:9にした状態でカメラの視錐台を確認すると以下のように表示されます。内側の白い長方形領域(16:9の範囲)は変化せず、それより外側の領域が変化していることが分かります。


Unityの用語としては Sensor Size によって決まる長方形領域がフィルムゲート(カメラの物理的性質によって決まる領域)、GameView や実際の端末画面、RenderTexture などによって決まる領域が解像度ゲートと呼ばれています。Gate Fit というのはフィルムゲートをどのように解像度ゲートに収めるか(あるいは収めないか)という設定です。

RenderTexture による解像度の制御、可視領域の制限

映像を実際のゲームに組み込む際には、端末のGPU負荷を軽減するために解像度を下げることが有効です。一方、ゲームでは多くの場合UIを一緒に表示する必要があり、UIの解像度は維持しつつ、後ろの映像だけ解像度を下げたいという状況になると思います。このような場合、映像側のカメラでは RenderTexture によって解像度を制御し、UIについては別の方法で描画するというのがシンプルな対応方法です。

例えば以下の画像は音楽ゲームのスクリーンショットですが、簡単に説明すると次のような構造になっています。

  • 後ろの映像と手前のUIはそれぞれ別のシーンで管理されている。映像側のシーンやアセットは Asset Bundle として配信され、音楽ゲームの開始時にロードされる。
  • 映像側シーンのメインカメラには RenderTexture がターゲットとしてセットされている。解像度の制御はこの RenderTexture に対して行われる。(実際には複数のカメラと RenderTexture を使って様々な制御を行っています)
  • UI側では RawImage を使って RenderTexture をUIの一部として表示する。

RenderTexture のアスペクト比は画面のアスペクト比を元に計算されます。このときアスペクト比の最小・最大値を決めておくことで可視領域の制限を行っています。制限がないと本来カメラに映ることが想定されていない部分まで見えてしまう可能性があるためです。可視領域の外側は黒塗りにするか、画像を使って模様を表示することが一般的に行われていると思います。

また、RenderTexture の解像度は16:9の領域に対してあらかじめ決めておき、それより縦長や横長の端末では16:9領域の解像度が変化しないように面積を大きくするということに注意が必要です。これはどのようなアスペクト比の端末でも16:9領域については同じ解像度で表示できることが望ましいためです。(実際には端末の性能や負荷に応じて解像度を変更するような工夫を行う場合もあると思います)

空間に色を加えるためのポストエフェクトの実装

上記のようにカメラの描画範囲が端末のアスペクト比に応じて変化するような状況では、一部のエフェクトの実装について少し工夫が必要になる場合があります。例えばあんスタのMVでは、空間に色を加えて鮮やかに見せたり、遠景の空気感を出すためにポストエフェクトを使用しています。以下の画像は「KEEP OUT」内の一場面で、このポストエフェクトのオン・オフを切り替えたものです。


このポストエフェクトはビューポート空間で座標、回転、大きさ、変形といったパラメータを設定できるようにしています。大きさは画面の縦幅を基準に計算しています。以下のようなイメージです。

しかし、単純にビューポート空間に合わせて設定を行うだけだと、端末のアスペクト比が変化した場合にエフェクトの位置や大きさがズレてしまいます。例えば上記の設定をアスペクト比が4:3の画面について適用すると以下の画像のようになり、16:9の画面と比べてエフェクトが大きな範囲を占めることになります。

これを防ぐためには「16:9領域におけるビューポート空間」に対して設定を行い、エフェクトの位置と大きさが正しく表示されるように、ポストエフェクトのシェーダーに適切なパラメータを渡す必要があります。

以下は実装方法の一例です。タイムラインを使ってエフェクトの位置や大きさなどの設定を行い、C#側ではそれを行列に変換してシェーダーにパラメータとして渡します。(分かりやすいように少し改変しています。また、必ずしも行列を使う必要があるわけではありません)

const float ReferenceAspect = 16f / 9f;

// カメラのアスペクト比(=RenderTextureのアスペクト比)を16:9と比較したときの比率を取得
Vector2 GetReferenceAspectScale(float cameraAspect)
{
    if (cameraAspect > ReferenceAspect)
    {
        return new Vector2(cameraAspect / ReferenceAspect, 1f);
    }
    else
    {
        return new Vector2(1f, ReferenceAspect / cameraAspect);
    }
}

// 実際のビューポート空間を16:9のビューポート空間に変換するための行列を取得
Matrix4x4 GetReferenceSpaceMatrix()
{
    var scale = GetReferenceAspectScale(_camera.aspect);
    return Matrix4x4.Translate(new Vector3(-(scale.x - 1f) / 2f, -(scale.y - 1f) / 2f, 0f)) *
        Matrix4x4.Scale(new Vector3(scale.x, scale.y, 1f));
}

Matrix4x4 GetEffectTransform(Setting setting)
{
    var offset = new Vector3(-setting.PositionX, -setting.PositionY, 0f);
    var rotation = Quaternion.Euler(0f, 0f, -setting.Rotation);
    var scale = new Vector3(1f - setting.ExpansionX, 1f - setting.ExpansionY, 1f) / Mathf.Max(setting.Scale, 0.01f);
    var toReferenceSpace = GetReferenceSpaceMatrix();
    return Matrix4x4.Scale(scale) *
        Matrix4x4.Rotate(rotation) *
        Matrix4x4.Scale(new Vector3(ReferenceAspect, 1f)) *
        Matrix4x4.Translate(offset) *
        toReferenceSpace;
}

void LateUpdate()
{
    ...
    _material.SetMatrix("_EffectTransform", GetEffectTransform(_setting));
}

ポストエフェクトのシェーダーでは受け取ったパラメータを使ってUVを変換します。

half4x4 _EffectTransform;

v2f vert (appdata v) {
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.uv = v.uv;
    o.effectUV = mul(_EffectTransform, float4(v.uv, 0, 1)).xy;
    return o;
}

half4 frag (v2f i) : SV_Target
{
    half3 color = tex2D(_MainTex, i.uv).rgb;
    // effectUVを使って円形の領域を計算し、色を反映する
    color = ApplyEffect(color, i.effectUV);
    return half4(color, 1);
}

注意点として、ここで行っているのは座標系に対する変換なので、上記の行列は通常のモデル行列の作り方(Scale, Rotation, Translateの順に右から掛け算する)とは異なり、逆行列を作っています。以下の画像のようなイメージです。

実際の作業での工夫

動画としてYouTube等で公開される16:9領域だけではなく、可視領域全体がユーザーの目に触れることに注意して制作を進めています。カメラやキャラクターのアニメーション制作を行っているDCCツール上では、可視領域全体が映るようにカメラを設定し、16:9領域に半透明の枠線を被せて範囲を確認できるようにしています。

また、Unityでの作業においても、可視領域全体に注意しながら各種エフェクトを制作しています。上記の空間に色味を加えるエフェクト以外にも、強い光に伴って表示されるレンズフレアや、オーバーレイ演出といったビューポート空間で扱いたいものについては、アスペクト比に依存せず設定できるようにしています。また、Unityのエディタ拡張を使ってGameViewの挙動を切り替えられるようにしています。以下のようなチェックボックスがあり、「全体を表示する」をオンにすると可視領域全体が見えるようになります。16:9領域にはラインを表示して範囲を分かりやすくしています。

まとめ

MVのマルチアスペクト対応において工夫している点をご紹介しました。詳細な説明は省いていますが、アイデアとしては様々なプロジェクトで必要になるかもしれないものですので、参考になれば幸いです。

Happy Elements

Discussion