Created
February 11, 2020 21:03
-
-
Save DeCarabas/4034973f00d8e6aead91d6774e6b1e1d to your computer and use it in GitHub Desktop.
I wrote an FBX loader a while ago....
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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