Loading fbx models without turning them to xnb - am I doing it right?

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:

  1. Use the FbxImporter to import the model as NodeContent.
  2. Use the ModelProcessor to convert into ModelContent.
  3. Emulate the work done in ModelReader.Read(), but instead of working with ContentReader work with the ModelContent 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 :slight_smile:

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!