Skip to content

Instantly share code, notes, and snippets.

@DeCarabas
Created February 11, 2020 21:03
Show Gist options
  • Save DeCarabas/4034973f00d8e6aead91d6774e6b1e1d to your computer and use it in GitHub Desktop.
Save DeCarabas/4034973f00d8e6aead91d6774e6b1e1d to your computer and use it in GitHub Desktop.
I wrote an FBX loader a while ago....
namespace Doty.Util.Game.Content
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Graphics.PackedVector;
// TODO: Optimize (slightly) model rendering
// TODO: Improve load perf
// TODO: Debug layer issues on warrior, orc, werewolf
// TODO: Debug binding pose warnings on dragon?
// TODO: Make warnings more sane
// TODO: Warn on unsupported version
// TODO: Warn on unsupported rotation order
// TODO: Support write so that we can do patch-up work.
/// <summary>FbxNode is a node in an FBX file.</summary>
/// <remarks>FBX is a node-based file format, like XML. Nodes have names, attributes, and sub-nodes, here called
/// "properties." (Because "children" is a better name for the object/scene tree, which is layered on top of the
/// file.)</remarks>
public class FbxNode
{
/// <summary>Gets the name of this node.</summary>
public string Name { get; set; }
/// <summary>Gets all of the attributes of this node.</summary>
public List<object> Attributes { get; } = new List<object>();
/// <summary>Gets all of the sub-nodes of this node.</summary>
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public List<FbxNode> Properties { get; } = new List<FbxNode>();
/// <summary>Get the first node with the specified name.</summary>
/// <param name="name">The name of the node to get.</param>
/// <returns>The node with the specified name, or null if there is no such name.</returns>
public FbxNode Prop(string name)
{
for (int i = 0; i < Properties.Count; i++)
{
if (String.Equals(Properties[i].Name, name, StringComparison.Ordinal))
{
return Properties[i];
}
}
return null;
}
/// <summary>Gets a list of the properties with the specified name.</summary>
/// <param name="name">The name to get.</param>
/// <returns>A list of the nodes with the specified name. Empty if there are none.</returns>
public List<FbxNode> Props(string name)
{
var results = new List<FbxNode>(capacity: this.Properties.Count);
for (int i = 0; i < Properties.Count; i++)
{
if (String.Equals(Properties[i].Name, name, StringComparison.Ordinal))
{
results.Add(Properties[i]);
}
}
return results;
}
/// <summary>Gets the attribute at the specified index.</summary>
/// <typeparam name="TValue">The type of the attribute.</typeparam>
/// <param name="index">The index of the attribute to get.</param>
/// <returns>The attribute value.</returns>
public TValue Value<TValue>(int index = 0)
{
return (TValue)Attributes[index];
}
public override string ToString()
{
string val = Name;
if (Attributes.Count > 0)
{
for (int i = 0; i < Attributes.Count; i++)
{
object attr = Attributes[i];
string attrString;
if (attr is Array)
{
var aa = (Array)attr;
attrString = String.Format("{0}[{1}]", aa.GetType().GetElementType(), aa.Length);
}
else if (attr is String)
{
attrString = String.Format("\"{0}\"", attr);
}
else
{
attrString = Attributes[i].ToString();
}
val += " " + attrString;
}
}
val += String.Format(" ({0} props)", Properties.Count);
return val;
}
}
public class FbxTreeEdge
{
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public FbxObject Target { get; set; }
public string Property { get; set; } = String.Empty;
public override string ToString()
{
if (Property.Length > 0)
{
return String.Format("{0} = {1}", Property, Target);
}
else
{
return Target.ToString();
}
}
}
/// <summary>FbxObject is an object in the scene graph.</summary>
public class FbxObject
{
public FbxNode Node { get; set; }
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public List<FbxTreeEdge> Edges { get; } = new List<FbxTreeEdge>();
public override string ToString() { return Node?.ToString(); }
public FbxObject Child(string name, string boundTo = null)
{
for (int i = 0; i < Edges.Count; i++)
{
FbxObject target = Edges[i].Target;
if (String.Equals(target.Node.Name, name, StringComparison.Ordinal) &&
((boundTo == null) || String.Equals(Edges[i].Property, boundTo)))
{
return target;
}
}
return null;
}
public List<FbxObject> Children(string name)
{
List<FbxObject> result = new List<FbxObject>();
for (int i = 0; i < Edges.Count; i++)
{
FbxObject target = Edges[i].Target;
if (String.Equals(target.Node.Name, name, StringComparison.Ordinal))
{
result.Add(target);
}
}
return result;
}
}
public static class FbxReader
{
static readonly byte[] HeaderMagic = Encoding.UTF8.GetBytes("Kaydara FBX Binary \0\x1a\0");
public static FbxNode ReadFile(BinaryReader r)
{
byte[] magic = r.ReadBytes(HeaderMagic.Length);
UInt32 ver = r.ReadUInt32();
FbxNode root = new FbxNode { Name = "" };
bool nullFound = false;
while (r.BaseStream.Position <= r.BaseStream.Length)
{
FbxNode child = ReadNode(r);
if (child == null) { nullFound = true; break; }
root.Properties.Add(child);
}
if (!nullFound) { Warn("End of node found before null child"); }
return root;
}
static FbxNode ReadNode(BinaryReader r)
{
long startPosition = r.BaseStream.Position;
UInt32 endOffset = r.ReadUInt32();
UInt32 numProperties = r.ReadUInt32();
UInt32 propertyListLen = r.ReadUInt32();
byte nameLen = r.ReadByte();
string name = "";
if (nameLen > 0) { name = Encoding.UTF8.GetString(r.ReadBytes(nameLen)); }
if (endOffset == 0)
{
if (numProperties != 0 || propertyListLen != 0 || nameLen != 0)
{
Warn("Expected all zeros on a null record but one or more values were set ({0}, {1}, {2})",
numProperties, propertyListLen, nameLen);
}
return null;
}
FbxNode node = new FbxNode { Name = name };
if (numProperties > 0)
{
long propertyStart = r.BaseStream.Position;
for (int i = 0; i < numProperties; i++)
{
if (r.BaseStream.Position >= propertyStart + propertyListLen)
{
Warn("Something went wrong reading properties; exiting loop");
r.BaseStream.Position = propertyStart + propertyListLen;
break;
}
object value;
char propertyType = (char)r.ReadByte();
switch (propertyType)
{
case 'Y': value = r.ReadInt16(); break;
case 'C': value = r.ReadBoolean(); break;
case 'I': value = r.ReadInt32(); break;
case 'F': value = r.ReadSingle(); break;
case 'D': value = r.ReadDouble(); break;
case 'L': value = r.ReadInt64(); break;
case 'f': value = ReadArray(r, br => br.ReadSingle()); break;
case 'd': value = ReadArray(r, br => br.ReadDouble()); break;
case 'l': value = ReadArray(r, br => br.ReadInt64()); break;
case 'i': value = ReadArray(r, br => br.ReadInt32()); break;
case 'b': value = ReadArray(r, br => br.ReadBoolean()); break;
case 'R': value = r.ReadBytes(r.ReadInt32()); break;
case 'S':
{
Int32 valueLength = r.ReadInt32();
string data = Encoding.UTF8.GetString(r.ReadBytes(valueLength));
if (data.Contains("\x0\x1"))
{
string[] parts = data.Split(new string[] { "\x0\x1" }, StringSplitOptions.None);
Array.Reverse(parts);
data = String.Join("::", parts);
}
value = data;
}
break;
default:
Warn("Unknown property type '{0}', skipping remaining properties", propertyType);
r.BaseStream.Position = startPosition + propertyListLen;
value = null;
break;
}
node.Attributes.Add(value);
}
r.BaseStream.Position = propertyStart + propertyListLen;
}
if (r.BaseStream.Position != endOffset)
{
bool nullFound = false;
while (r.BaseStream.Position <= endOffset)
{
FbxNode child = ReadNode(r);
if (child == null) { nullFound = true; break; }
node.Properties.Add(child);
}
if (!nullFound) { Warn("End of node found before null child"); }
if (r.BaseStream.Position != endOffset) { Warn("Node contents did not align with expected size"); }
}
r.BaseStream.Position = endOffset;
return node;
}
static TElement[] ReadArray<TElement>(BinaryReader r, Func<BinaryReader, TElement> readElement)
{
long startPos = r.BaseStream.Position;
UInt32 count = r.ReadUInt32();
UInt32 encoding = r.ReadUInt32();
UInt32 dataLength = r.ReadUInt32();
BinaryReader elementReader = r;
if (encoding == 1)
{
r.BaseStream.Position += 2; // Unk flags?
var innerStream = new DeflateStream(r.BaseStream, CompressionMode.Decompress, leaveOpen: true);
elementReader = new BinaryReader(innerStream);
}
else if (encoding != 0)
{
Warn("Unknown array encoding {0}", encoding);
}
TElement[] items = new TElement[count];
for (uint i = 0; i < count; i++)
{
items[i] = readElement(elementReader);
}
r.BaseStream.Position = startPos + dataLength;
return items;
}
static void Warn(string format, params object[] args)
{
Console.WriteLine("WARNING: {0}", String.Format(format, args));
}
public static FbxObject BuildObjectTree(FbxNode fileRoot)
{
FbxNode objects = fileRoot.Prop("Objects");
FbxNode connections = fileRoot.Prop("Connections");
var roots = new HashSet<long>();
foreach (FbxNode obj in objects.Properties) { roots.Add(obj.Value<long>()); }
var treeTable = new Dictionary<long, FbxObject>();
foreach (FbxNode connection in connections.Properties)
{
bool isProperty = connection.Value<string>() == "OP";
long child = connection.Value<long>(1);
long parent = connection.Value<long>(2);
string propName = isProperty ? connection.Value<string>(3) : string.Empty;
FbxObject parentNode = FindTreeNode(fileRoot, objects, treeTable, parent);
FbxObject childNode = FindTreeNode(fileRoot, objects, treeTable, child);
parentNode.Edges.Add(new FbxTreeEdge { Target = childNode, Property = propName });
roots.Remove(child);
}
// TODO: Read the root node ID from the properties somewhere ("Documents/Scene") but I don't care.
FbxObject rootObject = FindTreeNode(fileRoot, objects, treeTable, 0);
foreach (long id in roots)
{
rootObject.Edges.Add(new FbxTreeEdge
{
Target = FindTreeNode(fileRoot, objects, treeTable, id),
Property = String.Empty
});
}
return rootObject;
}
private static FbxObject FindTreeNode(
FbxNode root, FbxNode objects, Dictionary<long, FbxObject> treeTable, long id)
{
FbxObject treeNode;
if (!treeTable.TryGetValue(id, out treeNode))
{
FbxNode fbxNode = null;
if (id == 0)
{
fbxNode = root;
}
else
{
foreach (FbxNode candidate in objects.Properties)
{
if (candidate.Value<long>() == id)
{
fbxNode = candidate;
break;
}
}
}
if (fbxNode == null) { throw new InvalidDataException(); }
treeNode = new FbxObject { Node = fbxNode };
treeTable[id] = treeNode;
}
return treeNode;
}
}
// ---
public class FbxModelPart
{
public int Polycount;
public SkinnedModelVertex[] Vertices;
public string TextureFile;
public Matrix[] BindMatrices; // One per bone, maps vertex -> bone space
}
public class FbxAnimationCurve
{
const double FbxTicksPerMillisecond = 46186158;
public long[] Times;
public float[] Values;
public float GetAnimationValue(long fbxTime)
{
// TODO: Much much better curve evaluation.
// http://docs.autodesk.com/FBX/2013/ENU/FBX-SDK-Documentation/index.html?url=files/GUID-9481A726-315C-4A58-A347-8AC95C2AF0F2.htm,topicNumber=d30e10460
//
// Times is sorted.
int index = Array.BinarySearch(Times, fbxTime);
if (index < 0) { index = Math.Max((~index) - 1, 0); }
int nextIndex = Math.Min(index + 1, Times.Length - 1);
long frameTime = Times[index];
long nextFrameTime = Times[nextIndex];
float startValue = Values[index];
float endValue = Values[nextIndex];
float frameLength = nextFrameTime - frameTime;
if (frameLength == 0) { return endValue; }
float dt = (fbxTime - frameTime) / frameLength;
return MathHelper.Lerp(startValue, endValue, dt);
}
public static TimeSpan FbxTimeToTimespan(long fbxTime)
{
// We can also figure out FPS but I don't know why since keyframes always have times.
return TimeSpan.FromMilliseconds(((double)fbxTime) / FbxTicksPerMillisecond);
}
public static long TimeSpanToFbxTime(TimeSpan time)
{
// We can also figure out FPS but I don't know why since keyframes always have times.
return (long)(time.TotalMilliseconds * FbxTicksPerMillisecond);
}
public static long GetFrameTicks(long frame, float framesPerSecond)
{
// Frame / FPS = Frame Time in Seconds (FTIS)
// FTIS * 1000 = Frame Time in MS (FTIMS)
// FTIMS * TicksPerMS = Frame Time in Ticks (FTIT)
//
// (FTIS * 1000) * TicksPerMS = FTIT
// (Frame / FPS) * 1000 * TicksPerMS = FTIT
// 1000 * TicksPerMS * (Frame / FPS) = FTIT
// (1000 * TicksPerMS * Frame) / FPS = FTIT
return (long)(((double)(frame * FbxTicksPerMillisecond * 1000)) / (double)framesPerSecond);
}
}
public enum FbxBoneInheritType
{
RrSs, RSrs, Rrs
};
public class FbxBone
{
public long ObjectId;
public int ParentIndex;
public FbxBoneInheritType IneritType;
public Vector3 Translation;
public FbxAnimationCurve TranslationAnimationX;
public FbxAnimationCurve TranslationAnimationY;
public FbxAnimationCurve TranslationAnimationZ;
public Vector3 PreRotation;
public Vector3 Rotation;
public FbxAnimationCurve RotationAnimationX;
public FbxAnimationCurve RotationAnimationY;
public FbxAnimationCurve RotationAnimationZ;
public Vector3 Scale;
public FbxAnimationCurve ScaleAnimationX;
public FbxAnimationCurve ScaleAnimationY;
public FbxAnimationCurve ScaleAnimationZ;
public FbxBone()
{
Scale = new Vector3(1, 1, 1);
}
}
public class FbxAnimation
{
public string Name;
public long StartTime;
public long Length;
}
public class FbxModel
{
public FbxModelPart[] Parts;
public FbxBone[] Skeleton;
public FbxAnimation[] Animations;
public float FramesPerSecond;
}
public static class FbxModelLoader
{
static FbxAnimationCurve LoadAnimationCurve(FbxObject animationCurve)
{
if (animationCurve == null) { return null; }
FbxNode curveNode = animationCurve.Node;
// TODO: There are a great many options on evaluating the enimation, but we're not going to do that right
// now. (We'll fix it later?) Look here:
// http://docs.autodesk.com/FBX/2013/ENU/FBX-SDK-Documentation/index.html?url=files/GUID-9481A726-315C-4A58-A347-8AC95C2AF0F2.htm,topicNumber=d30e10460
float[] values = curveNode.Prop("KeyValueFloat").Value<float[]>();
long[] times = curveNode.Prop("KeyTime").Value<long[]>();
return new FbxAnimationCurve { Values = values, Times = times };
}
static void LoadBone(FbxObject bone, int parentIndex, List<FbxBone> bones)
{
var result = new FbxBone { ObjectId = bone.Node.Value<long>(), ParentIndex = parentIndex };
int thisIndex = bones.Count;
bones.Add(result);
FbxNode propNode = bone.Node.Prop("Properties70");
foreach (FbxNode prop in propNode.Props("P"))
{
if (prop.Value<string>(0) == "InheritType")
{
result.IneritType = (FbxBoneInheritType)prop.Value<int>(4);
}
else if (prop.Value<string>(0) == "Lcl Translation")
{
float x = (float)prop.Value<double>(4);
float y = (float)prop.Value<double>(5);
float z = (float)prop.Value<double>(6);
result.Translation = new Vector3(x, y, z);
}
else if (prop.Value<string>(0) == "Lcl Rotation")
{
float x = (float)prop.Value<double>(4);
float y = (float)prop.Value<double>(5);
float z = (float)prop.Value<double>(6);
result.Rotation = new Vector3(x, y, z);
}
else if (prop.Value<string>(0) == "Lcl Scaling")
{
float x = (float)prop.Value<double>(4);
float y = (float)prop.Value<double>(5);
float z = (float)prop.Value<double>(6);
result.Scale = new Vector3(x, y, z);
}
else if (prop.Value<string>(0) == "PreRotation")
{
float x = (float)prop.Value<double>(4);
float y = (float)prop.Value<double>(5);
float z = (float)prop.Value<double>(6);
result.PreRotation = new Vector3(x, y, z);
}
else if (prop.Value<string>(0) == "PostRotation")
{
//float x = (float)prop.Value<double>(4);
//float y = (float)prop.Value<double>(5);
//float z = (float)prop.Value<double>(6);
//result.PreRotation = new Vector3(x, y, z);
throw new NotImplementedException();
}
}
FbxObject translate = bone.Child("AnimationCurveNode", boundTo: "Lcl Translation");
if (translate != null)
{
result.TranslationAnimationX = LoadAnimationCurve(translate.Child("AnimationCurve", boundTo: "d|X"));
result.TranslationAnimationY = LoadAnimationCurve(translate.Child("AnimationCurve", boundTo: "d|Y"));
result.TranslationAnimationZ = LoadAnimationCurve(translate.Child("AnimationCurve", boundTo: "d|Z"));
}
FbxObject rotate = bone.Child("AnimationCurveNode", boundTo: "Lcl Rotation");
if (rotate != null)
{
result.RotationAnimationX = LoadAnimationCurve(rotate.Child("AnimationCurve", boundTo: "d|X"));
result.RotationAnimationY = LoadAnimationCurve(rotate.Child("AnimationCurve", boundTo: "d|Y"));
result.RotationAnimationZ = LoadAnimationCurve(rotate.Child("AnimationCurve", boundTo: "d|Z"));
}
FbxObject scale = bone.Child("AnimationCurveNode", boundTo: "Lcl Scaling");
if (scale != null)
{
result.ScaleAnimationX = LoadAnimationCurve(scale.Child("AnimationCurve", boundTo: "d|X"));
result.ScaleAnimationY = LoadAnimationCurve(scale.Child("AnimationCurve", boundTo: "d|Y"));
result.ScaleAnimationZ = LoadAnimationCurve(scale.Child("AnimationCurve", boundTo: "d|Z"));
}
List<FbxObject> limbs = bone.Children("Model");
for (int i = 0; i < limbs.Count; i++)
{
LoadBone(limbs[i], parentIndex: thisIndex, bones: bones);
}
}
static int[] ComputePolygonIndices(int[] fbxIndices, int[] referenceIndices)
{
// So, there can be a mix of triangles and non-triangles, and we need to convert non-triangles to
// triangles. So we can't just take the index array, we need to do some decoding. If an index is
// less than zero, then it marks the end of the polygon and the real index is the bitwise complement.
//
// Because we potentially need to do this work on multiple index arrays from the FBX file, we take two
// different index arrays. The fbxIndices are used to get the actual index values, and the referenceIndices
// are used to determine the polygon boundaries. (For the initial pass, they are the same array.)
int polygonLength = 0;
List<int> indexList = new List<int>(fbxIndices.Length * 2);
for (int indexindex = 0; indexindex < fbxIndices.Length; indexindex++)
{
int index = fbxIndices[indexindex];
polygonLength++;
if (polygonLength > 3)
{
// This is the 4th or greater index of the polygon, so we need to make a triangle!
// Make it out of the first index in the polygon, then the last one of the previous
// triangle, then (below) we'll add the current one.
indexList.Add(indexList[indexList.Count - (polygonLength - 1)]);
indexList.Add(indexList[indexList.Count - 2]);
}
if (index < 0) { index = ~index; }
if (referenceIndices[indexindex] < 0) { polygonLength = 0; }
indexList.Add(index);
}
int[] indices = indexList.ToArray();
for (int i = 0; i < indices.Length; i += 3)
{
int t = indices[i + 0];
indices[i + 0] = indices[i + 2];
indices[i + 2] = t;
}
return indices;
}
static FbxModelPart LoadModelPart(FbxObject model, FbxBone[] skeleton, Dictionary<long, Matrix> bindMatrices)
{
FbxObject geometry = model.Child("Geometry");
FbxNode mesh = geometry.Node;
Vector3[] positions;
int[] positionIndices;
int[] referenceIndices;
{
double[] fbxPositions = mesh.Prop("Vertices").Value<double[]>();
positions = new Vector3[fbxPositions.Length / 3];
for (int i = 0; i < positions.Length; i++)
{
float x = (float)fbxPositions[(i * 3) + 0];
float y = (float)fbxPositions[(i * 3) + 1];
float z = (float)fbxPositions[(i * 3) + 2];
positions[i] = new Vector3(x, y, z);
}
referenceIndices = mesh.Prop("PolygonVertexIndex").Value<int[]>();
positionIndices = ComputePolygonIndices(referenceIndices, referenceIndices);
}
Vector3[] normals = null;
int[] normalIndices = null;
{
List<FbxNode> layers = mesh.Props("LayerElementNormal");
if (layers.Count > 0)
{
FbxNode layer = layers[0];
for (int layerIndex = 1; layerIndex < layers.Count; layerIndex++)
{
FbxNode layerCandidate = layers[layerIndex];
if (layerCandidate.Value<int>() < layer.Value<int>())
{
layer = layerCandidate;
}
}
double[] fbxNormals = layer.Prop("Normals").Value<double[]>();
normals = new Vector3[fbxNormals.Length / 3];
for (int i = 0; i < normals.Length; i++)
{
float x = (float)fbxNormals[(i * 3) + 0];
float y = (float)fbxNormals[(i * 3) + 1];
float z = (float)fbxNormals[(i * 3) + 2];
normals[i] = new Vector3(x, y, z);
}
normalIndices = LoadLayerIndices(positionIndices, referenceIndices, layer, "NormalIndex");
}
}
Vector2[] textureCoordinates = null;
int[] textureIndices = null;
{
List<FbxNode> layers = mesh.Props("LayerElementUV");
if (layers.Count > 0)
{
FbxNode layer = layers[0];
for (int layerIndex = 1; layerIndex < layers.Count; layerIndex++)
{
FbxNode layerCandidate = layers[layerIndex];
if (layerCandidate.Value<int>() < layer.Value<int>())
{
layer = layerCandidate;
}
}
double[] fbxUV = layer.Prop("UV").Value<double[]>();
textureCoordinates = new Vector2[fbxUV.Length / 2];
for (int i = 0; i < textureCoordinates.Length; i++)
{
float u = (float)fbxUV[(i * 2) + 0];
float v = 1.0f - (float)fbxUV[(i * 2) + 1];
textureCoordinates[i] = new Vector2(u, v);
}
textureIndices = LoadLayerIndices(positionIndices, referenceIndices, layer, "UVIndex");
}
}
// Weights
Matrix[] boneMatrices = null; // Mesh space -> bone space
Vector4[] weightValues = null; // Only four bones can affect a given vertex.
Byte4[] weightIndices = null; // 4 ints per vertex, identifying the bone the weight goes to.
FbxObject deformer = geometry.Child("Deformer");
if (deformer != null)
{
List<Tuple<byte, float>>[] gatheredWeightsByVertex = new List<Tuple<byte, float>>[positions.Length];
boneMatrices = new Matrix[skeleton.Length];
for (int i = 0; i < boneMatrices.Length; i++) { boneMatrices[i] = Matrix.Identity; }
// Deform accuracy? (50)
// Skinning Type? ("Linear")
List<FbxObject> subDeformers = deformer.Children("Deformer");
foreach (FbxObject subDeformer in subDeformers)
{
FbxNode subDeformerNode = subDeformer.Node;
long boneId = subDeformer.Child("Model").Node.Value<long>();
int boneIndex = -1;
for (int i = 0; i < skeleton.Length; i++)
{
if (skeleton[i].ObjectId == boneId)
{
boneIndex = i;
break;
}
}
if (boneIndex < 0)
{
throw new InvalidDataException("Cannot find bone in skeleton");
}
// NB: There is more data here-- a bind matrix for the mesh, and then the two transformlink
// matrices, but AFAICT we don't need any of those things, just the bind matrix for the
// bone. Applying the inverse of this puts the vertices into bone space, and then the bone
// matrices that are computed per-frame take us back to world space again.
Matrix boneBindMatrix = bindMatrices[boneId];
boneMatrices[boneIndex] = Matrix.Invert(boneBindMatrix);
FbxNode indicesNode = subDeformerNode.Prop("Indexes");
FbxNode weightsNode = subDeformerNode.Prop("Weights");
if (indicesNode != null && weightsNode != null)
{
int[] deformerIndices = indicesNode.Value<int[]>();
double[] deformerWeights = weightsNode.Value<double[]>();
for (int vertexIndexIndex = 0; vertexIndexIndex < deformerIndices.Length; vertexIndexIndex++)
{
float weight = (float)deformerWeights[vertexIndexIndex];
if (weight == 0) { continue; }
int vertexIndex = deformerIndices[vertexIndexIndex];
List<Tuple<byte, float>> influences = gatheredWeightsByVertex[vertexIndex];
if (influences == null)
{
influences = new List<Tuple<byte, float>>();
gatheredWeightsByVertex[vertexIndex] = influences;
}
influences.Add(Tuple.Create((byte)boneIndex, weight));
}
}
}
weightValues = new Vector4[positions.Length];
weightIndices = new Byte4[positions.Length];
for (int vertexIndex = 0; vertexIndex < weightValues.Length; vertexIndex++)
{
List<Tuple<byte, float>> influences = gatheredWeightsByVertex[vertexIndex];
if (influences != null)
{
float weight0 = 0, weight1 = 0, weight2 = 0, weight3 = 0;
byte idx0 = 0, idx1 = 0, idx2 = 0, idx3 = 0;
if (influences.Count > 0) { weight0 = influences[0].Item2; idx0 = influences[0].Item1; }
if (influences.Count > 1) { weight1 = influences[1].Item2; idx1 = influences[1].Item1; }
if (influences.Count > 2) { weight2 = influences[2].Item2; idx2 = influences[2].Item1; }
if (influences.Count > 3) { weight3 = influences[3].Item2; idx3 = influences[3].Item1; }
if (influences.Count > 4)
{
//TODO: WARN HERE. (Micro demon hits this, has 5 at least.)
//throw new InvalidDataException("Vertices may be attached to at most 4 bones");
}
// Weights must sum up to one in order to work out properly given the way that the matrices
// will be summed by the shader.
float normValue = weight0 + weight1 + weight2 + weight3;
if (normValue != 0)
{
weight0 /= normValue;
weight1 /= normValue;
weight2 /= normValue;
weight3 /= normValue;
}
weightValues[vertexIndex] = new Vector4(weight0, weight1, weight2, weight3);
weightIndices[vertexIndex].PackedValue =
(uint)((idx0 << 0) | (idx1 << 8) | (idx2 << 16) | (idx3 << 24));
}
}
}
// TODO: We can be better if all the indices are the same, we can just build a single array of vertices
// with all the values and do an indexed draw, instead of unwrapping everything.
//
var vertices = new SkinnedModelVertex[positionIndices.Length];
for (int i = 0; i < vertices.Length; i++)
{
vertices[i].Position = positions[positionIndices[i]];
}
if (normals != null)
{
for (int i = 0; i < vertices.Length; i++)
{
vertices[i].Normal = normals[normalIndices[i]];
}
}
if (textureCoordinates != null)
{
for (int i = 0; i < vertices.Length; i++)
{
vertices[i].TextureCoordinate = textureCoordinates[textureIndices[i]];
}
}
if (weightValues != null)
{
for (int i = 0; i < vertices.Length; i++)
{
vertices[i].WeightValues = weightValues[positionIndices[i]];
}
for (int i = 0; i < vertices.Length; i++)
{
vertices[i].WeightIndices = weightIndices[positionIndices[i]];
}
}
// Materials!
// TODO: More better materials? Yesss....
FbxObject material = model.Child("Material");
FbxObject texture = material.Child("Texture");
string textureFile = texture.Node.Prop("RelativeFilename").Value<string>();
return new FbxModelPart
{
Polycount = vertices.Length / 3,
Vertices = vertices,
TextureFile = textureFile,
BindMatrices = boneMatrices,
};
}
static int[] LoadLayerIndices(
int[] positionIndices, int[] referenceIndices, FbxNode layerNode, string indexPropName)
{
string mappingType = layerNode.Prop("MappingInformationType").Value<string>();
string referenceType = layerNode.Prop("ReferenceInformationType").Value<string>();
int[] indices = null;
if (mappingType == "ByVertice")
{
// Values in the array map to the positions (not what we would call a vertex), so the position
// indices are also value indices.
if (referenceType == "Direct")
{
// There aren't any explicit index values that tell us how to map values to position values, they
// just go 1:1. Therefore, to map them onto vertices, we just use the position indices.
//
// NB: Position indices have already been re-wrapped, so we don't need to re-do it here.
indices = positionIndices;
}
}
else if (mappingType == "ByPolygonVertex")
{
// Values in the array map to the vertices (not positions), so we need some kind of mapping to
// tell us how to do that.
if (referenceType == "Direct")
{
// Each value in the array maps directly to a polygon vertex. We can't just build the
// stupid mapping, though, because some of the polygons may be quads and stuff like that.
// So we need to build the stupid mapping and then explicitly wrap it with the reference
// indices.
int[] fbxIndices = new int[referenceIndices.Length];
for (int i = 0; i < fbxIndices.Length; i++) { fbxIndices[i] = i; }
indices = ComputePolygonIndices(fbxIndices, referenceIndices);
}
else if (referenceType == "IndexToDirect")
{
// We have our own indices that map values in the value array to vertices. There are two
// wrinkles: first, these indices need to be re-wrapped, and secondly, we need to use the
// position indices in order to figure how to tesselate the polygons into triangles.
int[] fbxIndices = layerNode.Prop(indexPropName).Value<int[]>();
indices = ComputePolygonIndices(fbxIndices, referenceIndices);
}
}
if (indices == null)
{
throw new NotSupportedException(String.Format(
"Unsupported mapping/reference combination ({0}/{1})",
mappingType, referenceType));
}
return indices;
}
public static FbxModel LoadModel(FbxObject root)
{
int timeMode = 0;
double customFrameRate = 0;
long timeSpanStart = 0, timeSpanStop = 0;
FbxNode globalProperties = root.Node.Prop("GlobalSettings").Prop("Properties70");
if (globalProperties != null)
{
foreach (FbxNode p in globalProperties.Props("P"))
{
if (p.Value<string>() == "TimeMode")
{
timeMode = p.Value<int>(4);
}
else if (p.Value<string>() == "CustomFrameRate")
{
customFrameRate = p.Value<double>(4);
}
else if (p.Value<string>() == "TimeSpanStart")
{
timeSpanStart = p.Value<long>(4);
}
else if (p.Value<string>() == "TimeSpanStop")
{
timeSpanStop = p.Value<long>(4);
}
}
}
double fps = 0;
switch (timeMode)
{
case 1: fps = 120; break; //eFrames120,
case 2: fps = 100; break; //eFrames100,
case 3: fps = 60; break; //eFrames60,
case 4: fps = 50; break; //eFrames50,
case 5: fps = 48; break; //eFrames48,
case 6: fps = 30; break; //eFrames30,
case 7: fps = 30; break; //eFrames30Drop,
case 8: fps = 29.97; break; //eNTSCDropFrame,
case 9: fps = 29.97; break; //eNTSCFullFrame,
case 10: fps = 25; break; //ePAL,
case 11: fps = 24; break; //eFrames24,
case 12: fps = 1000; break; //eFrames1000,
case 13: fps = 23.976; break; //eFilmFullFrame,
case 14: fps = customFrameRate; break; //eCustom,
case 15: fps = 96; break; //eFrames96,
case 16: fps = 72; break; //eFrames72,
case 17: fps = 59.94; break; //eFrames59dot94,
default: throw new NotSupportedException();
}
List<FbxAnimation> animations = new List<FbxAnimation>();
foreach (FbxObject animationStack in root.Children("AnimationStack"))
{
string name = animationStack.Node.Value<string>(1);
long animationStartTime = 0, animationEndTime = 0;
FbxNode props = animationStack.Node.Prop("Properties70");
foreach (FbxNode p in props.Properties)
{
if (p.Value<string>(0) == "LocalStart")
{
animationStartTime = p.Value<long>(4);
}
else if (p.Value<string>(0) == "LocalStop")
{
animationEndTime = p.Value<long>(4);
}
}
long animationLength = animationEndTime - animationStartTime;
animations.Add(new FbxAnimation
{
Name = name,
StartTime = animationStartTime,
Length = animationLength
});
}
Dictionary<long, Matrix> bindMatrices = new Dictionary<long, Matrix>();
foreach (FbxObject poseObject in root.Children("Pose"))
{
FbxNode pose = poseObject.Node;
if (pose.Prop("Type").Value<string>() != "BindPose") { continue; }
foreach (FbxNode poseNode in pose.Props("PoseNode"))
{
long objectId = poseNode.Prop("Node").Value<long>();
double[] m = poseNode.Prop("Matrix").Value<double[]>();
Matrix matrix = new Matrix(
(float)m[00], (float)m[01], (float)m[02], (float)m[03],
(float)m[04], (float)m[05], (float)m[06], (float)m[07],
(float)m[08], (float)m[09], (float)m[10], (float)m[11],
(float)m[12], (float)m[13], (float)m[14], (float)m[15]);
Matrix existingMatrix;
if (bindMatrices.TryGetValue(objectId, out existingMatrix))
{
if (matrix != existingMatrix) { Console.WriteLine("WARNING: BIND MATRICES DIFFERENT"); }
}
bindMatrices[objectId] = matrix;
}
}
List<FbxObject> models = root.Children("Model");
List<FbxBone> bones = new List<FbxBone>();
int meshCount = 0;
foreach (FbxObject model in models)
{
string kind = model.Node.Value<string>(2);
if (String.Equals(kind, "LimbNode", StringComparison.Ordinal))
{
LoadBone(model, -1, bones);
}
else if (String.Equals(kind, "Mesh", StringComparison.Ordinal))
{
meshCount++;
}
}
FbxBone[] skeleton = bones.ToArray();
int modelIndex = 0;
FbxModelPart[] parts = new FbxModelPart[meshCount];
foreach (FbxObject model in models)
{
string kind = model.Node.Value<string>(2);
if (String.Equals(kind, "Mesh", StringComparison.Ordinal))
{
parts[modelIndex] = LoadModelPart(model, skeleton, bindMatrices);
modelIndex++;
}
}
return new FbxModel
{
Parts = parts,
Skeleton = skeleton,
Animations = animations.ToArray(),
FramesPerSecond = (float)fps,
};
}
public static FbxAnimation[] LoadAndCutAnimations(string cutFilePath, FbxModel fbxModel)
{
List<FbxAnimation> newAnimations = new List<FbxAnimation>();
string[] lines = File.ReadAllLines(cutFilePath);
foreach (string line in lines)
{
if (line.StartsWith("#")) { continue; }
if (String.IsNullOrWhiteSpace(line)) { continue; }
string[] parts = line.Split(',');
string clip = parts[0].Trim();
long start = Int64.Parse(parts[1].Trim());
long end = Int64.Parse(parts[2].Trim());
string name = parts[3].Trim();
FbxAnimation sourceAnimation = null;
foreach (FbxAnimation fa in fbxModel.Animations)
{
if (String.Equals(fa.Name, clip, StringComparison.OrdinalIgnoreCase))
{
sourceAnimation = fa;
break;
}
}
if (sourceAnimation == null) { throw new InvalidDataException("Cannot find clip " + clip); }
long frameStartTicks = FbxAnimationCurve.GetFrameTicks(start, fbxModel.FramesPerSecond);
long frameEndTicks = FbxAnimationCurve.GetFrameTicks(end, fbxModel.FramesPerSecond);
newAnimations.Add(new FbxAnimation
{
Name = name,
StartTime = frameStartTicks,
Length = frameEndTicks - frameStartTicks,
});
}
return newAnimations.ToArray();
}
public static SkinnedModel ConvertToSkinnedModel(
WorkLogger logger,
GraphicsDevice device,
IContentLoader contentLoader,
FbxModel model)
{
SkinnedModel result = new SkinnedModel()
{
Parts = new ModelGeometry[model.Parts.Length],
Animations = new AnimationClip[model.Animations.Length],
FramesPerSecond = model.FramesPerSecond,
Skeleton = new Bone[model.Skeleton.Length],
};
for (int i = 0; i < result.Parts.Length; i++)
{
FbxModelPart fbxPart = model.Parts[i];
IContentHandle<Texture2D> texture = null;
if (fbxPart.TextureFile != null)
{
texture = contentLoader.LoadTexture(logger, device, fbxPart.TextureFile);
}
var vertices = new VertexBuffer(
device,
typeof(SkinnedModelVertex),
fbxPart.Vertices.Length,
BufferUsage.WriteOnly);
vertices.SetData(fbxPart.Vertices);
var boneMatrices = (Matrix[])fbxPart.BindMatrices.Clone();
// TODO: Better material parsing
result.Parts[i] = new ModelGeometry
{
BoneMatrices = boneMatrices,
Material = new SkinnedEffectParameters
{
DiffuseColor = new Vector3(1, 1, 1),
Texture = texture,
},
Vertices = vertices,
};
}
for (int boneIndex = 0; boneIndex < result.Skeleton.Length; boneIndex++)
{
FbxBone fbxBone = model.Skeleton[boneIndex];
result.Skeleton[boneIndex] = new Bone
{
ParentIndex = fbxBone.ParentIndex,
PreRotation = fbxBone.PreRotation,
Rotation = fbxBone.Rotation,
RotationAnimationX = ConvertAnimationCurve(fbxBone.RotationAnimationX),
RotationAnimationY = ConvertAnimationCurve(fbxBone.RotationAnimationY),
RotationAnimationZ = ConvertAnimationCurve(fbxBone.RotationAnimationZ),
Scale = fbxBone.Scale,
ScaleAnimationX = ConvertAnimationCurve(fbxBone.ScaleAnimationX),
ScaleAnimationY = ConvertAnimationCurve(fbxBone.ScaleAnimationY),
ScaleAnimationZ = ConvertAnimationCurve(fbxBone.ScaleAnimationZ),
Translation = fbxBone.Translation,
TranslationAnimationX = ConvertAnimationCurve(fbxBone.TranslationAnimationX),
TranslationAnimationY = ConvertAnimationCurve(fbxBone.TranslationAnimationY),
TranslationAnimationZ = ConvertAnimationCurve(fbxBone.TranslationAnimationZ),
};
}
for (int animationIndex = 0; animationIndex < result.Animations.Length; animationIndex++)
{
FbxAnimation fbxAnimation = model.Animations[animationIndex];
result.Animations[animationIndex] = new AnimationClip
{
Name = fbxAnimation.Name,
StartTime = (float)FbxAnimationCurve.FbxTimeToTimespan(fbxAnimation.StartTime).TotalSeconds,
Length = (float)FbxAnimationCurve.FbxTimeToTimespan(fbxAnimation.Length).TotalSeconds,
};
}
return result;
}
static BoneAnimationCurve ConvertAnimationCurve(FbxAnimationCurve fbxCurve)
{
if (fbxCurve == null) { return null; }
float[] resultTimes = new float[fbxCurve.Times.Length];
for (int i = 0; i < resultTimes.Length; i++)
{
resultTimes[i] = (float)FbxAnimationCurve.FbxTimeToTimespan(fbxCurve.Times[i]).TotalSeconds;
}
float[] resultValues = (float[])fbxCurve.Values.Clone();
return new BoneAnimationCurve { Times = resultTimes, Values = resultValues };
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment