Unityの新MeshAPIでMeshColliderをリアルタイム変形させる

# 概要
Unity2021.2以降からMeshAPIでMeshのvertexbufferをComputeshaderで編集できるようになってます。これを利用したSkinnedMeshRendererの変形に追従するMeshColliderのデモが存在しなかったので作ってみました。

# リアルタイムMeshCollider変形デモ

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">アドベントカレンダー用のサンプルの作成をしている。新Mesh APIを利用しているMeshColliderをランタイムで変形させるデモ。<a href="https://twitter.com/hashtag/Unity?src=hash&amp;ref_src=twsrc%5Etfw">#Unity</a> <a href="https://t.co/M6uEiu6uqn">pic.twitter.com/M6uEiu6uqn</a></p>&mdash; unagi (@UnagiHuman) <a href="https://twitter.com/UnagiHuman/status/1472947237739466758?ref_src=twsrc%5Etfw">December 20, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

デモはこちらのレポジトリに用意しました。
https://github.com/unagiHuman/NewMeshAPI_RuntimeMeshCollider

動作したUnityのバージョンはUnity2021.2.7f1になります。

# 参考にしたレポジトリ

* Unity公式のレポジトリ
https://github.com/Unity-Technologies/MeshApiExamples

* このレポジトリのSkinMeshRendererのvertex bufferをcomputeshaderで取り扱うやり方を参照にした
https://github.com/cinight/MinimalCompute

# 解説

MeshAPI以前もMeshColliderを変形させる方法はありました。MeshColliderはMeshを差し込むと形状がMeshの形状になるので、Update処理でSkinnedMeshRenderer.BakeMeshでMesh形状を取得してMeshColliderに差し込めばSkinnedMeshRendererの形状は一応リアルタイムに反映させる事が出来ます。でも、BakeMeshの処理が重いので処理落ちしがちです。

新MeshAPIではComputeShaderを利用して非同期にMeshの頂点情報のゲットとセットができるので、処理落ちなくMeshColliderを変形させる事が出来ます。

以下がcsharpのコードになります。
すべて以下のレポジトリにありますので、興味ある方は動かしてみて下さい。
https://github.com/unagiHuman/NewMeshAPI_RuntimeMeshCollider


