Assimp on Android

2015-08-19

Tips and tricks for getting Assimp to work on Android.

Compiling Assimp for Android

See the following links:

IMPORTANT NOTE: make sure you compile Assimp with exception handling enabled (-fexceptions). Otherwise, throwing exceptions like we do in the code that follows will result in undefined behaviour.

Loading Assets from an APK

Assimp attempts to load assets from the regular file system by default. While this is certainly possible on Android (after setting the appropriate permissions in the application’s manifest), packaging assets with the APK seems to be the preferred way for small projects. Fortunately enough, the Assimp developers saw this coming and designed Assimp so that one can load assets from any kind of file system.

Loading assets from custom file systems in Assimp is done by writing an implementation of IOSystem and IOStream. The former is an abstraction for file systems, the latter is an abstraction for file streams. On the other hand, reading files from an APK is done using the NDK Asset API. Putting two and two together, we can get Assimp to load assets from an APK.

First, we implement a custom IOSystem to open files from an APK. We call this APKIOSystem:

#include <assimp/IOSystem.hpp>
#include <android/asset_manager.h>

class APKIOSystem final : public Assimp::IOSystem
{
public:

    APKIOSystem (AAssetManager*)
        : mgr(mgr) {}

    Assimp::IOStream* Open (const char* file,
                            const char* mode = "rb") override {
        AAsset* asset = AAssetManager_open(mgr, file, AASSET_MODE_UNKNOWN);
        if (asset == NULL)
        {
            // Workaround for issue
            // https://github.com/assimp/assimp/issues/641
            // Look for the file in the directory of the previously loaded
            // file.
            std::string file2 = last_path + "/" + file;
            asset = AAssetManager_open(mgr, file2.c_str(),
                                     AASSET_MODE_UNKNOWN);
            if (asset == NULL)
                // Replace with proper exception class.
                throw "Failed opening asset file";
        }
        last_path = directory(file);
        return new ZippedFile(asset);
    }

    void Close (Assimp::IOStream*) override {
        delete ((ZippedFile*)zipped_file);
    }

    bool ComparePaths (const char*, const char*) const override {
        return strcmp(a,b) == 0;
    }

    bool Exists (const char* file) const override {
        AAsset* asset = AAssetManager_open(mgr, file, AASSET_MODE_UNKNOWN);
        if (asset != NULL)
        {
            AAsset_close(asset);
            return true;
        }
        else return false;
    }

    char getOsSeparator () const override {
        return '/';
    }

private:

    AAssetManager* mgr;
    std::string last_path;
};

APKIOSystem uses the Asset API to open files from an APK. Upon successfully loading a file, APKIOSystem returns a ZippedFile. A ZippedFile is an implementation of IOStream that wraps an AAsset to read from the asset file:

class ZippedFile final : public Assimp::IOStream
{
public:

    /// Construct a ZippedFile from an AAsset*.
    /// ZippedFile takes ownership of the AAsset*.
    ZippedFile (AAsset* asset)
        : asset(asset) {}

    ~ZippedFile ()
    {
        AAsset_close(asset);
    }

    std::size_t FileSize () const override {
        return AAsset_getLength64(asset);
    }

    void Flush () override {
        throw "ZippedFile::Flush() is unsupported";
    }

    std::size_t Read (void* buf, std::size_t size, std::size_t count)
      override {
        return AAsset_read(asset, buf, size*count);
    }

    aiReturn Seek (std::size_t offset, aiOrigin origin) override {
        AAsset_seek64(asset, offset, to_whence(origin));
    }

    std::size_t Tell () const override {
        return AAsset_getLength64(asset) -
               AAsset_getRemainingLength64(asset);
    }

    std::size_t Write (const void*, std::size_t, std::size_t) override {
        throw "ZippedFile::Write() is unsupported";
    }

private:

    AAsset* asset;
};

Writing to the file is unsupported in this implementation, which is why we throw exceptions in Flush() and Write(). It would also be wise to throw a proper exception object from a class deriving from std::exception instead of throwing strings in the above implementations.

The helper functions used in ZippedFile follow:

int to_whence (aiOrigin origin)
{
    if (origin == aiOrigin_SET) return SEEK_SET;
    if (origin == aiOrigin_CUR) return SEEK_CUR;
    if (origin == aiOrigin_END) return SEEK_END;
    throw EXCEPTION("APKIOSystem to_whence: invalid aiOrigin");
}

std::string directory (const char* filepath)
{
    std::string dir = filepath;
    std::size_t i = dir.rfind('/');
    if (i != std::string::npos)
        dir = dir.substr(0, i);
    return dir;
}

Loading Textures

Assimp will tell us what textures a model is using, but it won’t load the texture data for us. Again, textures must be loaded from the application’s APK. To this end, we can write a function like the following:

std::string read_file (AAssetManager* mgr, const char* path)
{
    AAsset* asset = AAssetManager_open(mgr, path, AASSET_MODE_UNKNOWN);
    if (asset == NULL)
        throw "Failed opening file";
    std::size_t size = AAsset_getLength64(asset);
    std::string data;
    data.resize(size);
    AAsset_read(asset, &data[0], size);
    AAsset_close(asset);
    return data;
}

The read_file function reads a file from an APK into memory. We can then use a library like DevIL to read the in-memory texture file and get a hand on the raw texture data. Unlike other image libraries, DevIL allows us to load texture files not only from the file system, but also directly from memory. This is done with the ilLoadL function.