crashinvaders / gdx-basis-universal

KTX2/Basis Universal supercompressed GPU textures for libGDX game framework.
17 stars 2 forks source link
basis-universal libgdx opengl texture

GDX Basis Universal

Maven Central libGDX Basis Universal

Cross-platform support for Binomial's Basis Universal supercompressed GPU textures.

Use the same intermediate compressed texture assets (.ktx2/.basis) for all the libGDX backends and save on video memory leveraging the platforms' natively supported GPU compression.

If you've never heard of the Basis Universal project or are unfamiliar with the "supercompressed texture" term, this is how it works... #### The problem When using traditional image formats (like PNG, JPG, TIFF, etc), they all get decoded to plain (uncompressed) RGB/RGBA representation before being supplied to a rendering API (OpenGL) and loaded to RAM. This is mostly fine, but once you get to the point when you need to use lots of simultaneously loaded huge textures you may easily run out of memory (especially on mobile devices). To give a better idea, a 4096x4096 RGBA32 (8 bits per channel) texture, being loaded into the GPU, holds roughly 64MB of memory. Stack a few of those and you're pretty much screwed. #### The solution(?) To address this issue many GPU manufacturers implement hardware support for specific texture compression formats. Some may help you to chop down the memory footprint with the compression ratio of an impressive 8 times if you've agreed on a small trade-off in image quality (most of the texture compression formats are lossy). The only downside is there is no one universal texture compression format that guarantees to work on every single platform/hardware. And if you're up for the game of supplying GPU compressed textures for your game, you have to pack your distributions with a lot of specific types of compressed textures per device hardware support for them (to mention, what in a way [Unity practices](https://docs.unity3d.com/Manual/class-TextureImporterOverride.html) for quite a while). #### The compromise To address this issue, [Binomial LLC](http://www.binomial.info/) founded their Basis Universal project. The solution is to use one intermediate compressed texture format (super-compressed) and transcode it on runtime to one of the supported texture formats by the running platform. The Basis transcoder is capable of transcoding a Basis texture to one of the dozen native GPU compressed formats and covers all the modern platforms that way. There's a little overhead price for processing the texture data, but the transcoding operation is highly optimized and, to say, only should happen once upon asset loading. The transcoder uses a number of transcoding tables to port data across different formats. Some of them are pretty bulky and thus being dropped from compilation for specific platforms (e.g. there's no reason to support mobile-only formats like PVRTC1 on desktop). So this library maintains the selection logic as well, leaving you with a simple portable compressed texture format that will transparently work everywhere. Basis Universal is backed by Google, open-source, and now available to everyone!

Cool, how do I turn my images into Basis textures?

GDX Texture Packer GUI has full support for encoding atlases as Basis Universal textures. Also, it provides a CLI interface to turn any PNG/JPG image into a KTX2/Basis texture.

You can also use the official command-line tool or build the encoder from the sources yourself.

There could be some other options (even potentially encoding in the browser), but I'm not aware of ATM, and it's worth googling.

Please read the "Texture format notes" and "Feature support notes" sections before encoding your textures.

Using the library

Once added as a Maven dependency to libGDX project modules, the library is pretty easy to deal with, no global initialization calls are required in the code.

All the official libGDX backends are fully supported.

Connecting dependencies (Gradle)

The release and snapshot Maven artifacts are available on the Maven Central repository

buildscript {
    repositories {
        mavenCentral()
        maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } // <-- optional, for snapshot versions
    }
}

And then just add these records to the dependency section of your build.gradle files.

Don't forget to set gdxBasisuVersion property with the correct library version (e.g. declaring gdxBasisuVersion=1.0.2 in the project's root settings.gradle file).

Core module

dependencies {
    api "com.crashinvaders.basisu:basisu-wrapper:$gdxBasisuVersion"
    api "com.crashinvaders.basisu:basisu-gdx:$gdxBasisuVersion"
}

Desktop module (LWJGL, LWJGL3, Headless backends)

dependencies {
    runtimeOnly "com.crashinvaders.basisu:basisu-wrapper:$gdxBasisuVersion:natives-desktop"
}

Android module (Android backend)

