UnityでAnimationInstancingを実現してみよう(2)

## 序文
[前回の記事はこちら](https://qiita.com/piti5/items/f101ba632085eea25775)
[Github](https://github.com/piti6/UnityGpuInstancedAnimation
) (Unity 2017.1.3f1)

今回はスクリプト側の処理や事前準備するフローを説明させていただきたいと思います。

1. SkinnedMeshRendererからボーンデータを含むMeshRendererへの変換
2. アニメーションデータを持つテキスチャーの生成
3. メッシュをアニメーションさせることにあたって必要な情報を持たせる

## 実装
### SkinnedMeshRendererからボーンデータを含むMeshRendererへの変換

```csharp:AnimatedMeshToAsset.cs
private static Mesh GenerateUvBoneWeightedMesh(SkinnedMeshRenderer smr)
{
    var mesh = Object.Instantiate(smr.sharedMesh);

    var boneSets = smr.sharedMesh.boneWeights;
    var boneIndexes = boneSets.Select(x => new Vector4(x.boneIndex0, x.boneIndex1, x.boneIndex2, x.boneIndex3)).ToList();
    var boneWeights = boneSets.Select(x => new Vector4(x.weight0, x.weight1, x.weight2, x.weight3)).ToList();

    mesh.SetUVs(2, boneIndexes);
    mesh.SetUVs(3, boneWeights);

    return mesh;
}
```

前回と同じようにUVチャンネル3と4を使ってバーテックスごとのボーンデータをメッシュに入力します。
UV3はDynamicGIで使われるらしいですが、今回はこのままDynamicGIは使わない想定で行きます。


### アニメーションデータを持つテキスチャーの生成

まずはアニメーションデータを入れるテキスチャーのサイズを決めます。
ピクセル一つつき四つのデータを含むことができ(RGBA)、4x4マトリックス一つを表現するには
4pxが必要となります。ですが、TRSマトリックスの最後の行は重複してしまうのでデータとしては3pxを使うことにします。

ですのでアニメーション一つの全体のピクセルの数は

```
3px(ボーン一つの4x4行列) * ボーンの数 * アニメーションの長さ * サンプルフレームレート
```
になります。これを含む正方形テキスチャーを作ったとしてサンプルした瞬間のボーンの行列情報をピクセル上に記録して行きます。

スキニングもGpuが担当することになるので、停止状態やプレビュー環境でメッシュが壊れないよう0フレーム目にバインドポーズを書くことにしました。(コード上省略)

```csharp:AnimatedMeshToAsset.cs
private static Texture GenerateAnimationTexture(GameObject targetObject, IEnumerable<AnimationClip> clips, SkinnedMeshRenderer smr)
{
    var textureBoundary = GetCalculatedTextureBoundary(clips, smr.bones.Count());

    var texture = new Texture2D((int)textureBoundary.x, (int)textureBoundary.y, TextureFormat.RGBAHalf, false, true);
    var pixels = texture.GetPixels();
    var pixelIndex = 0;

    foreach (var clip in clips)
    {
        var totalFrames = (int)(clip.length * TargetFrameRate);
        foreach (var frame in Enumerable.Range(0, totalFrames))
        {
            clip.SampleAnimation(targetObject, (float)frame / TargetFrameRate);

            foreach (var boneMatrix in smr.bones.Select((b, idx) => b.localToWorldMatrix * smr.sharedMesh.bindposes[idx]))
            {
                pixels[pixelIndex++] = new Color(boneMatrix.m00, boneMatrix.m01, boneMatrix.m02, boneMatrix.m03);
                pixels[pixelIndex++] = new Color(boneMatrix.m10, boneMatrix.m11, boneMatrix.m12, boneMatrix.m13);
                pixels[pixelIndex++] = new Color(boneMatrix.m20, boneMatrix.m21, boneMatrix.m22, boneMatrix.m23);
            }
        }
    }

    texture.SetPixels(pixels);
    texture.Apply();
    texture.filterMode = FilterMode.Point;

    return texture;
}
```

### メッシュをアニメーションさせることにあたって必要な情報を持たせる

アニメーションを再生することに必要な情報を持たせる必要があります。

- 1フレームはどこからどこまでなのかを知らないとこのフレームがテキスチャー上どこにあるのかを知らなかったりするので、まず一フレームは3px * ボーンの数というのをシェーダー上に伝えておきましょう。

```csharp:AnimatedMeshToAsset.cs
private static Material GenerateMaterial(GameObject targetObject, SkinnedMeshRenderer smr, Texture texture, IEnumerable<AnimationClip> clips, int boneLength)
{
    var material = Object.Instantiate(smr.sharedMaterial);
    material.shader = Shader.Find("AnimationGpuInstancing/Standard");
    material.SetTexture("_AnimTex", texture);
    material.SetInt("_PixelCountPerFrame", BoneMatrixRowCount * boneLength);
    material.enableInstancing = true;

    return material;
}
```


- 次は 再生したいアニメーションはどれか、テキスチャー上何フレーム目から何フレーム目までなのかを伝えておかないとループや再生の対応ができなくなるので、その情報を該当メッシュを含むオブジェクトのSerializeFieldとして持たせましょう。

```csharp
foreach (var clip in clips)
{
    var frameCount = (int)(clip.length * TargetFrameRate);
    var startFrame = currentClipFrames + 1;
    var endFrame = startFrame + frameCount - 1;

    frameInformations.Add(new AnimationFrameInfo(clip.name, startFrame, endFrame, frameCount));

    currentClipFrames = endFrame;
}
```

```csharp:AnimatedMeshAnimator.cs
public void Play(string animationName, float offsetSeconds)
{
    var frameInformation = FrameInformations.First(x => x.Name == animationName);

    PropertyBlockController.SetFloat("_OffsetSeconds", offsetSeconds);
    PropertyBlockController.SetFloat("_StartFrame", frameInformation.StartFrame);
    PropertyBlockController.SetFloat("_EndFrame", frameInformation.EndFrame);
    PropertyBlockController.SetFloat("_FrameCount", frameInformation.FrameCount);
    PropertyBlockController.Apply();

    IsPlaying = true;
}
```

これで必要な情報が全て揃いました。

## まとめ
今回はAnimationInstancingを実装することにあたって必要なスクリプト側の処理や事前準備するフローに関して調べてみました。
(前回との投稿のタームが長くなってしまい申し訳ございませんでした。)
次回はなるべく早いうちにシェーダ側の実装を説明させていただきます。

サンプル付き完成版は下記のリンクをご覧ください。
[Github](https://github.com/piti6/UnityGpuInstancedAnimation
)