Skip to content

Commit

Permalink
Merge pull request #468 from ousttrue/fix_normalize
Browse files Browse the repository at this point in the history
fix normalize if bones contains null
  • Loading branch information
hiroj committed Jul 8, 2020
2 parents 2fe0132 + 6d61db7 commit 3b68eb7
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 63 deletions.
89 changes: 89 additions & 0 deletions 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<Transform> SrcBones = new List<Transform>();
public List<Transform> DstBones = new List<Transform>();
public Dictionary<Transform, Transform> Map = new Dictionary<Transform, Transform>();

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<BoneWeight> 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);
}
}
}
}
11 changes: 11 additions & 0 deletions Assets/VRM/UniVRM/Editor/Tests/NormalizeTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

156 changes: 93 additions & 63 deletions Assets/VRM/UniVRM/Scripts/SkinnedMeshUtility/BoneNormalizer.cs
Expand Up @@ -172,90 +172,117 @@ public override string ToString()
}
}

static BoneWeight[] MapBoneWeight(BoneWeight[] src,
/// <summary>
/// index が 有効であれば、setter に weight を渡す。無効であれば setter に 0 を渡す。
/// </summary>
/// <param name="indexMap"></param>
/// <param name="srcIndex"></param>
/// <param name="weight"></param>
/// <param name="setter"></param>
static bool CopyOrDropWeight(int[] indexMap, int srcIndex, float weight, Action<int, float> 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;
}
}

/// <summary>
/// BoneWeight[] src から新しいボーンウェイトを作成する。
/// </summary>
/// <param name="src">変更前のBoneWeight[]</param>
/// <param name="boneMap">新旧のボーンの対応表。新しい方は無効なボーンが除去されてnullの部分がある</param>
/// <param name="srcBones">変更前のボーン配列</param>
/// <param name="dstBones">変更後のボーン配列。除去されたボーンがある場合、変更前より短い</param>
/// <returns></returns>
public static BoneWeight[] MapBoneWeight(BoneWeight[] src,
Dictionary<Transform, Transform> 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];

if (indexMap[x.boneIndex0] != -1)
{
dst[i].boneIndex0 = indexMap[x.boneIndex0];
dst[i].weight0 = x.weight0;
}
else if (x.weight0 > 0)
{
Debug.LogWarningFormat("{0} weight0 to {1} is lost", i, srcBones[x.boneIndex0].name);
dst[i].weight0 = 0;
}
BoneWeight srcBoneWeight = src[i];

if (indexMap[x.boneIndex1] != -1)
{
dst[i].boneIndex1 = indexMap[x.boneIndex1];
dst[i].weight1 = x.weight1;
}
else if (x.weight1 > 0)
// 0
CopyOrDropWeight(indexMap, srcBoneWeight.boneIndex0, srcBoneWeight.weight0, (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)
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.boneIndex2].name);
dst[i].weight2 = 0;
}

if (indexMap[x.boneIndex3] != -1)
newBoneWeights[i].boneIndex1 = newIndex;
newBoneWeights[i].weight1 = newWeight;
});
// 2
CopyOrDropWeight(indexMap, srcBoneWeight.boneIndex2, srcBoneWeight.weight2, (newIndex, newWeight) =>
{
dst[i].boneIndex3 = indexMap[x.boneIndex3];
dst[i].weight3 = x.weight3;
}
else if (x.weight3 > 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.boneIndex3].name);
dst[i].weight3 = 0;
}
newBoneWeights[i].boneIndex3 = newIndex;
newBoneWeights[i].weight3 = newWeight;
});
}

return dst;
return newBoneWeights;
}

/// <summary>
Expand Down Expand Up @@ -288,10 +315,12 @@ static void NormalizeSkinnedMesh(Transform src, Transform dst, Dictionary<Transf
}
}

// 元の 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)
{
Expand Down Expand Up @@ -331,7 +360,8 @@ static void NormalizeSkinnedMesh(Transform src, Transform dst, Dictionary<Transf
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();
Expand Down

0 comments on commit 3b68eb7

Please sign in to comment.