dependencies {
    natives "com.crashinvaders.basisu:basisu-wrapper:$gdxBasisuVersion:natives-armeabi-v7a"
    natives "com.crashinvaders.basisu:basisu-wrapper:$gdxBasisuVersion:natives-arm64-v8a"
    natives "com.crashinvaders.basisu:basisu-wrapper:$gdxBasisuVersion:natives-x86"
    natives "com.crashinvaders.basisu:basisu-wrapper:$gdxBasisuVersion:natives-x86_64"
}

iOS module (RoboVM backend)

It's highly recommended to use robovm-metalangle libGDX backend in favor of classic robovm, as this is the only way to unlock access to more compressed texture formats on Apple devices.

dependencies {
    implementation "com.crashinvaders.basisu:basisu-wrapper:$gdxBasisuVersion:natives-ios"

    // This is highly recomended, otherwise you're stuck with only PVRTC1 textures.
    implementation "com.badlogicgames.gdx:gdx-backend-robovm-metalangle:$gdxVersion"
}

Web module (GWT backend)

As usual, the GWT module requires a bit more dance around. You need to declare an extra dependency and the sources for all the used jars.

dependencies {
    implementation "com.crashinvaders.basisu:basisu-gdx-gwt:$gdxBasisuVersion"
    implementation "com.crashinvaders.basisu:basisu-gdx-gwt:$gdxBasisuVersion:sources"
    implementation "com.crashinvaders.basisu:basisu-gdx:$gdxBasisuVersion:sources"
    implementation "com.crashinvaders.basisu:basisu-wrapper:$gdxBasisuVersion:sources"
    implementation "com.crashinvaders.basisu:basisu-wrapper:$gdxBasisuVersion:natives-web"
}

Don't forget to add a GWT module entry to your GdxDefinition.gwt.xml file.

<module>
    <inherits name='com.crashinvaders.basisu.BasisuGdxGwt'/>
</module>

Example code

The library provides transparent support for KTX2/Basis format textures using Ktx2TextureData/BasisuTextureData classes respectively. Each acts very similar to libGDX's implementation of ETC1TextureData. The only difference is that libGDX doesn't know how to load .ktx2/.basis texture files out of the box, so you have to explicitly use the proper texture data class when creating a texture.

Texture myTexture = new Texture(new Ktx2TextureData(Gdx.files.internal("MyTexture.ktx2")));

From now on, it's safe to use the texture instance as usual, and it already should hold the data transcoded to the best suited native GPU compressed texture format for your platform.

Asset manager integration

If you're using AssetManager to load game assets, you can easily integrate with it as well using BasisuTextureLoader class.

// Register the texture loader for the ".ktx2" file extension.
assetManager.setLoader(Texture.class, ".ktx2", new Ktx2TextureLoader(assetManager.getFileHandleResolver()));

// Post your texture assets for loading as usual.
assetManager.load("MyTexture.ktx2", Texture.class);
// You can also use ktx2-based atlases.
// The Basis textures will be automatically resolved and loaded.
assetManager.load("MyAtlas.atlas", TextureAtlas.class);

// When the asset manager has finished loading, retrieve the assets as usual.
Texture myTexture = assetManager.get("MyTexture.ktx2", Texture.class);
TextureAtlas myAtlas = assetManager.get("MyAtlas.atlas", TextureAtlas.class);

You can use gdx-texture-packer-gui to create Basis based texture atlases.

Platform limitations

Here's the list of the limitations you should be aware of when using this library (on top of regular libGDX backend limitations).

Basis Universal feature support notes

Most of the essential Basis transcoder features are exposed and implemented for Java (including file validation, transcoding to all the necessary formats, and KTX2/Basis file/image information lookup methods).

Transcoding from both intermediate formats (ETC1S low-medium quality and UASTC high quality) works as intended.

Mipmaps are supported, but not implemented on the libGDX integration side. The feature will be enabled in future releases.

Texture channel layout types

There are four possible texture channel layout types:

  1. RGBA - three-color component images with full alpha channel support
  2. RGB - fully opaque three-color component images
  3. RG or XY - luminance and alpha
  4. R - luminance or alpha

