Hi all,
So I looked at code and old documentation, and I got to the conclusion that to load fbx directly without building xnb, I need to do the following steps:
- Use the FbxImporter to import the model as
NodeContent
. - Use the ModelProcessor to convert into
ModelContent
. - Emulate the work done in
ModelReader.Read()
, but instead of working withContentReader
work with theModelContent
I have.
Now I managed to do the above and successfully load an fbx from file, but I feel like its wrong because I’m duplicating code from ContentReader
and have to access internal method via reflection to make it work (BuildHierarchy
). My code is not that long, but I was wondering if maybe there’s a more “proper” way to do this that I somehow missed and maybe I did all that work for nothing
Here’s my code:
public Model LoadModel(string modelPath)
{
// get from cache
if (_loadedModels.TryGetValue(modelPath, out Model cached))
{
return cached;
}
// load fbx and get model content
var node = _fbxImporter.Import(modelPath, _importContext);
// convert to model content
ModelContent modelContent = _modelProcessor.Process(node, _processContext);
// extract bones
var bones = new List<ModelBone>();
foreach (var boneContent in modelContent.Bones)
{
var bone = new ModelBone
{
Transform = boneContent.Transform,
Index = bones.Count,
Name = boneContent.Name,
ModelTransform = modelContent.Root.Transform
};
bones.Add(bone);
}
// resolve bones hirarchy
for (var index = 0; index < bones.Count; ++index)
{
var bone = bones[index];
var content = modelContent.Bones[index];
if (content.Parent != null && content.Parent.Index != -1)
{
bone.Parent = bones[content.Parent.Index];
bone.Parent.AddChild(bone);
}
}
// extract meshes
var meshes = new List<ModelMesh>();
foreach (var meshContent in modelContent.Meshes)
{
// get params
var name = meshContent.Name;
var parentBoneIndex = meshContent.ParentBone.Index;
var boundingSphere = meshContent.BoundingSphere;
var meshTag = meshContent.Tag;
// extract parts
var parts = new List<ModelMeshPart>();
foreach (var partContent in meshContent.MeshParts)
{
// build index buffer
IndexBuffer indexBuffer = new IndexBuffer(_graphics, IndexElementSize.ThirtyTwoBits, partContent.IndexBuffer.Count, BufferUsage.WriteOnly);
{
Int32[] data = new Int32[partContent.IndexBuffer.Count];
partContent.IndexBuffer.CopyTo(data, 0);
indexBuffer.SetData(data);
}
// build vertex buffer
var vbDeclareContent = partContent.VertexBuffer.VertexDeclaration;
List<VertexElement> elements = new List<VertexElement>();
foreach (var declareContentElem in vbDeclareContent.VertexElements)
{
elements.Add(new VertexElement(declareContentElem.Offset, declareContentElem.VertexElementFormat, declareContentElem.VertexElementUsage, declareContentElem.UsageIndex));
}
var vbDeclare = new VertexDeclaration(elements.ToArray());
VertexBuffer vertexBuffer = new VertexBuffer(_graphics, vbDeclare, partContent.NumVertices, BufferUsage.WriteOnly);
{
vertexBuffer.SetData(partContent.VertexBuffer.VertexData);
}
// create and add part
#pragma warning disable CS0618 // Type or member is obsolete
ModelMeshPart part = new ModelMeshPart()
{
VertexOffset = partContent.VertexOffset,
NumVertices = partContent.NumVertices,
PrimitiveCount = partContent.PrimitiveCount,
StartIndex = partContent.StartIndex,
Tag = partContent.Tag,
IndexBuffer = indexBuffer,
VertexBuffer = vertexBuffer
};
#pragma warning restore CS0618 // Type or member is obsolete
parts.Add(part);
}
// create and add mesh to meshes list
var mesh = new ModelMesh(_graphics, parts)
{
Name = name,
BoundingSphere = boundingSphere,
Tag = meshTag,
};
meshes.Add(mesh);
// set parts effect (note: this must come *after* we add parts to the mesh otherwise we get exception).
foreach (var part in parts)
{
var effect = EffectsGenerator != null ? EffectsGenerator(modelPath, modelContent, part) ?? DefaultEffect : DefaultEffect;
part.Effect = effect;
}
// add to parent bone
if (parentBoneIndex != -1)
{
mesh.ParentBone = bones[parentBoneIndex];
mesh.ParentBone.AddMesh(mesh);
}
}
// create model
var model = new Model(_graphics, bones, meshes);
model.Root = bones[modelContent.Root.Index];
model.Tag = modelContent.Tag;
// we need to call BuildHierarchy() but its internal, so we use reflection to access it ¯\_(ツ)_/¯
var methods = model.GetType().GetMethods(BindingFlags.Instance | BindingFlags.NonPublic);
var BuildHierarchy = methods.Where(x => x.Name == "BuildHierarchy" && x.GetParameters().Length == 0).First();
BuildHierarchy.Invoke(model, null);
// add to cache and return
_loadedModels[modelPath] = model;
return model;
}
Am I doing it right or is there a more decent way?
PS to answer the question why I don’t use the MonoGame content tool and xnb - I want to make something that is more approachable for users to modify and add their own packages. while adding a conversion tool is possible and not that big of a deal, its still a barrier for many people IMO.
Thanks!