Skip to content

Instantly share code, notes, and snippets.

@mkc1370
Created September 7, 2023 02:28
Show Gist options
  • Save mkc1370/665c29e0b76d3d81bf45f4c6ff303641 to your computer and use it in GitHub Desktop.
Save mkc1370/665c29e0b76d3d81bf45f4c6ff303641 to your computer and use it in GitHub Desktop.
SkinnedMeshRendererのスキニングの処理を再現するコンポーネント SkinnedMeshRendererと同じGameObjectにアタッチすると動作する
/*
MIT License
Copyright (c) 2023 mkc1370
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
using System;
using UnityEngine;
[ExecuteAlways]
[RequireComponent(typeof(SkinnedMeshRenderer))]
public class MySkinnedMeshRenderer : MonoBehaviour
{
[SerializeField] private SkinnedMeshRenderer referenceSkinnedMeshRenderer;
[SerializeField] private bool drawBoneGizmo;
private Mesh _currentMesh;
private Vector3[] _currentVertices;
private Vector3[] _currentNormals;
private Vector3[] _bindPoseVertices;
private Vector3[] _bindPoseNormals;
private Transform[] _bones;
private BoneWeight[] _boneWeights;
private Matrix4x4[] _bindPoseBoneInverseMatrices;
private void Start()
{
referenceSkinnedMeshRenderer.enabled = false;
_currentMesh = Instantiate(referenceSkinnedMeshRenderer.sharedMesh);
// 頻繁にメッシュを更新するので動的バッファを使用するように指定する
_currentMesh.MarkDynamic();
_currentVertices = _currentMesh.vertices;
_currentNormals = _currentMesh.normals;
_bindPoseVertices = _currentMesh.vertices;
_bindPoseNormals = _currentMesh.normals;
_bones = referenceSkinnedMeshRenderer.bones;
_boneWeights = _currentMesh.boneWeights;
_bindPoseBoneInverseMatrices = _currentMesh.bindposes;
}
private void OnDestroy()
{
DestroyImmediate(_currentMesh);
}
private void LateUpdate()
{
// 現在のボーンの変換行列をキャッシュ
// 必要に応じてスキニングの処理をマルチスレッドで行えるようになる
var currentBoneMatrices = new Matrix4x4[_bones.Length];
for (var i = 0; i < _bones.Length; i++)
{
currentBoneMatrices[i] = _bones[i].localToWorldMatrix;
}
// LBS(Linear Blend Skinning)でのスキニング
for (var vertexIndex = 0; vertexIndex < _currentVertices.Length; vertexIndex++)
{
// 初期化
_currentVertices[vertexIndex] = Vector3.zero;
_currentNormals[vertexIndex] = Vector3.zero;
// 1頂点あたりのボーンのインフルエンス数を5個以上にするとAPI的に処理が面倒なので、簡単のために4個までに制限
var maxBoneWeightCount = 4;
for (var i = 0; i < maxBoneWeightCount; i++)
{
// 頂点に影響するボーンの情報を取得
// forで回すためにGetBoneWeightでラップしている
var (boneIndex, weight) = GetBoneWeight(vertexIndex, i);
var bindPoseBoneInverseMatrix = _bindPoseBoneInverseMatrices[boneIndex];
var currentBoneMatrix = currentBoneMatrices[boneIndex];
// 頂点の位置のブレンド
var bindPoseVertex = _bindPoseVertices[vertexIndex];
_currentVertices[vertexIndex] += CalcVertex(currentBoneMatrix, bindPoseBoneInverseMatrix, bindPoseVertex) * weight;
// 頂点の法線のブレンド
var bindPoseNormal = _bindPoseNormals[vertexIndex];
_currentNormals[vertexIndex] += CalcNormal(currentBoneMatrix, bindPoseBoneInverseMatrix, bindPoseNormal) * weight;
}
}
// メッシュに反映
_currentMesh.vertices = _currentVertices;
_currentMesh.normals = _currentNormals;
// メッシュを描画
for (var i = 0; i < _currentMesh.subMeshCount; i++)
{
Graphics.DrawMesh(_currentMesh, Matrix4x4.identity, referenceSkinnedMeshRenderer.sharedMaterials[i], gameObject.layer, null, i);
}
}
private (int boneIndex, float weight) GetBoneWeight(int vertexIndex, int weightIndex)
{
var boneWeightInfo = _boneWeights[vertexIndex];
switch (weightIndex)
{
case 0:
return (boneWeightInfo.boneIndex0, boneWeightInfo.weight0);
case 1:
return (boneWeightInfo.boneIndex1, boneWeightInfo.weight1);
case 2:
return (boneWeightInfo.boneIndex2, boneWeightInfo.weight2);
case 3:
return (boneWeightInfo.boneIndex3, boneWeightInfo.weight3);
default:
throw new ArgumentOutOfRangeException();
}
}
private Vector3 CalcVertex(Matrix4x4 currentBoneMatrix, Matrix4x4 bindPoseBoneInverseMatrix, Vector3 bindPoseVertexInWorldSpace)
{
// バインドポーズ時の頂点の位置をボーン空間に変換
// (ボーンが原点で上を向いている状態に移動・回転させる)
var bindPoseVertexInBoneSpace = bindPoseBoneInverseMatrix.MultiplyPoint3x4(bindPoseVertexInWorldSpace);
// 現在の頂点の位置をワールド空間に変換
// (原点で上を向いている状態のボーンを現在のボーンの状態まで移動・回転させる)
var currentVertexInWorldSpace = currentBoneMatrix.MultiplyPoint3x4(bindPoseVertexInBoneSpace);
return currentVertexInWorldSpace;
}
private Vector3 CalcNormal(Matrix4x4 currentBoneMatrix, Matrix4x4 bindPoseBoneInverseMatrix, Vector3 bindPoseNormalInWorldSpace)
{
// バインドポーズ時の法線をボーン空間に変換
// (ボーンが上を向いている状態に回転させる)
var bindPoseNormalInBoneSpace = bindPoseBoneInverseMatrix.MultiplyVector(bindPoseNormalInWorldSpace);
// 現在の法線をワールド空間に変換
// (上を向いている状態のボーンを現在のボーンの状態まで回転させる)
var currentNormalInWorldSpace = currentBoneMatrix.MultiplyVector(bindPoseNormalInBoneSpace);
return currentNormalInWorldSpace;
}
private void OnDrawGizmosSelected()
{
if (!drawBoneGizmo) return;
foreach (var bindPoseBoneInverseMatrix in _bindPoseBoneInverseMatrices)
{
Gizmos.matrix = transform.localToWorldMatrix * bindPoseBoneInverseMatrix.inverse;
DrawPyramid(Vector3.zero, 0.05f, 0.2f);
}
}
private void DrawPyramid(Vector3 position, float baseSideLength, float height)
{
// 頂点を定義
var top = position + Vector3.up * height;
var base0 = position + new Vector3(-baseSideLength / 2, 0, -baseSideLength / 2);
var base1 = position + new Vector3(baseSideLength / 2, 0, -baseSideLength / 2);
var base2 = position + new Vector3(baseSideLength / 2, 0, baseSideLength / 2);
var base3 = position + new Vector3(-baseSideLength / 2, 0, baseSideLength / 2);
// 側辺を描画
Gizmos.DrawLine(top, base0);
Gizmos.DrawLine(top, base1);
Gizmos.DrawLine(top, base2);
Gizmos.DrawLine(top, base3);
// 底辺を描画
Gizmos.DrawLine(base0, base1);
Gizmos.DrawLine(base1, base2);
Gizmos.DrawLine(base2, base3);
Gizmos.DrawLine(base3, base0);
}
private void Reset()
{
referenceSkinnedMeshRenderer = GetComponent<SkinnedMeshRenderer>();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment