Extending the FBX Importer
With the recent code drop of Lumberyard came our new FBX Importer, which gives developers the ability to export single meshes and materials. The FBX Importer has been one of the most consistently requested features for Lumberyard. We’re quite excited about how the interface works and looks and the overall direction it’s heading, but there’s also some pretty exciting technology behind it. Sorry artists, this one’s for the programmers.
Let’s take a quick look at the overall system. There are three major components involved with importing anything from an FBX file, which are:
- Internal Storage
Rather than take an FBX file and convert it straight into data that Lumberyard can consume, the process is broken down into two distinct phases. Builders and importers read data from one or more source files and use this to fill up the internal storage in the first phase. The internal storage contains a tree to describe a scene (
SceneGraph) and a dictionary containing meta-data (
SceneManifest). This storage is passed on to one or more exporters in the second phase. The exporters look at the content of the internal storage and generate one or more files ready to be used by Lumberyard.
The FBX Importer preview currently contains a builder for the FBX format (see
FbxSceneBuilder), a json importer and exporter to read/write metadata and an exporter to generate mesh data in Lumberyards CGF format (see
CgfExporter). Materials are also supported, but these don’t behave as true exporters since they produce more of an intermediate file.
This two-phase approach is used by both the editor and the resource compiler (RC). When opening an FBX file in the editor, the FBX file and meta-data (“.scenesettings”-files) are read and the editor uses the tree and meta-data in the internal storage to populate the UI. The user can edit the settings in the meta-data and, after confirming the changes, write it out to disk. The RC monitors for any changes to the FBX file or the meta-data so the previous action by the editor will trigger an RC task to build or update the Lumberyard CGF files. The RC task, in turn, will read the FBX file and meta-data. It looks through the meta-data and executes these using the data in the tree. The RC will continue to watch for changes, so when any .scenesettings of .fbx file change, those changes will be reflected in the editor and/or the game as well after a short delay; no need to reimport anything.
From that dry piece of information above, it’s hopefully clear that the internal storage is pivotal in our plans for importing. It’s the central place all data goes to and gets read from. We even went so far as to create
DataObject, a type-erased storage object so that you don’t have to bother with interfaces and including multiple libraries but can simply call
DataObject::Create<Type>(argument); to have your own data in either the tree or the meta-data.
Why put so much effort into this instead of just exporting with the FBX? At the root is the problem that, quite frankly, we can never know what data is important to you. Maybe the names in an FBX file have special meanings or a certain hierarchy means something important. Perhaps there’s a set of assets in a legacy format that need to be imported or Lumberyard was extended with a game-specific format. Perhaps there’s another reason; we just can’t know.
When the development of the FBX Importer started, we wanted to provide core functionality to import data from an FBX file and export it to data Lumberyard could process. We were also aware of the customizations users would eventually require. When it came to a lot of our technical choices, the most important question we asked ourselves was: How will a user extend this?
At times, that can be a hard question. Picture, if you will, a meeting in a darkened room at the very early stages of development. It was just established that a dictionary was going to be used as our meta-data and has to store export instructions. The faces of the meeting attendees are dimly lit by the light of the teleconferencing television. A can of soda opens with a hiss.
Lower the house lights and raise the curtains
Developer A: The data stored in the
SceneManifest will derive from a common mesh export group or rule interface.
Developer B: Sounds good, but how will a user extend this?
A: Well, they just implement their version of the interface.
B: Sure but what about data that is not directly mesh-related, like animation or something like an author name?
A: OK, well, in that case let’s have a very basic
SceneManifest-entry interface with no functions so everything can be added and have the mesh group/rule interfaces inherit from that.
Developer C: Good! Solved. But how will the user extend the UI?
B: We could use our property grid to automatically build a UI for editing.
C: Cool, then the user would only need to add runtime type information [RTTI] and reflection.
C: Hmmmm, now that I think about it, why do we need both an interface and RTTI?
A: Well, no real need, but we would need to write some sort of type-erasing storage to get rid of the interface, which will take extra time.
B: But it will make it easier for the user to put any of their data in, no matter what it might be.
A: That’s right. That’s what we’ll do.
My Tony Award must be in the mail. Still, this is more or less what happened. What we initially thought was a straightforward solution turned into
DataObject to keep code requirements to a minimum. Just a few optional hints will help automate tasks such as building a UI. The only reason we came to this conclusion was because the question “How will a user extend this?” was brought up several times.
The part that’s actually interesting
DataObjects are used to store any data in the tree or the meta-data. The only requirement is that RTTI is implemented for the class that will be stored in the
DataObject, although a single line to use the
AZ_RTTI macro is already enough to satisfy this requirement.
For all the data added by our team, we’ve added an additional layer of convenience in the form of common interfaces to all data classes. We base our functionality on the interface, not the concrete implementation. You have the choice to extend the interface instead of creating a new type, so you can use the functionality that’s already there.
That’s a lot of information, so let’s run through an example to see how all these pieces tie together. Let’s pretend there’s a world where technology exists to procedurally generate hats. In such a world, Lumberyard clearly has to be extended to support importing .hat files.
- Let’s begin by creating a new class to store the .hat content called “HatData”; no need to inherit from an interface, just make sure there’s an
AZ_RTTI(“HatData”, …)in there.
- A new importer will be responsible for loading the .hat file into HatData and adding one or more
DataObjects with the loaded data. Let’s call that a HatSceneBuilder which will call something like
tree.AddChild(GetRoot(), “TopHat”, DataObject::Create<HatData>(data));.
- Next, there should be an option to attach the hat to a mesh node. To configure this, a HatRule is created. Let’s avoid writing a custom UI for this, so inherit from the
IRuleinterface and add it to the rule factory. After adding
AZ_RTTI, reflection is added (which is a bit too much to discuss here, so will skip for now). Since the
IRuleinterface is used and given that reflection has been set up, there’s nothing else that needs to be done. The editor will be aware of the rule and allows it to be added to a mesh group. It will be serialized to and from the meta-data.
- Finally a RuntimeHat exporter is created that will use the new rule in the meta-data and the content of the tree to generate the .rthat file, the fictional file which Lumberyard will use once the procedural hat technology has been added to the engine.
This blog covered a basic overview of the mechanics behind the FBX Importer and gave a quick peek into our approach to making an easily extensible system. There are plenty more examples and details I’d love to discuss but I didn’t want to drag on and on.
Your feedback on the entire FBX Importer is very important to us. The current version is a preview of things to come, and we have no shortage of plans that will hopefully lead to some very exciting news in the (near) future. Your feedback will be instrumental for us to prioritize those plans and give us even more concrete examples and ideas on how to answer that important question: How will a user extend this? Send us your asset importing needs and ideas, no matter how crazy! OK, maybe draw the line at procedurally generated hats.
Ronald Koppers is a technomancer specialized in the teachings of Stroustrup. For over a decade Ronald has been reordering bytes in the most optimal order in over half a dozen engines and editors as well as extending them with new technologies. At Amazon he’s currently passionately working on the easiest way possible to transmogrify your work into Lumberyard.