Skip to content

Commit

Permalink
Merge pull request #1374 from anatawa12/animation-without-m-broken
Browse files Browse the repository at this point in the history
blendshapes with sane name in one model
  • Loading branch information
anatawa12 authored Dec 22, 2024
2 parents f4beb31 + 80bc8eb commit b971351
Show file tree
Hide file tree
Showing 8 changed files with 316 additions and 9 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG-PRERELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ The format is based on [Keep a Changelog].
- Prefab overrides on the scene are reverted on first load of the scene at first launch `#1372`
- Animating transform with C# named properties are broken by merge bone `#1373`
- Animator window won't create such animation but some script generates and it works surprisingly
- Errors with blendShapes with exactly same name in a mesh `#1374`
- Such mesh can be generated with Autodesk Maya or 3ds Max
- Unity API denies generating such mesh with C# so AAO will rename such blendShapes to unique name to support.
- Unity Animator does animate first blendshale only so second shape would generally removed by remove unused blendShapes.

### Security

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ The format is based on [Keep a Changelog].
- Prefab overrides on the scene are reverted on first load of the scene at first launch `#1372`
- Animating transform with C# named properties are broken by merge bone `#1373`
- Animator window won't create such animation but some script generates and it works surprisingly
- Errors with blendShapes with exactly same name in a mesh `#1374`
- Such mesh can be generated with Autodesk Maya or 3ds Max
- Unity API denies generating such mesh with C# so AAO will rename such blendShapes to unique name to support.
- Unity Animator does animate first blendshale only so second shape would generally removed by remove unused blendShapes.

### Security

Expand Down
45 changes: 36 additions & 9 deletions Internal/MeshInfo2/MeshInfo2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using UnityEngine.Rendering;
using Debug = System.Diagnostics.Debug;
using Object = UnityEngine.Object;
using Random = UnityEngine.Random;

namespace Anatawa12.AvatarOptimizer.Processors.SkinnedMeshes
{
Expand Down Expand Up @@ -201,14 +202,9 @@ private void ReadBones(Mesh mesh)

private void ReadBlendShapes(Mesh mesh)
{
BlendShapes.Clear();
Profiler.BeginSample("Save Applied Weights");
for (var blendShape = 0; blendShape < mesh.blendShapeCount; blendShape++)
BlendShapes.Add((mesh.GetBlendShapeName(blendShape), 0.0f));
Profiler.EndSample();

Profiler.BeginSample("New Reading Method");
var buffer = new BlendShapeBuffer(mesh);
BlendShapes.Clear();
var buffer = new BlendShapeBuffer(mesh, BlendShapes);
for (var vertex = 0; vertex < Vertices.Count; vertex++)
{
Vertices[vertex].BlendShapeBuffer = buffer;
Expand Down Expand Up @@ -1203,7 +1199,7 @@ public class BlendShapeBuffer : IReferenceCount
public readonly NativeArray<Vector3>[] DeltaTangents;
public readonly int VertexCount;

public BlendShapeBuffer(Mesh sourceMesh)
public BlendShapeBuffer(Mesh sourceMesh, List<(string name, float weight)> blendShapes)
{
Profiler.BeginSample("BlendShapeBuffer:Create");
var totalFrames = 0;
Expand Down Expand Up @@ -1254,12 +1250,43 @@ public BlendShapeBuffer(Mesh sourceMesh)
Profiler.EndSample();
}

Shapes.Add(name, new BlendShapeShape(frameInfos));
if (!Shapes.TryAdd(name, new BlendShapeShape(frameInfos)))
{
// duplicated blendShape name detected.
// This can be generated with 3ds Max or other tools.
// Rename blendShape a little to avoid conflict.
name = $"{name}-nameConflict-{GetShortRandom()}";
Shapes.Add(name, new BlendShapeShape(frameInfos));
}
blendShapes.Add((name, 0.0f));
Profiler.EndSample();
}
Profiler.EndSample();
}

private static string GetShortRandom()
{
// generate 4-char base64 string
// with 4 of 64 characters, it has 64^4 = 16777216 possibilities.
// When we create 100 times, the possibility of collision is one in about 3390
var chars = new char[4];

for (var i = 0; i < chars.Length; i++)
chars[i] = Base64Char(Random.Range(0, 64));
return new string(chars);

static char Base64Char(int value) => value switch
{
< 0 => throw new ArgumentOutOfRangeException(nameof(value), $"{value}"),
< 10 => (char)('0' + value),
< 36 => (char)('A' + value - 10),
< 62 => (char)('a' + value - 36),
62 => '-',
63 => '_',
_ => throw new ArgumentOutOfRangeException(nameof(value), $"{value}"),
};
}

// create empty
private BlendShapeBuffer()
{
Expand Down
54 changes: 54 additions & 0 deletions Test~/MeshInfo2/MeshInfo2Test.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,5 +217,59 @@ public void ComputeActualPositionWithBones()
Assert.That(position, Is.EqualTo(vertex.Position));
}
}

// test with binary-edited fbx which is originally exported from blender
[Test]
public void MultipleSameNameBlendShapeBlenderBinaryEdited()
{
var fbx = TestUtils.GetAssetAt<GameObject>("MeshInfo2/same-name-blendshape-blender-binary-edited.fbx");
var renderer = fbx.GetComponent<SkinnedMeshRenderer>();
var mesh = renderer.sharedMesh;

// check the mesh has same name blendShape
Assert.That(mesh.blendShapeCount, Is.EqualTo(2));
Assert.That(mesh.GetBlendShapeName(0), Is.EqualTo("BlendShape1"));
Assert.That(mesh.GetBlendShapeName(1), Is.EqualTo("BlendShape1"));

using var meshInfo2 = new MeshInfo2(renderer);
// we've checked no exception is thrown

// second shape is renamed
Assert.That(meshInfo2.BlendShapes[0].name, Is.EqualTo("BlendShape1"));
Assert.That(meshInfo2.BlendShapes[1].name, Does.StartWith("BlendShape1-nameConflict-"));
// and there is buffer for each vertex
foreach (var vertex in meshInfo2.Vertices)
{
Assert.That(vertex.BlendShapeBuffer.Shapes[meshInfo2.BlendShapes[0].name], Is.Not.Null);
Assert.That(vertex.BlendShapeBuffer.Shapes[meshInfo2.BlendShapes[1].name], Is.Not.Null);
}
}

// test with real fbx
[Test]
public void MultipleSameNameBlendShape3dsMax()
{
var fbx = TestUtils.GetAssetAt<GameObject>("MeshInfo2/same-name-blendshape-3ds-max.fbx");
var renderer = fbx.transform.Find("Box001").GetComponent<SkinnedMeshRenderer>();
var mesh = renderer.sharedMesh;

// check the mesh has same name blendShape
Assert.That(mesh.blendShapeCount, Is.EqualTo(2));
Assert.That(mesh.GetBlendShapeName(0), Is.EqualTo("Shape"));
Assert.That(mesh.GetBlendShapeName(1), Is.EqualTo("Shape"));

using var meshInfo2 = new MeshInfo2(renderer);
// we've checked no exception is thrown

// second shape is renamed
Assert.That(meshInfo2.BlendShapes[0].name, Is.EqualTo("Shape"));
Assert.That(meshInfo2.BlendShapes[1].name, Does.StartWith("Shape-nameConflict-"));
// and there is buffer for each vertex
foreach (var vertex in meshInfo2.Vertices)
{
Assert.That(vertex.BlendShapeBuffer.Shapes[meshInfo2.BlendShapes[0].name], Is.Not.Null);
Assert.That(vertex.BlendShapeBuffer.Shapes[meshInfo2.BlendShapes[1].name], Is.Not.Null);
}
}
}
}
Binary file added Test~/MeshInfo2/same-name-blendshape-3ds-max.fbx
Binary file not shown.
109 changes: 109 additions & 0 deletions Test~/MeshInfo2/same-name-blendshape-3ds-max.fbx.meta

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

Binary file not shown.
109 changes: 109 additions & 0 deletions Test~/MeshInfo2/same-name-blendshape-blender-binary-edited.fbx.meta

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

0 comments on commit b971351

Please sign in to comment.