Creating custom importers

Importers process raw resources in a third-party format (like FBX mesh or a PNG image) into an engine-ready format (e.g. a Mesh or a Texture). Banshee has an extensible importer system so you may easily add your own importers, either for existing resource types or for new ones. This way you can add support for new third party file formats.

To implement your own importer you need to implement the SpecificImporter interface.

Implementing SpecificImporter

Implementing this interface involves implementation of the following methods:

  • SpecificImporter::isExtensionSupported() - Receives a file extension and returns true or false depending if the importer can process that file. Used by the importer to find which importer plugin to use for import of a specific file.
  • SpecificImporter::isMagicNumberSupported() - Similar to the method above, but receives a magic number (first few bytes of a file) instead of the extension, as this is the more common way of identifying files on non-Windows systems.
  • SpecificImporter::import() - Receives a path to a file, as well as a set of import options. This is the meat of the importer where you will read the file and convert it into engine ready format. When done the method returns a Resource of a valid type, or null if it failed. The method should take into account the import options it was provided (if your importer supports any).
// Simple importer for plain text types.
class PlainTextImporter : public SpecificImporter
{
public:
bool isExtensionSupported(const WString& ext) const override
{
WString lowerCaseExt = ext;
StringUtil::toLowerCase(lowerCaseExt);
return lowerCaseExt == L"txt";
}
bool isMagicNumberSupported(const UINT8* magicNumPtr, UINT32 numBytes) const override
{
// Magic numbers don't make sense for plain text files, so we rely on extension checking
return true;
}
SPtr<Resource> import(const Path& filePath, SPtr<const ImportOptions> importOptions) override
{
SPtr<DataStream> stream = FileSystem::openFile(filePath);
WString textData = stream->getAsWString();
// ... initialize some resource with the text and return
}
};

Registering SpecificImporter

To register your SpecificImporter implementation with the importer system you must call _registerAssetImporter(). You can do this after application start-up, or during by implementing your own Application class as described in the Advanced startup manual.

Application::startUp(...);
PlainTextImporter* myImporter = bs_new<PlainTextImporter>();

Your importer must be allocated using a general purpose allocator (bs_new) because the importer system automatically frees it on shutdown, and it doesn't expect any special memory types.

Optionally you can do this on the higher level by providing a list of importers to Application::startUp() method. This method expects a list of dynamic library file-names, which means you must implement your importer as a plugin, as described in the Plugins manual.

Optional features

Your importer may also optionally implement any of the following features.

Import options

If you want to allow the user to control how is a file imported, you need to implement the ImportOptions class. The class has no special interface except the requirement to create a RTTI object for it.

// Import options for our PlainTextImporter. Contains a single option
// that allows the user to choose whether or not to convert all text
// to lowercase on import.
class PlainTextImportOptions : public ImportOptions
{
public:
// Converts all text to lowercase on import
bool convertToLowercase = false;
friend class PlainTextImportOptionsRTTI;
static RTTITypeBase* getRTTIStatic() { return PlainTextImportOptionsRTTI::instance(); }
RTTITypeBase* getRTTI() const override { return getRTTIStatic(); }
};

The RTTI is implemented as normal, as described in the Serializing objects manual.

You can then instantiate import options and provide them to the call of Importer::import() and they will be passed through all the way to your SpecificImporter implementation. After that you can read the relevant options and perform the import accordingly.

class PlainTextImporter : public SpecificImporter
{
public:
// ... other importer code
SPtr<Resource> import(const Path& filePath, SPtr<const ImportOptions> importOptions) override
{
const PlainTextImportOptions* myIO = static_cast<const PlainTextImportOptions*>(importOptions.get());
if(myIO->convertToLowercase)
{
// Do something
}
else
{
// Do something else
}
// ... import and return a resource
}
};

Multiple resources

If a single external file can be interpreted as multiple engine resources you should override the SpecificImporter::importAll() method. For example this is the case with FBX format which can contain a mesh and one or multiple animations.

SpecificImporter::importAll() method will return a list of resources, each with a unique identifier which allows external code to know what those resources are. One of the resources must always be considered primary, and that's the resource that should be returned by SpecificImporter::import() (others should be ignored). The primary resource must have the "primary" identifier, while you are free to add custom identifiers for every other resource.

class PlainTextImporter : public SpecificImporter
{
public:
// ... other importer code
Vector<SubResourceRaw> importAll(const Path& filePath, SPtr<const ImportOptions> importOptions) override
{
Vector<SubResourceRaw> output;
// ... read the file and generate resources
output.push_back({ "primary", someResourceA });
output.push_back({ "otherOne", someResourceB });
output.push_back({ "anotherOne", someResourceC });
return output;
}
};

Then you can call Importer::importAll() to import multiple resources from a file.