ノートの端の書き残し

UnityやらC#やら。設計が得意かもしれない。

Vertex Animation Textureを使った大量描画

Vertex Anination Texture(VAT)とは

例えば、あるメッシュとそれを動かすアニメーションクリップがあった場合、 アニメーションクリップの各フレームにおける頂点データをテクスチャとして保存できます。
1ピクセルが1頂点の座標データを持ち、横軸を頂点インデックス、縦軸をアニメーションのフレーム番号とみなすことで、頂点シェーダでテクスチャをサンプリングすることが、頂点操作によるアニメーションに直結します。

利点

Unityで言えばSkinnedMeshRendererとかAnimatorとか、C#領域での操作を必要とするアニメーションと比べ、テクスチャのサンプリングがアニメーションを表現するため、全てGPU側の処理で済むのが強いです。
事前に計算しておいたアニメーションしかできませんが、頂点データの補間によるアニメーションのブレンドも可能です。 なぜVATが良いかというと、普通にSkinnedMeshRendererとAnimatorでアニメーションするオブジェクトの場合、1つ1つCPUでボーンの状態とか管理されていて、いちいちGPUにそれを送っています。データ転送は悪ですね。それに対し、VATはアニメーションの状態すらGPU計算なので、GPUで完結します。これは嬉しい。

欠点

テクスチャのサンプリングで済む、と言いますが、テクスチャサンプリングは決して軽い処理では無いです。というか、現代でGPUの最適化を考える場合は計算量よりもメモリ帯域を重く見なければなりません。
飽くまでも、データ転送をいちいち行うよりマシ、という話で、GPU完結でもっと向いた話はあります。(スキニングをComputeShaderで行うとか)

VATをUnity上で作る

VATの作り方としてはHoudiniでそういうノードがあるよとか出てくるんですが、VATとは要するに頂点座標や法線をテクスチャに焼いたものでしか無いので普通にUnityで作れます。
やることは、「1フレームずつアニメーションさせながら各頂点の座標を記録していってテクスチャを生成、保存する」だけです。

github.com

このようなリポジトリもあります。ちなみにこのリポジトリはSwitch2のシャインポストで使用されているようです。
ライセンス表記がありましたので。
おそらくライブシーンの観客に使用されているのだと思います。

まぁ自作できる程度なので、自身の要件に合わないなら自作するのが早いと思います。

コードの一部

部分的なコードになってしまうのですが、
↓のようにアニメーションを更新しながら状態を記録していって、

        // アニメーション回しつつ頂点座標を保存する
        var baked = new Mesh();
        var verts = new List<List<Vector3>>();
        var norms = new List<List<Vector3>>();
        for (var f = 0; f < frameCount; f++)
        {
            var newVertsLine = new List<Vector3>();
            var newNormsLine = new List<Vector3>();
            var normalized = (f + 0.0001f) / frameCount;
            
            // 今の状態を更新
            animator.Play(st.name, 0, normalized);
            animator.Update(0);
            skinnedMeshRenderer.BakeMesh(baked);
            baked.GetVertices(newVertsLine);
            baked.GetNormals(newNormsLine);
            
            verts.Add(newVertsLine);
            norms.Add(newNormsLine);
        }

次のような感じでテクスチャに保存します。
TextureFormat.Halfにしているのはメモリ帯域節約です。VATならこの精度で十分です。
負数や1より大きい値もHalfは保存できるので、そのまま入れちゃえます。

var vertexCount = pixelData[0].Count; // 横の長さ
var frameCount = pixelData.Count; // 縦の長さ
var tex = new Texture2D(vertexCount, frameCount, TextureFormat.RGBAHalf, false, true)
{
    wrapMode = TextureWrapMode.Clamp,
    filterMode = FilterMode.Point,
};
        
// テクスチャに保存
for (var y = 0; y < pixelData.Count; y++)
{
    var t = pixelData[y];
    for (var x = 0; x < t.Count; x++)
    {
        var ratePos = t[x];
        tex.SetPixel(x, y, new Color(ratePos.x, ratePos.y, ratePos.z));
    }
}
        
tex.Apply(updateMipmaps: false, makeNoLongerReadable: false);
return tex;

あとはどう使うかですが、アニメーションクリップが複数ある場合はまとめてTexture2DArrayにしてしまうのがよいかと思います。
Texture2DArrayは全部同じ大きさである必要がありますが、アニメーションクリップはフレーム数が異なるのが普通なので、縦の長さが異なるはずです。その分は最長に合わせて他に余白を入れることになります。

public static Texture2DArray CreateVat2DArray(Texture2D[] sources)
{
    var width  = sources[0].width;
    var height = sources.Max(tex => tex.height); // 最大フレーム数

    var texArray = new Texture2DArray(width, height, sources.Length, TextureFormat.RGBAHalf, false, true)
    {
        filterMode = FilterMode.Point,
        wrapMode   = TextureWrapMode.Clamp,
        anisoLevel = 1
    };

    for (int i = 0; i < sources.Length; i++)
    {
        var src = sources[i];
        // ここで src は ToTexture2DでApply済み

        // フルサイズ分の配列を用意(既定は0=黒でOK)
        var colors = new Color[width * height];

        // 元テクスチャの全ピクセル(幅×元高さ)
        var srcColors = src.GetPixels(0);

        // 行ごとにコピー(先頭から詰める)
        for (int y = 0; y < src.height; y++)
        {
            System.Array.Copy(
                srcColors, y * width,
                colors,    y * width,
                width
            );
        }

        texArray.SetPixels(colors, i, 0);
    }
        
    texArray.Apply(updateMipmaps: false, makeNoLongerReadable: false);
    return texArray;
}

これでVATはできたので、頂点シェーダ側ではSAMPLE_TEXTURE2D_ARRAY_LODを使ってサンプリングします。