Skip to content

Commit 97f100b

Browse files
authored
Merge pull request #277 from unity3d-jp/blendshape_support
Uni-35840-Blendshape support
2 parents 4545060 + 3a43326 commit 97f100b

File tree

7 files changed

+377
-18
lines changed

7 files changed

+377
-18
lines changed

Assets/FbxExporters/Editor/FbxExporter.cs

Lines changed: 98 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,86 @@ private static bool ExportUVs(FbxMesh fbxMesh, MeshInfo mesh, int[] unmergedTria
426426
return k > 0;
427427
}
428428

429+
/// <summary>
430+
/// Export the mesh's blend shapes.
431+
/// </summary>
432+
private bool ExportBlendShapes(MeshInfo mesh, FbxMesh fbxMesh, FbxScene fbxScene, int[] unmergedTriangles)
433+
{
434+
var umesh = mesh.mesh;
435+
if (umesh.blendShapeCount == 0)
436+
return false;
437+
438+
var fbxBlendShape = FbxBlendShape.Create(fbxScene, umesh.name + "_BlendShape");
439+
fbxMesh.AddDeformer(fbxBlendShape);
440+
441+
var numVertices = umesh.vertexCount;
442+
var basePoints = umesh.vertices;
443+
var baseNormals = umesh.normals;
444+
var baseTangents = umesh.tangents;
445+
var deltaPoints = new Vector3[numVertices];
446+
var deltaNormals = new Vector3[numVertices];
447+
var deltaTangents = new Vector3[numVertices];
448+
449+
for (int bi = 0; bi < umesh.blendShapeCount; ++bi)
450+
{
451+
var bsName = umesh.GetBlendShapeName(bi);
452+
var numFrames = umesh.GetBlendShapeFrameCount(bi);
453+
var fbxChannel = FbxBlendShapeChannel.Create(fbxScene, bsName);
454+
fbxBlendShape.AddBlendShapeChannel(fbxChannel);
455+
456+
for (int fi = 0; fi < numFrames; ++fi)
457+
{
458+
var weight = umesh.GetBlendShapeFrameWeight(bi, fi);
459+
umesh.GetBlendShapeFrameVertices(bi, fi, deltaPoints, deltaNormals, deltaTangents);
460+
461+
var fbxShape = FbxShape.Create(fbxScene, "");
462+
fbxChannel.AddTargetShape(fbxShape, weight);
463+
464+
// control points
465+
fbxShape.InitControlPoints(ControlPointToIndex.Count());
466+
for (int vi = 0; vi < numVertices; ++vi)
467+
{
468+
int ni = ControlPointToIndex[basePoints[vi]];
469+
var v = basePoints[vi] + deltaPoints[vi];
470+
fbxShape.SetControlPointAt(ConvertToRightHanded(v, UnitScaleFactor), ni);
471+
}
472+
473+
// normals
474+
if (mesh.HasValidNormals())
475+
{
476+
var elemNormals = fbxShape.CreateElementNormal();
477+
elemNormals.SetMappingMode(FbxLayerElement.EMappingMode.eByPolygonVertex);
478+
elemNormals.SetReferenceMode(FbxLayerElement.EReferenceMode.eDirect);
479+
var dstNormals = elemNormals.GetDirectArray();
480+
dstNormals.SetCount(unmergedTriangles.Length);
481+
for (int ii = 0; ii < unmergedTriangles.Length; ++ii)
482+
{
483+
int vi = unmergedTriangles[ii];
484+
var n = baseNormals[vi] + deltaNormals[vi];
485+
dstNormals.SetAt(ii, ConvertToRightHanded(n));
486+
}
487+
}
488+
489+
// tangents
490+
if (mesh.HasValidTangents())
491+
{
492+
var elemTangents = fbxShape.CreateElementTangent();
493+
elemTangents.SetMappingMode(FbxLayerElement.EMappingMode.eByPolygonVertex);
494+
elemTangents.SetReferenceMode(FbxLayerElement.EReferenceMode.eDirect);
495+
var dstTangents = elemTangents.GetDirectArray();
496+
dstTangents.SetCount(unmergedTriangles.Length);
497+
for (int ii = 0; ii < unmergedTriangles.Length; ++ii)
498+
{
499+
int vi = unmergedTriangles[ii];
500+
var t = (Vector3)baseTangents[vi] + deltaTangents[vi];
501+
dstTangents.SetAt(ii, ConvertToRightHanded(t));
502+
}
503+
}
504+
}
505+
}
506+
return true;
507+
}
508+
429509
/// <summary>
430510
/// Takes in a left-handed UnityEngine.Vector3 denoting a normal,
431511
/// returns a right-handed FbxVector4.
@@ -754,6 +834,9 @@ bool ExportMesh (MeshInfo meshInfo, FbxNode fbxNode)
754834
// Set up normals, etc.
755835
ExportComponentAttributes (meshInfo, fbxMesh, unmergedPolygons.ToArray());
756836