The RGBA and RGB types are the general case. For these, there is the most variety of transcoder texture formats supported.

The rest two are considered niche types, and the transcoder has fewer options.

Please be aware, that at the moment the default texture format selector doesn't recognize RG and R texture types, and they will be treated as RGBA and RGB respectively. If you need support for them, provide a custom selector implementation.

Basis texture types

Basis supports five different texture types:

  1. Regular 2D - an arbitrary array of 2D RGB or RGBA images with optional mipmaps, array size = # images, each image may have a different resolution and # of mipmap levels.
  2. Regular 2D array - an array of 2D RGB or RGBA images with optional mipmaps, array size = # images, each image has the same resolution and mipmap levels.
  3. Cubemap array - an array of cubemap levels, total # of images must be divisible by 6, in X+, X-, Y+, Y-, Z+, Z- order, with optional mipmaps.
  4. Video frames - an array of 2D video frames, with optional mipmaps, # frames = # images, each image has the same resolution and # of mipmap levels.
  5. volume - a 3D texture with optional mipmaps, Z dimension = # images, each image has the same resolution and # of mipmap levels.

Out of which support only for Regular 2D is implemented through BasisuTextureData for libGDX textures.

I'm not very familiar with 3D related formats like Cubemap array and skipped them for now. The Basis data for those is fully available from basisu-wrapper, only the libGDX support is missing. If you're in demand for those or may assist with the implementation, please come forward and open an issue with the details.

KTX2 vs Basis files

Simply put, when in doubt, go with .ktx2 files in favor of .basis.

The long story. Those are simply two different texture file containers. Both are capable of containing the same Basis textures, but a little differently in terms of inner layout (like mp4 and mpv in the world of video files). Historically .basis was the only container, made specifically for the needs of the Basis Universal library. Later on, Basis Universal was standardized by The Khronos Group and the new .ktx2 container came along.

Debugging/diagnostics

If you're writing your own transcoder format selector or just wish to know which of the textures are supported on the specific platform, here's a little snippet that logs out the Basis Universal support report:

BasisuNativeLibLoader.loadIfNeeded(); // Make sure the Basis Universal natives are loaded.
Gdx.app.log(TAG, BasisuGdxUtils.reportAvailableTranscoderFormats(BasisuTextureFormat.ETC1S));
Gdx.app.log(TAG, BasisuGdxUtils.reportAvailableTranscoderFormats(BasisuTextureFormat.UASTC4x4));

Besides that, the library prints a bunch of useful information (like texture transcoding operations) to debug log. Check it on your device.

Texture format notes

Basis Universal texture transcoder supports a bunch of very different GPU-compressed texture formats. Some of them impose very important limitations and cannot be used (cannot be transcoded to on runtime) unless all the requirements are met.

To have the widest possible native format support, it's highly recommended to encode intermediate Basis images that comply with ALL of these specifics.

To round up, always use square images with the power of two dimensions for Basis texture assets.

Texture format resolution strategy

Basis textures can be easily transcoded to many other texture formats. This is great, but another challenge here is to transcode to the format that is most appropriate for the current runtime platform.

Here are all the criteria we should respect in making such a decision (the most important ones at the top):

The default texture format selector logic is implemented based on these. That way it should always pick the best available option. In case there are none of the texture formats are passing the check, the selector falls back to the uncompressed texture formats (RGBA8888/RGB888). Which are regular libGDX texture formats and have guaranteed support on all the platforms.

If you require a different selection strategy, you can always create a custom implementation for BasisuTextureFormatSelector and use it selectively with BasisuTextureData#setTextureFormatSelector()/Ktx2TextureData#setTextureFormatSelector() methods or set it to be used as the default selector by updating the BasisuGdxUtils#defaultFormatSelector static field.

Please be aware, that at the moment the default texture format selector doesn't recognize RG and R texture types and they will be treated as RGBA and RGB respectively. If you need support for them, provide a custom selector implementation.

Native dependencies

The project uses Basis Universal C/C++ code and JNI wrappers to connect to libGDX cross-platform code. basisu-wrapper module provides a pure Java (no dependencies) abstraction layer over the native libs.

Read more about the module and the native library building notes on the module's page.