Open expenses opened 2 years ago
toktx
from https://github.com/KhronosGroup/KTX-Software has the ability to compress JPEG/PNG images to ASTC KTX2s. I didn't realise before how many different block sizes for ASTC there are:
This means we can be extremely flexible and get extremely high levels of compression for images that need it.
Background
Textures in the GPU sense are essentially linear arrays of (generally but not always) pixels in memory that is accessible to the GPU. These can be various sizes, 1D or 2D or 3D, LDR (Low Dynamic Range, with values between 0 and 1) or HDR (High Dynamic Range, with values in a greater range (sometimes negative!)). Blah blah blah.
One important element is that textures generally contain mipmaps. These are sets of smaller versions of the image where each mip is half the size of the mip before.
_https://commons.wikimedia.org/wiki/File:MipMap_Example_STS101.jpg_
The reason for mipmaps is that if you always read from textures at the highest resolution, you're going to get aliasing effects when the texture is far away and you start skipping over pixels. If you read a mip level for the appropriate size the texture appears on the screen then can avoid this:
_https://commons.wikimedia.org/wiki/File:Mipmap_Aliasing_Comparison.png_
Regular PNG and JPEG images
When loading PNG and JPEG images, we need to decompress them to get out the raw bytes of pixels to upload to the GPU. Because PNG and JPEG images have no support for mipmaps, we need to generate these on the GPU after uploading. This takes a small amount of time.
Additionally, if an image we attempt to load exceeds the maximum allowed texture size of the device (e.g. 2048x2048) then we're in trouble as loading it (even to just generate mipmaps) would crash the device. We could either (s l o w l y) downscale the image on the CPU to the max allowed size or just not load the image at all. Both of these options are bad.
The KTX2 format
The KTX2 file format is a container format for images intended to be loaded as textures onto a GPU. It is not, itself, an image codec in the same was as PNG or JPEG is.
KTX2s contain some really nice properties:
An additional consequence of KTX2s containing mipmaps is that if the largest size of the texture is exceeds the maximum allowed texture size of the device, we can just use a texture down in the mip chain as the base texture. This ensures that pretty much any valid KTX2 should be loadable.
While you could create KTX2 with the raw decompressed bytes of PNG/JPEG images, it's generally a bad idea because the file sizes will be very large. With 1 byte per channel (red/green/blue/alpha), you're looking at 4 bytes per pixel and 1024 1024 4 = 4MB for a 1024^2 image.
Therefore people generally use them with:
Block-compressed texture formats
GPUs have their own type of compressed texture format, but these are a bit different from the run-length-encoding compression that JPG/PNG etc. have. The basic idea is that the textures are divided up into 4x4 blocks which are then compressed and decompressed individually.
I'm just going to quote https://themaister.net/blog/2020/08/12/compressed-gpu-texture-formats-a-review-and-compute-shader-decoders-part-1/ (all 3 parts are worth reading!) here:
The main idea behind these block-compressed formats is that as they're 4x smaller than the equivalent bytes without compression, they take up much less memory bandwidth when rendering, which increases performance.
Because of patents (now expired?) and other stuff, the compressed texture ecosystem is split in this wonky way. There are essentially 2 families of compressed textures worth caring about:
To re-post the platform support table from https://github.com/KhronosGroup/3D-Formats-Guidelines/blob/main/KTXDeveloperGuideWebGL.md#platform-support-table:
Because the ecosystem is split this way we get to:
The Basis Universal GPU Texture Codec
https://github.com/BinomialLLC/basis_universal
So Basis Universal is another compressed texture codec, but it's not actually one that any GPU natively supports. Instead it's more of an interchange format that can be quickly transcoded to either ASTC or BC7. There are actually 2 formats: ETC1S and UASTC. As far as I'm aware, ETC1S is lower quality and older so we'll ignore that. The
basisu
binary takes a JPG/PNG image, generates mipmaps for it, compresses it and writes it to a KTX2 file.After both block compression and KTX2 supercompression has been applied, the resulting KTX2 files are at max 3x bigger than the source JPG/PNG images, while taking up much less memory and bandwidth on the GPU and being able to be progressively loaded.
How we use textures in superconductor
Cubemaps
Cubemaps for Image-Based Lighting (IBL) require HDR colour data (you could try and use LDR data with them but I doubt it'd turn out well ^_^). Currently we use BC6H textures for these, in a KTX2 container that also specifies the sphere harmonics in the key-value section.
As BC6H is not a format supported on mobile, I use a shader on mobile to decompress from BC6H to another format (currently
Rg11b10Float
): https://github.com/MeetKai/superconductor/tree/main/granite-shaders.This is a bad solution. Decompressing like this introduces some loss in quality (as
Rg11b10Float
supports a reduced float range), takes some time andRg11b10Float
is still 4x bigger than BC6H.Ideally I'd have 2 seperate cubemap files, one that's BC6H for Desktop and one that's ASTC for mobile. Unfortunately the only ASTC encoder that supports HDR data is astcenc. I need to update the rust bindings to that so that I can add it to my cubemap compression tool.
UASTC
For transcoding UASTC files in a native binary, the https://github.com/aclysma/basis-universal-rs bindings work great. Wasm is a bit more painful though. Basis universal is a C++ library that can't be bound to with wasm-bindgen, so the best (but still bad) solution is to bind against the pre-made emscripten wasm binary. I've got some bindings for this here: https://github.com/expenses/basis-universal-rs-wasm and a PR open here: https://github.com/aclysma/basis-universal-rs/pull/11.
ZSTD supercompression
The
zstd
crate luckily works in wasm just fine.How we can load textures faster
It's possible that UASTC transcoding is not fast enough on mobile for our needs. In that case, I think we want to have a seperate set of texture files for mobile that are purely ASTC and load them without transcoding. Additionally, if ZSTD supercompression becomes a bottleneck (it shouldn't be) then we can turn that off for those textures as well.
At present JPEG/PNGs don't load especially fast in the browser. This is because we're decompressing them in wasm, instead of using the built-in browser's ability to do this.