837+
// Set up blend shapes.
838+
ExportBlendShapes(meshInfo, fbxMesh, fbxScene, unmergedPolygons.ToArray());
839+
757840
// set the fbxNode containing the mesh
758841
fbxNode.SetNodeAttribute (fbxMesh);
759842
fbxNode.SetShadingMode (FbxNode.EShadingMode.eWireFrame);
@@ -781,31 +864,28 @@ SkinnedMeshRenderer unitySkin
781864
Debug.Log (string.Format ("exporting {0} {1}", "Skin", fbxNode.GetName ()));
782865

783866

784-
Dictionary<SkinnedMeshRenderer, Transform[]> skinnedMeshToBonesMap;
785-
// export skeleton
786-
if (!ExportSkeleton (unitySkin, fbxScene, out skinnedMeshToBonesMap)) {
787-
Debug.LogWarning ("failed to export skeleton");
788-
return false;
789-
}
790-
791-
var meshInfo = new MeshInfo (unitySkin.sharedMesh, unitySkin.sharedMaterials);
867+
var meshInfo = new MeshInfo(unitySkin.sharedMesh, unitySkin.sharedMaterials);
792868

793-
// export skin mesh
794869
FbxMesh fbxMesh = null;
795-
if (ExportMesh (meshInfo, fbxNode)) {
796-
fbxMesh = fbxNode.GetMesh ();
870+
if (ExportMesh(meshInfo, fbxNode))
871+
{
872+
fbxMesh = fbxNode.GetMesh();
797873
}
798-
799-
if (fbxMesh == null) {
800-
Debug.LogError ("Could not find mesh");
874+
if (fbxMesh == null)
875+
{
876+
Debug.LogError("Could not find mesh");
801877
return false;
802878
}
803879

804-
// bind mesh to skeleton
805-
ExportSkin (unitySkin, meshInfo, fbxScene, fbxMesh, fbxNode);
880+
Dictionary<SkinnedMeshRenderer, Transform[]> skinnedMeshToBonesMap;
881+
// export skeleton
882+
if (ExportSkeleton (unitySkin, fbxScene, out skinnedMeshToBonesMap)) {
883+
// bind mesh to skeleton
884+
ExportSkin (unitySkin, meshInfo, fbxScene, fbxMesh, fbxNode);
806885

807-
// add bind pose
808-
ExportBindPose (unitySkin, fbxNode, fbxScene, skinnedMeshToBonesMap);
886+
// add bind pose
887+
ExportBindPose (unitySkin, fbxNode, fbxScene, skinnedMeshToBonesMap);
888+
}
809889

810890
return true;
811891
}

Assets/FbxExporters/Editor/UnitTests/FbxAnimationTest.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@ public static IEnumerable SkinnedMeshTestCases {
9090
yield return "Models/DefaultMale/Male_DyingHitFromBack_Blend_T3_Cut01_James.fbx";
9191
}
9292
}
93+
94+
public static IEnumerable BlendShapeTestCases {
95+
get {
96+
yield return "FbxExporters/Editor/UnitTests/Models/blendshape.fbx";
97+
yield return "FbxExporters/Editor/UnitTests/Models/blendshape_with_skinning.fbx";
98+
}
99+
}
93100
}
94101

95102
[TestFixture]

