diff --git a/Assets/VRM/UniVRM/Editor/Tests/NormalizeTests.cs b/Assets/VRM/UniVRM/Editor/Tests/NormalizeTests.cs new file mode 100644 index 000000000..51510cfb7 --- /dev/null +++ b/Assets/VRM/UniVRM/Editor/Tests/NormalizeTests.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using UnityEngine; + +namespace VRM +{ + public class NormalizeTests + { + class BoneMap + { + public List SrcBones = new List(); + public List DstBones = new List(); + public Dictionary Map = new Dictionary(); + + public void Add(GameObject src, GameObject dst) + { + SrcBones.Add(src?.transform); + if (dst != null) + { + DstBones.Add(dst.transform); + } + if (src != null) + { + Map.Add(src?.transform, dst?.transform); + } + } + + public IEnumerable CreateBoneWeight(int vertexCount) + { + int j = 0; + for (int i = 0; i < vertexCount; ++i) + { + yield return new BoneWeight + { + boneIndex0 = j++, + boneIndex1 = j++, + boneIndex2 = j++, + boneIndex3 = j++, + weight0 = 0.25f, + weight1 = 0.25f, + weight2 = 0.25f, + weight3 = 0.25f, + }; + } + } + } + + [Test] + public void MapBoneWeightTest() + { + { + var map = new BoneMap(); + map.Add(new GameObject("a"), new GameObject("A")); + map.Add(new GameObject("b"), new GameObject("B")); + map.Add(new GameObject("c"), new GameObject("C")); + map.Add(new GameObject("d"), new GameObject("D")); + map.Add(null, new GameObject("null")); + // map.Add(new GameObject("c"), null); // ありえないので Exception にしてある + var boneWeights = map.CreateBoneWeight(64).ToArray(); + var newBoneWeight = BoneNormalizer.MapBoneWeight(boneWeights, map.Map, + map.SrcBones.ToArray(), map.DstBones.ToArray()); + + // 正常系 + // exception が出なければよい + } + + { + var map = new BoneMap(); + map.Add(new GameObject("a"), new GameObject("A")); + map.Add(new GameObject("b"), new GameObject("B")); + map.Add(new GameObject("c"), new GameObject("C")); + map.Add(new GameObject("d"), new GameObject("D")); + map.Add(null, new GameObject("null")); + // map.Add(new GameObject("c"), null); // ありえないので Exception にしてある + var boneWeights = map.CreateBoneWeight(64).ToArray(); + var newBoneWeight = BoneNormalizer.MapBoneWeight(boneWeights, map.Map, + map.SrcBones.ToArray(), map.DstBones.ToArray()); + + // 4 つめが 0 になる + Assert.AreEqual(0, newBoneWeight[1].boneIndex0); + Assert.AreEqual(0, newBoneWeight[1].weight0); + // 5 つめ以降が 0 になる。out of range + Assert.AreEqual(0, newBoneWeight[1].boneIndex1); + Assert.AreEqual(0, newBoneWeight[1].weight1); + } + } + } +} diff --git a/Assets/VRM/UniVRM/Editor/Tests/NormalizeTests.cs.meta b/Assets/VRM/UniVRM/Editor/Tests/NormalizeTests.cs.meta new file mode 100644 index 000000000..b3822bbd4 --- /dev/null +++ b/Assets/VRM/UniVRM/Editor/Tests/NormalizeTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4014814a6ed5bf84faa36c7b99d6d4b8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/VRM/UniVRM/Scripts/SkinnedMeshUtility/BoneNormalizer.cs b/Assets/VRM/UniVRM/Scripts/SkinnedMeshUtility/BoneNormalizer.cs index 295561774..b4b51ee25 100644 --- a/Assets/VRM/UniVRM/Scripts/SkinnedMeshUtility/BoneNormalizer.cs +++ b/Assets/VRM/UniVRM/Scripts/SkinnedMeshUtility/BoneNormalizer.cs @@ -172,90 +172,117 @@ namespace VRM } } - static BoneWeight[] MapBoneWeight(BoneWeight[] src, + /// + /// index が 有効であれば、setter に weight を渡す。無効であれば setter に 0 を渡す。 + /// + /// + /// + /// + /// + static bool CopyOrDropWeight(int[] indexMap, int srcIndex, float weight, Action setter) + { + if (srcIndex < 0 || srcIndex >= indexMap.Length) + { + // ありえるかどうかわからないが BoneWeight.boneIndexN に変な値が入っている. + setter(0, 0); + return false; + } + + var dstIndex = indexMap[srcIndex]; + if (dstIndex != -1) + { + // 有効なindex。weightをコピーする + setter(dstIndex, weight); + return true; + } + else + { + // 無効なindex。0でクリアする + setter(0, 0); + return false; + } + } + + /// + /// BoneWeight[] src から新しいボーンウェイトを作成する。 + /// + /// 変更前のBoneWeight[] + /// 新旧のボーンの対応表。新しい方は無効なボーンが除去されてnullの部分がある + /// 変更前のボーン配列 + /// 変更後のボーン配列。除去されたボーンがある場合、変更前より短い + /// + public static BoneWeight[] MapBoneWeight(BoneWeight[] src, Dictionary boneMap, Transform[] srcBones, Transform[] dstBones ) { - var indexMap = - srcBones - .Select((x, i) => new { i, x }) - .Select(x => + // 処理前後の index の対応表を作成する + var indexMap = new int[srcBones.Length]; + for (int i = 0; i < srcBones.Length; ++i) + { + var srcBone = srcBones[i]; + if (srcBone == null) { - Transform dstBone; - if (boneMap.TryGetValue(x.x, out dstBone)) + // 元のボーンが無い + indexMap[i] = -1; + Debug.LogWarningFormat("bones[{0}] is null", i); + } + else + { + if (boneMap.TryGetValue(srcBone, out Transform dstBone)) { - return dstBones.IndexOf(dstBone); + // 対応するボーンが存在する + var dstIndex = dstBones.IndexOf(dstBone); + if (dstIndex == -1) + { + // ありえない。バグ + throw new Exception(); + } + indexMap[i] = dstIndex; } else { - return -1; + // 先のボーンが無い + indexMap[i] = -1; + Debug.LogWarningFormat("{0} is removed", srcBone.name); } - }) - .ToArray(); - - for (int i = 0; i < srcBones.Length; ++i) - { - if (indexMap[i] < 0) - { - Debug.LogWarningFormat("{0} is removed", srcBones[i].name); } } - var dst = new BoneWeight[src.Length]; - Array.Copy(src, dst, src.Length); - + // 新しいBoneWeightを作成する + var newBoneWeights = new BoneWeight[src.Length]; for (int i = 0; i < src.Length; ++i) { - var x = src[i]; + BoneWeight srcBoneWeight = src[i]; - if (indexMap[x.boneIndex0] != -1) + // 0 + CopyOrDropWeight(indexMap, srcBoneWeight.boneIndex0, srcBoneWeight.weight0, (newIndex, newWeight) => { - dst[i].boneIndex0 = indexMap[x.boneIndex0]; - dst[i].weight0 = x.weight0; - } - else if (x.weight0 > 0) + newBoneWeights[i].boneIndex0 = newIndex; + newBoneWeights[i].weight0 = newWeight; + }); + // 1 + CopyOrDropWeight(indexMap, srcBoneWeight.boneIndex1, srcBoneWeight.weight1, (newIndex, newWeight) => { - Debug.LogWarningFormat("{0} weight0 to {1} is lost", i, srcBones[x.boneIndex0].name); - dst[i].weight0 = 0; - } - - if (indexMap[x.boneIndex1] != -1) + newBoneWeights[i].boneIndex1 = newIndex; + newBoneWeights[i].weight1 = newWeight; + }); + // 2 + CopyOrDropWeight(indexMap, srcBoneWeight.boneIndex2, srcBoneWeight.weight2, (newIndex, newWeight) => { - dst[i].boneIndex1 = indexMap[x.boneIndex1]; - dst[i].weight1 = x.weight1; - } - else if (x.weight1 > 0) + newBoneWeights[i].boneIndex2 = newIndex; + newBoneWeights[i].weight2 = newWeight; + }); + // 3 + CopyOrDropWeight(indexMap, srcBoneWeight.boneIndex3, srcBoneWeight.weight3, (newIndex, newWeight) => { - Debug.LogWarningFormat("{0} weight0 to {1} is lost", i, srcBones[x.boneIndex1].name); - dst[i].weight1 = 0; - } - - if (indexMap[x.boneIndex2] != -1) - { - dst[i].boneIndex2 = indexMap[x.boneIndex2]; - dst[i].weight2 = x.weight2; - } - else if (x.weight2 > 0) - { - Debug.LogWarningFormat("{0} weight0 to {1} is lost", i, srcBones[x.boneIndex2].name); - dst[i].weight2 = 0; - } - - if (indexMap[x.boneIndex3] != -1) - { - dst[i].boneIndex3 = indexMap[x.boneIndex3]; - dst[i].weight3 = x.weight3; - } - else if (x.weight3 > 0) - { - Debug.LogWarningFormat("{0} weight0 to {1} is lost", i, srcBones[x.boneIndex3].name); - dst[i].weight3 = 0; - } + newBoneWeights[i].boneIndex3 = newIndex; + newBoneWeights[i].weight3 = newWeight; + }); } - return dst; + return newBoneWeights; } /// @@ -288,10 +315,12 @@ namespace VRM } } + // 元の Transform[] bones から、無効なboneを取り除いて前に詰めた配列を作る var dstBones = srcRenderer.bones - .Where(x => boneMap.ContainsKey(x)) + .Where(x => x != null && boneMap.ContainsKey(x)) .Select(x => boneMap[x]) .ToArray(); + var hasBoneWeight = srcRenderer.bones != null && srcRenderer.bones.Length > 0; if (!hasBoneWeight) { @@ -331,7 +360,8 @@ namespace VRM if (val > 0) blendShapeValues.Add(i, val); } - mesh.boneWeights = MapBoneWeight(srcMesh.boneWeights, boneMap, srcRenderer.bones, dstBones); // restore weights. clear when BakeMesh + // 新しい骨格のボーンウェイトを作成する + mesh.boneWeights = MapBoneWeight(srcMesh.boneWeights, boneMap, srcRenderer.bones, dstBones); // recalc bindposes mesh.bindposes = dstBones.Select(x => x.worldToLocalMatrix * dst.transform.localToWorldMatrix).ToArray();