見出し画像

UnityでImposterを作った

Imposterとは

3Dを描画する場合、基本的には見えている部分のポリゴン数だけ、
頂点シェーダーを実行する必要があります。

遠方にあるオブジェクトは
LODによってポリゴン数を削減させることもできますが、
数多くのオブジェクトを表示するためにさらにポリゴン数を減らしたい…..。

そんな時に使える手法として「Imposter」と呼ばれるものがあります。
これは、視点によってビルボードのテクスチャを切り替えることで、
物体を3Dっぽく表現するもので七つの大罪でも用いられている手法です。

Unityでは、有料AssetにImposterを表現できるものがあるのですが、
$55 とそこそこするので、自分で作ってみることにしました。
(自作後、どういう作りになっているのか気になり買いました)

作ったもの

動画にしました。
上に表示されているのが3D、真ん中がImposterで表示したユニティちゃんです。

Githubリンク →  (準備中…..)

自作のAtlasを用いた手法

まずは色んな角度からの見え方を保存した1枚絵を作成し、
視線に応じて表示する領域を決定する手法を考えました。

・自作のAtlas画像を作成する

まずは、Imposter表示用に使用するAtlas画像(つまり1枚絵)を作成します。

カメラをユニティちゃんの周りに配置

各視点での画像が必要なので、
カメラ数を指定した上で「カメラ配置」のボタンを押すと上のように
撮影したいオブジェクトの周りにカメラを配置するようにしました。

camera.rect = new Rect((float) widthInd / cameraWidthCount, (float) heightInd / cameraHeightCount,
                    1f / cameraWidthCount, 1f / cameraHeightCount);
camera.targetTexture = renderTexture;

そして上のように各カメラのRectをカメラ数に合わせてあげて、
各カメラを描画後、レンダーテクスチャを画像として出力すると、
下記のような画像が生成できます。

    private void createSprite(RenderTexture _renderTexture)
    {
        for (int heightInd = 0; heightInd < cameraHeightCount; heightInd++)
        {
            for (int widthInd = 0; widthInd < cameraWidthCount; widthInd++)
            {
                captureCameras[heightInd, widthInd].Render();
            }
        }

        var texture = new Texture2D(cameraSize.x * cameraWidthCount, cameraSize.y * cameraHeightCount);
        texture.ReadPixels(new Rect(0, 0, _renderTexture.width, _renderTexture.height), 0, 0);

        File.WriteAllBytes(
            $"{Application.dataPath}/{outputName}.png",
            texture.EncodeToPNG());
    }
生成された画像

・自作のAtlas画像の表示

表示時はカメラの視線に正面が来るよう(=ビルボード)にしつつ、
視線の角度によって表示する画像の位置を変更する必要があります。

視線の角度の取得は平面に投影したベクトル間の角度を求めるVector3.ProjectOnPlane()
を用いました。

x-z平面上の角度はそのままx-z平面上に投影時の角度で良いのですが、
x(z)-y平面上の角度はx軸上、z軸上にカメラがあった時に正しく角度が取得できません。
そのため、x-z平面上に視線ベクトルを投影したベクトルと視線ベクトルの
角度を取得しました。

    private Vector2 calcAngle()
    {
        Vector3 cameraVecForUp = Vector3.ProjectOnPlane(-transform.forward, Vector3.up);
        Vector3 rightForUp = Vector3.ProjectOnPlane(Vector3.right, Vector3.up);

        float xAngle = (Vector3.SignedAngle(cameraVecForUp, rightForUp, Vector3.up) + 360) % 360;

        Vector3 cameraVecForForward = Vector3.ProjectOnPlane(transform.forward, transform.right);
        Vector3 forwardForRight = Vector3.ProjectOnPlane(Vector3.down, transform.right);

        float yAngle = Vector3.Angle(cameraVecForForward, forwardForRight);

        return new Vector2(xAngle, yAngle);
    }

角度は求められたので、後は角度から使用する画像を決定し、
Shaderに位置情報を渡してその部分だけ表示すれば完成です。

・課題

Atlas画像作成時にカメラの設置数を増やすと、画像サイズを大きくしても
表示時の見た目が荒くなるという課題にぶつかりました。

粗いユニティちゃん

原因はShaderの画像取得時にありました。

カメラの設置数が多いと、
出力した画像の1カメラあたりのUV座標の範囲が小さくなります。
結果、Shaderでテクセル取得が荒くなっていました。

ですが、変数の精度に依存しており改善するのは難しいため、
別のアプローチで対処することにしました。

Sprite Atlasを用いた手法

UnityにはSprite Atlasと呼ばれる
複数のテクスチャを 1 つのテクスチャに統合する機能があります。

基本的な処理はAtlasと同様で、
Sprite Atlasでは各カメラごとに画像を出力しそれをパックすれば
表示時に使用するSpriteAtlasが生成できます。

スクリプトでパックする方法が検索してもわからず困っていたのですが、
SpriteAtlasUtility.PackAtlases() で行うことができました。

        Object[] spriteAtlasObj = new Object[atlasGenerator.CameraHeightCount * atlasGenerator.CameraWidthCount];
        for (int heightIndex = 0; heightIndex < atlasGenerator.CameraHeightCount; heightIndex++)
        {
            for (int widthIndex = 0; widthIndex < atlasGenerator.CameraWidthCount; widthIndex++)
            {
                Object spriteObj = AssetDatabase.LoadAssetAtPath<Object>(
                    $"Assets/ImposterGenerator/{atlasGenerator.OutputName}/{heightIndex}_{widthIndex}.png");

                spriteAtlasObj[heightIndex * atlasGenerator.CameraWidthCount + widthIndex] = spriteObj;
            }
        }

        SpriteAtlas spriteAtlas = new SpriteAtlas();
        spriteAtlas.Add(spriteAtlasObj);

        SpriteAtlasUtility.PackAtlases(new[] {spriteAtlas}, EditorUserBuildSettings.activeBuildTarget);
        AssetDatabase.CreateAsset(spriteAtlas, $"Assets/{atlasGenerator.OutputName}.spriteatlas");

上はカメラから生成した画像フォルダを読み込み、
パックしてSpriteAssetを生成するまでのコードです。

自作AtlasではShaderも書く必要がありましたが、
Sprite AtlasではSpriteRendereコンポーネントで表示するSpriteを、
SpriteAtlasファイルから取得した画像群から視線情報をもとに選択するだけなので、マテリアルを自由に差し替え可能であるという拡張性が高まったのも良いと思いました。

注意点

最初は 2021.3.0f1(LTS) で実装していたのですが、
コードで SpriteAtlasUtility.PackAtlases() を呼ぶとクラッシュする罠にハマりました。

2022.1.4f1に上げてみたところ解消されましたが、
Unityのバージョンによってはクラッシュするようなので注意してください。

描画結果比較

SpriteAtlasのほうが非常に綺麗に表示されています。
自作Atlasだと、上手くサンプリングできず黒い部分が輪郭のように表示されている箇所がありますが、SpriteAtlasではそれがないことがわかります。

上が3D、真ん中がSprite Atlas、下が自作Atlas

最後に

初めは有料Assetsを買わずに済ますために始めたImposter作りでしたが、
思っていた以上に良い完成度のImposterが作れたと思っています。

七つの大罪ではこのImposterにアニメーションさせているので、
この後もさらに自作Imposterを拡張していきたいと思います。

参考

・Unityちゃんライセンス: © Unity Technologies Japan/UCL
・〈七つの大罪〉をゲームで!高品質グラフィックを具現化するための技法
開発最適化のご紹介: https://learning.unity3d.jp/3227/
https://nekojara.city/unity-vector-angle#平面に投影したベクトル間の角度を求める


この記事が気に入ったらサポートをしてみませんか?