Assets/FbxExporters/Editor/UnitTests/ModelExporterTest.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,5 +677,77 @@ public void TestBoneWeightExport(string fbxPath){
677677
}
678678
Debug.LogWarningFormat ("Compared {0} out of a possible {1} bone weights", comparisonCount, minVertCount);
679679
}
680+
681+
682+
public class Vector3Comparer : IComparer<Vector3>
683+
{
684+
public int Compare(Vector3 a, Vector3 b)
685+
{
686+
Assert.That(a.x, Is.EqualTo(b.x).Within(0.00001f));
687+
Assert.That(a.y, Is.EqualTo(b.y).Within(0.00001f));
688+
Assert.That(a.z, Is.EqualTo(b.z).Within(0.00001f));
689+
return 0; // we're almost equal
690+
}
691+
}
692+
693+
[Test, TestCaseSource(typeof(AnimationTestDataClass), "BlendShapeTestCases")]
694+
public void TestBlendShapeExport(string fbxPath)
695+
{
696+
// add fbx to scene
697+
GameObject originalFbxObj = AssetDatabase.LoadMainAssetAtPath("Assets/" + fbxPath) as GameObject;
698+
Assert.IsNotNull(originalFbxObj);
699+
GameObject originalGO = GameObject.Instantiate(originalFbxObj);
700+
Assert.IsTrue(originalGO);
701+
702+
// export fbx
703+
// get GameObject
704+
string filename = GetRandomFbxFilePath();
705+
ModelExporter.ExportObject(filename, originalGO);
706+
GameObject fbxObj = AssetDatabase.LoadMainAssetAtPath(filename) as GameObject;
707+
Assert.IsTrue(fbxObj);
708+
709+
var originalSMR = originalGO.GetComponentInChildren<SkinnedMeshRenderer>();
710+
var exportedSMR = fbxObj.GetComponentInChildren<SkinnedMeshRenderer>();
711+
Assert.IsNotNull(originalSMR);
712+
Assert.IsNotNull(exportedSMR);
713+
714+
var originalMesh = originalSMR.sharedMesh;
715+
var exportedMesh = exportedSMR.sharedMesh;
716+
Assert.IsNotNull(originalMesh);
717+
Assert.IsNotNull(exportedMesh);
718+
719+
// compare blend shape data
720+
Assert.AreNotEqual(originalMesh.blendShapeCount, 0);
721+
Assert.AreEqual(originalMesh.blendShapeCount, exportedMesh.blendShapeCount);
722+
{
723+
var deltaVertices = new Vector3[originalMesh.vertexCount];
724+
var deltaNormals = new Vector3[originalMesh.vertexCount];
725+
var deltaTangents = new Vector3[originalMesh.vertexCount];
726+
var fbxDeltaVertices = new Vector3[originalMesh.vertexCount];
727+
var fbxDeltaNormals = new Vector3[originalMesh.vertexCount];
728+
var fbxDeltaTangents = new Vector3[originalMesh.vertexCount];
729+
730+
for (int bi = 0; bi < originalMesh.blendShapeCount; ++bi)
731+
{
732+
Assert.AreEqual(originalMesh.GetBlendShapeName(bi), exportedMesh.GetBlendShapeName(bi));
733+
Assert.AreEqual(originalMesh.GetBlendShapeFrameCount(bi), exportedMesh.GetBlendShapeFrameCount(bi));
734+
735+
int frameCount = originalMesh.GetBlendShapeFrameCount(bi);
736+
for (int fi = 0; fi < frameCount; ++fi)
737+
{
738+
Assert.AreEqual(originalMesh.GetBlendShapeFrameWeight(bi, fi), exportedMesh.GetBlendShapeFrameWeight(bi, fi));
739+
740+
originalMesh.GetBlendShapeFrameVertices(bi, fi, deltaVertices, deltaNormals, deltaTangents);
741+
exportedMesh.GetBlendShapeFrameVertices(bi, fi, fbxDeltaVertices, fbxDeltaNormals, fbxDeltaTangents);
742+
743+
var v3comparer = new Vector3Comparer();
744+
Assert.That(deltaVertices, Is.EqualTo(fbxDeltaVertices).Using<Vector3>(v3comparer), string.Format("delta vertices don't match"));
745+
Assert.That(deltaNormals, Is.EqualTo(fbxDeltaNormals).Using<Vector3>(v3comparer), string.Format("delta normals don't match"));
746+
Assert.That(deltaTangents, Is.EqualTo(fbxDeltaTangents).Using<Vector3>(v3comparer), string.Format("delta tangents don't match"));
747+
748+
}
749+
}
750+
}
751+
}
680752
}
681753
}
Binary file not shown.

Assets/FbxExporters/Editor/UnitTests/Models/blendshape.fbx.meta

Lines changed: 93 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Binary file not shown.

0 commit comments

Comments
 (0)