概要
SkinnedMeshRenderer が持つメッシュの頂点情報やポリゴン情報を操作して特殊な演出などを作る方法の紹介です。
アニメーションするメッシュを直接操作することはできませんが、現在のフレームの頂点情報を通常の Mesh に転写し、そちらを操作するという方法を取ります。
できること

例えばこういう、キャラクターがアニメーションしながら転送されてくるエフェクトなんかを作ることができます。
はじめに
キャラクターを被ダメージに応じて傷つける、ポータルやワームホールを潜り抜けるような演出を入れる、といった場面で、プロシージャルに 3D モデルを変形させたり切断したり、一部を非表示にしたいことがあります。
通常の Mesh であれば、 Mesh.triangles や Mesh.vertices といったパラメータを操作することで 3D モデルをスクリプト側から編集することができます。
一方で、 SkinnedMeshRenderer が持つメッシュからこれらのパラメータにアクセスすると、デフォルトの状態での頂点情報が返ってきてしまい、現在のアニメーションフレームにおける頂点情報を取得することができません。
何でなんですかね? アニメーションの処理を GPU で行っているため CPU がアクセスできるメモリに現在のフレームの頂点情報が乗っていないから? SkinnedMeshRenderer にも GetVertexBuffer() というメソッドが用意されており、これを使って ComputeShader から編集することはできるようです。
ComputeShader はできれば触りたくないので、何とかスクリプトから制御できるようにしたいと思い調べていると、 SkinnedMeshRenderer.BakeMesh() というメソッドを見つけました。
これを使えば、通常の Mesh に現在のフレームにおける SkinnedMeshRenderer が持つメッシュの頂点情報をコピーできるようです。
つまり、これを毎フレーム呼べば現在フレームの頂点情報を Mesh に持ってくることができ、その Mesh を操作してやればアニメーションするキャラクターのポリゴンをいじることができます。
BakeMesh はそれなりに重い処理だろうと思うのですが、冒頭の gif 画像のキャラクター (7446 頂点, 11186 ポリゴン) で試したところ、毎フレーム呼んでも特に問題なく 60fps 以上で動いたので、ここではとりあえず良しとしています。複数のキャラクターに対して大量に使ったりするとまた変わるかもしれませんが。
ソースコード
冒頭の gif のように、ワームホール的なものから下に向かって吐き出される演出を作ります。
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
public class TransferringEffect : MonoBehaviour
{
[SerializeField] SkinnedMeshRenderer originalMeshRenderer;
[SerializeField] MeshFilter targetMesh;
[SerializeField] MeshRenderer targetMeshRenderer;
[SerializeField, Range(0.0f, 2.0f)] float threshold;
void Start()
{
targetMesh.sharedMesh = new Mesh();
}
void Update()
{
// SkinnedMesh の現在のフレームの頂点情報を Mesh に転写
originalMeshRenderer.BakeMesh(targetMesh.sharedMesh);
// Mesh の頂点を高さでフィルタリング
var vertices = targetMesh.sharedMesh.vertices;
var availableIndices = vertices
.Select((v, idx) => (v, idx))
.Where(item => item.v.y < threshold)
.Select(item => item.idx).ToHashSet();
// 穴よりも低い位置にある頂点だけで構成されるポリゴンのみを result に抽出
var triangles = targetMesh.sharedMesh.triangles;
int trianglesLength = triangles.Length;
List<int> result = new List<int>(trianglesLength);
for (int i = 0; i < trianglesLength; i += 3)
{
if (availableIndices.Contains(triangles[i]) && availableIndices.Contains(triangles[i + 1]) && availableIndices.Contains(triangles[i + 2]))
{
result.Add(triangles[i]);
result.Add(triangles[i + 1]);
result.Add(triangles[i + 2]);
}
}
// result をポリゴン情報として書き戻すことで、穴よりも低い位置にある頂点のみで構成されるポリゴンのみで構成された Mesh が作られる
targetMesh.sharedMesh.triangles = result.ToArray();
targetMeshRenderer.material = originalMeshRenderer.material;
}
}
targetMesh は targetMeshRenderer で描画されるようになっています。
ワームホールでごまかすことができるので、ポリゴンを切断するような処理は入れておらず、全ての頂点が一定のラインより下にあるポリゴンのみを表示する、というやり方を取っています。
もちろん操作対象は通常の Mesh ですので、通常の Mesh に対するポリゴン切断の処理と組み合わせることもできるでしょう。
この方法の欠点は、アニメーションを行う SkinnedMeshRenderer と、それをコピーする先となる MeshRenderer の2つが必要になるという点です。
上記の gif 画像でも、実は画面外にもう一人 SkinnedMeshRenderer を持った同じキャラクターがいて、そいつがアニメーションした結果を画面に映っている MeshRenderer を持つキャラクターに転写するということをしています。
一時的、もしくは限定されたキャラクターに対してのみであれば大丈夫かもしれませんが、大量のキャラクターに対して同じようなことをするとオーバーヘッドが問題になりそうです。
おわりに
冒頭の gif のようなことをする場合はステンシルなどを使った方が簡単でキレイなものができるかもしれませんが、キャラクターの切断といった凝ったことをやりたい場合は、この記事の方法が使えるかもしれません。
SkinnedMeshRenderer.BakeMesh の紹介でした。