概要
Unity2021.2以降からMeshAPIでMeshのvertexbufferをComputeshaderで編集できるようになってます。これを利用したSkinnedMeshRendererの変形に追従するMeshColliderのデモが存在しなかったので作ってみました。
リアルタイムMeshCollider変形デモ
アドベントカレンダー用のサンプルの作成をしている。新Mesh APIを利用しているMeshColliderをランタイムで変形させるデモ。#Unity pic.twitter.com/M6uEiu6uqn
— unagi (@UnagiHuman) December 20, 2021
デモはこちらのレポジトリに用意しました。
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
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();
}
}
コンピュートシェーダーのソースが以下になります。
#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が追従出来ません。若干ディレイを伴っています。