Assimp on Android
2015-08-19
Tips and tricks for getting Assimp to work on Android.
Compiling Assimp for Android
See the following links:
- http://www.myandroidonline.com/2015/06/09/compile-assimp-open-source-library-for-android/
- http://scriptogr.am/phnguyen/post/assimp-on-desktop-and-mobile
- Android NDK Adventures
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.