```csharp:SkinnedMeshCollider.cs

using Unity.Collections;
using UnityEngine;
using UnityEngine.Rendering;

public class SkinnedMeshCollider : MonoBehaviour
	{
		[SerializeField] private SkinnedMeshRenderer _skinnedMeshRenderer;
		[SerializeField] private MeshCollider _meshCollider;

		private NativeArray<VertexData> _vertData;
		private ComputeBuffer _vertBuffer;
		private int _kernel;
		private int _dispatchCount;
		private AsyncGPUReadbackRequest _request;

		private ComputeShader _bakePointComputeShader;
		
		/// <summary>
		/// 頂点バッファ定義
		/// </summary>
		[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
		struct VertexData
		{
			public Vector3 pos;
			public Vector3 nor;
			public Vector2 uv;
		}

		private Mesh _mesh;
		private void Start()
		{
			Debug.Log(this.gameObject.name);

			_skinnedMeshRenderer = GetComponent<SkinnedMeshRenderer>();
			_meshCollider = GetComponent<MeshCollider>();

			if (_skinnedMeshRenderer == null)
			{
				return;
			}
			
			//頂点座標の入れ物であるMeshを一回Bakeする
			var mesh = _skinnedMeshRenderer.sharedMesh;
			_mesh = new Mesh();
			_mesh.name = "skinmesh";
			_skinnedMeshRenderer.BakeMesh(_mesh);
			
			//Computeshaderに
			_vertData = new NativeArray<VertexData>(mesh.vertexCount, Allocator.Temp);
			for (int i = 0; i < mesh.vertexCount; ++i)
			{
				VertexData v = new VertexData();
				v.pos = mesh.vertices[i];
				v.nor = mesh.normals[i];
				v.uv = mesh.uv[i];
				_vertData[i] = v;
			}
			var layout = new[] 
			{
				new VertexAttributeDescriptor(VertexAttribute.Position, _mesh.GetVertexAttributeFormat(VertexAttribute.Position), 3),
				new VertexAttributeDescriptor(VertexAttribute.Normal, _mesh.GetVertexAttributeFormat(VertexAttribute.Normal), 3),
				new VertexAttributeDescriptor(VertexAttribute.TexCoord0, _mesh.GetVertexAttributeFormat(VertexAttribute.TexCoord0), 2),

			};
			_mesh.SetVertexBufferParams(mesh.vertexCount, layout);
			_vertBuffer = new ComputeBuffer(mesh.vertexCount, 8*4);

			if (_vertData.IsCreated) _vertBuffer.SetData(_vertData);
			
			//computeshaderをロード。
			_bakePointComputeShader = Instantiate(Resources.Load<ComputeShader>("SkinMeshtoMesh")) as ComputeShader;
			_kernel = _bakePointComputeShader.FindKernel("CSMain");
			uint threadX = 0;
			uint threadY = 0;
			uint threadZ = 0;
			_bakePointComputeShader.GetKernelThreadGroupSizes(_kernel, out threadX, out threadY, out threadZ);
			_dispatchCount = Mathf.CeilToInt(mesh.vertexCount / threadX + 1);

			_bakePointComputeShader.SetBuffer(_kernel, "vertexBuffer", _vertBuffer);
			
			//頂点バッファをRawとして定義。下記マニュアル参照
			//https://docs.unity3d.com/2021.2/Documentation/ScriptReference/SkinnedMeshRenderer-vertexBufferTarget.html
			_skinnedMeshRenderer.vertexBufferTarget |= GraphicsBuffer.Target.Raw;

			
			_request = AsyncGPUReadback.Request(_vertBuffer);
		}

		private void Update()
		{
			if (_skinnedMeshRenderer == null)
			{
				return;
			}
			var buffer = _skinnedMeshRenderer.GetVertexBuffer();
			if (buffer == null)
			{
				//何故か起動直後の何フレームかはVertexBufferが取れない
				Debug.Log("No VertexBuffer");
				return;
			}
			_bakePointComputeShader.SetBuffer(_kernel, "Verts", buffer);
			_bakePointComputeShader.SetMatrix("LocalToWorld", _skinnedMeshRenderer.worldToLocalMatrix * _skinnedMeshRenderer.rootBone.localToWorldMatrix);
			_bakePointComputeShader.Dispatch(_kernel, _dispatchCount, 1,1);

			buffer.Release();

			if (_request.done && !_request.hasError)
			{
				//Readback and show result on texture
				_vertData = _request.GetData<VertexData>();

				//Update mesh
				_mesh.MarkDynamic();
				_mesh.SetVertexBufferData(_vertData, 0, 0, _vertData.Length);
				_mesh.RecalculateNormals();

				//Update to collider
				_meshCollider.sharedMesh = _mesh;

				//Request AsyncReadback again
				_request = AsyncGPUReadback.Request(_vertBuffer);
			}
			
		}

		void Release()
		{
			if(_vertBuffer!=null) _vertBuffer.Release();
			if (_bakePointComputeShader != null)
			{
				Destroy(_bakePointComputeShader);
			}

			_skinnedMeshRenderer = null;
			_meshCollider = null;
		}

		private void OnDestroy()
		{
			Release();
		}
	}

```

コンピュートシェーダーのソースが以下になります。

```csharp:SkinMeshtoMesh.compute
#pragma kernel CSMain

struct VertexData
{
    float3 pos;
    float3 nor;
    float2 uv;
};

ByteAddressBuffer Verts;

float4x4 LocalToWorld;

RWStructuredBuffer<VertexData> vertexBuffer;

[numthreads(32, 1, 1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    //https://github.com/cinight/MinimalCompute/blob/master/Assets/SkinnedMeshBuffer/SkinnedMeshBuffer.shader
    //のGetVertexData_Positionの処理を参考にしました。
    
    //layout for vertex buffer (observed by using RenderDoc):
    //float3 position
    //float3 normal
    //float4 tangent
    //therefore total 10 floats and 4 bytes each = 10*4 = 40
    int vidx = id.x * 40;
    uint3 praw = Verts.Load3(vidx);
    float3 lvert = asfloat(praw);
    float4 vert = mul(LocalToWorld, float4(lvert, 1));
    vertexBuffer[id.x].pos = float3(vert.xyz);
}

```


# できる事、出来ないこと
* 出来る事
 *  デモのように変形したMeshColliderとPhysicsとの相互作用がとれます。SkinnedMeshRendererの形状で当たり判定がとれるので、以前のようにMeshの形状に合うようにBoneにコライダーをつける方法よりも正確な当たり判定になります。
 *  Raycastで当たり判定がとれます。

* 出来ない事
 * RigidBodyはisKinematic=trueしか使えません。isKinematic=falseにするとエラーを吐きます。
 * MeshColliderのconvex=trueにできないので、IsTrigger=trueにできません。つまりOnTrrigerで当たり判定をとるのは無理です。
 * 早い動きにMeshColliderが追従出来ません。若干ディレイを伴っています。