https://github.com/xonotic/netradiant/pull/1 https://gitlab.com/xonotic/netradiant/-/issues/5 This pull request adds support for Khronos Textures to NetRadiant and Q3Map2, with OpenGL ES 2.0 formats, Ericsson Texture Compression version 1, and BGR/BGRA. The patent-free Ericsson Texture Compression format will be used in the next version of Warsow to significantly reduce VRAM usage on mobile GPUs and the integrated GPU of Intel Broadwell.
417 lines
10 KiB
C++
417 lines
10 KiB
C++
/*
|
|
Copyright (C) 2015, SiPlus, Chasseur de bots.
|
|
All Rights Reserved.
|
|
|
|
This file is part of GtkRadiant.
|
|
|
|
GtkRadiant is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
GtkRadiant is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with GtkRadiant; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
#include "ktx.h"
|
|
|
|
#include <string.h>
|
|
|
|
#include "bytestreamutils.h"
|
|
#include "etclib.h"
|
|
#include "ifilesystem.h"
|
|
#include "imagelib.h"
|
|
|
|
|
|
const int KTX_TYPE_UNSIGNED_BYTE = 0x1401;
|
|
const int KTX_TYPE_UNSIGNED_SHORT_4_4_4_4 = 0x8033;
|
|
const int KTX_TYPE_UNSIGNED_SHORT_5_5_5_1 = 0x8034;
|
|
const int KTX_TYPE_UNSIGNED_SHORT_5_6_5 = 0x8363;
|
|
|
|
const int KTX_FORMAT_ALPHA = 0x1906;
|
|
const int KTX_FORMAT_RGB = 0x1907;
|
|
const int KTX_FORMAT_RGBA = 0x1908;
|
|
const int KTX_FORMAT_LUMINANCE = 0x1909;
|
|
const int KTX_FORMAT_LUMINANCE_ALPHA = 0x190A;
|
|
const int KTX_FORMAT_BGR = 0x80E0;
|
|
const int KTX_FORMAT_BGRA = 0x80E1;
|
|
|
|
const int KTX_FORMAT_ETC1_RGB8 = 0x8D64;
|
|
|
|
class KTX_Decoder
|
|
{
|
|
public:
|
|
virtual ~KTX_Decoder() = default;
|
|
virtual void Decode( PointerInputStream& istream, byte* out ) const = 0;
|
|
virtual unsigned int GetPixelSize() const = 0;
|
|
};
|
|
|
|
class KTX_Decoder_A8 final : public KTX_Decoder
|
|
{
|
|
public:
|
|
void Decode( PointerInputStream& istream, byte* out ) const override {
|
|
out[0] = out[1] = out[2] = 0;
|
|
out[3] = istream_read_byte( istream );
|
|
}
|
|
unsigned int GetPixelSize() const override {
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
class KTX_Decoder_RGB8 final : public KTX_Decoder
|
|
{
|
|
public:
|
|
void Decode( PointerInputStream& istream, byte* out ) const override {
|
|
istream.read( out, 3 );
|
|
out[3] = 255;
|
|
}
|
|
unsigned int GetPixelSize() const override {
|
|
return 3;
|
|
}
|
|
};
|
|
|
|
class KTX_Decoder_RGBA8 final : public KTX_Decoder
|
|
{
|
|
public:
|
|
void Decode( PointerInputStream& istream, byte* out ) const override {
|
|
istream.read( out, 4 );
|
|
}
|
|
unsigned int GetPixelSize() const override {
|
|
return 4;
|
|
}
|
|
};
|
|
|
|
class KTX_Decoder_L8 final : public KTX_Decoder
|
|
{
|
|
public:
|
|
void Decode( PointerInputStream& istream, byte* out ) const override {
|
|
byte l = istream_read_byte( istream );
|
|
out[0] = out[1] = out[2] = l;
|
|
out[3] = 255;
|
|
}
|
|
unsigned int GetPixelSize() const override {
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
class KTX_Decoder_LA8 final : public KTX_Decoder
|
|
{
|
|
public:
|
|
void Decode( PointerInputStream& istream, byte* out ) const override {
|
|
byte la[2];
|
|
istream.read( la, 2 );
|
|
out[0] = out[1] = out[2] = la[0];
|
|
out[3] = la[1];
|
|
}
|
|
unsigned int GetPixelSize() const override {
|
|
return 2;
|
|
}
|
|
};
|
|
|
|
class KTX_Decoder_BGR8 final : public KTX_Decoder
|
|
{
|
|
public:
|
|
void Decode( PointerInputStream& istream, byte* out ) const override {
|
|
byte bgr[3];
|
|
istream.read( bgr, 3 );
|
|
out[0] = bgr[2];
|
|
out[1] = bgr[1];
|
|
out[2] = bgr[0];
|
|
out[3] = 255;
|
|
}
|
|
unsigned int GetPixelSize() const override {
|
|
return 3;
|
|
}
|
|
};
|
|
|
|
class KTX_Decoder_BGRA8 final : public KTX_Decoder
|
|
{
|
|
public:
|
|
void Decode( PointerInputStream& istream, byte* out ) const override {
|
|
byte bgra[4];
|
|
istream.read( bgra, 4 );
|
|
out[0] = bgra[2];
|
|
out[1] = bgra[1];
|
|
out[2] = bgra[0];
|
|
out[3] = bgra[3];
|
|
}
|
|
unsigned int GetPixelSize() const override {
|
|
return 4;
|
|
}
|
|
};
|
|
|
|
class KTX_Decoder_RGBA4 final : public KTX_Decoder
|
|
{
|
|
protected:
|
|
const bool m_bigEndian;
|
|
public:
|
|
KTX_Decoder_RGBA4( bool bigEndian ) : m_bigEndian( bigEndian ){}
|
|
void Decode( PointerInputStream& istream, byte* out ) const override {
|
|
uint16_t rgba;
|
|
if ( m_bigEndian ) {
|
|
rgba = istream_read_uint16_be( istream );
|
|
}
|
|
else {
|
|
rgba = istream_read_uint16_le( istream );
|
|
}
|
|
int r = ( rgba >> 12 ) & 0xf;
|
|
int g = ( rgba >> 8 ) & 0xf;
|
|
int b = ( rgba >> 4 ) & 0xf;
|
|
int a = rgba & 0xf;
|
|
out[0] = ( r << 4 ) | r;
|
|
out[1] = ( g << 4 ) | g;
|
|
out[2] = ( b << 4 ) | b;
|
|
out[3] = ( a << 4 ) | a;
|
|
}
|
|
unsigned int GetPixelSize() const override {
|
|
return 2;
|
|
}
|
|
};
|
|
|
|
class KTX_Decoder_RGBA5 final : public KTX_Decoder
|
|
{
|
|
protected:
|
|
const bool m_bigEndian;
|
|
public:
|
|
KTX_Decoder_RGBA5( bool bigEndian ) : m_bigEndian( bigEndian ){}
|
|
void Decode( PointerInputStream& istream, byte* out ) const override {
|
|
uint16_t rgba;
|
|
if ( m_bigEndian ) {
|
|
rgba = istream_read_uint16_be( istream );
|
|
}
|
|
else {
|
|
rgba = istream_read_uint16_le( istream );
|
|
}
|
|
int r = ( rgba >> 11 ) & 0x1f;
|
|
int g = ( rgba >> 6 ) & 0x1f;
|
|
int b = ( rgba >> 1 ) & 0x1f;
|
|
out[0] = ( r << 3 ) | ( r >> 2 );
|
|
out[1] = ( g << 3 ) | ( g >> 2 );
|
|
out[2] = ( b << 3 ) | ( b >> 2 );
|
|
out[3] = ( rgba & 1 ) * 255;
|
|
}
|
|
unsigned int GetPixelSize() const override {
|
|
return 2;
|
|
}
|
|
};
|
|
|
|
class KTX_Decoder_RGB5 final : public KTX_Decoder
|
|
{
|
|
protected:
|
|
const bool m_bigEndian;
|
|
public:
|
|
KTX_Decoder_RGB5( bool bigEndian ) : m_bigEndian( bigEndian ){}
|
|
void Decode( PointerInputStream& istream, byte* out ) const override {
|
|
uint16_t rgb;
|
|
if ( m_bigEndian ) {
|
|
rgb = istream_read_uint16_be( istream );
|
|
}
|
|
else {
|
|
rgb = istream_read_uint16_le( istream );
|
|
}
|
|
int r = ( rgb >> 11 ) & 0x1f;
|
|
int g = ( rgb >> 5 ) & 0x3f;
|
|
int b = rgb & 0x1f;
|
|
out[0] = ( r << 3 ) | ( r >> 2 );
|
|
out[1] = ( g << 2 ) | ( g >> 4 );
|
|
out[2] = ( b << 3 ) | ( b >> 2 );
|
|
out[3] = 255;
|
|
}
|
|
unsigned int GetPixelSize() const override {
|
|
return 2;
|
|
}
|
|
};
|
|
|
|
static void KTX_DecodeETC1( PointerInputStream& istream, Image& image ){
|
|
unsigned int width = image.getWidth(), height = image.getHeight();
|
|
unsigned int stride = width * 4;
|
|
byte* pixbuf = image.getRGBAPixels();
|
|
byte etc[8], rgba[64];
|
|
|
|
for ( unsigned int y = 0; y < height; y += 4, pixbuf += stride * 4 )
|
|
{
|
|
unsigned int blockrows = height - y;
|
|
if ( blockrows > 4 ) {
|
|
blockrows = 4;
|
|
}
|
|
|
|
byte* p = pixbuf;
|
|
for ( unsigned int x = 0; x < width; x += 4, p += 16 )
|
|
{
|
|
istream.read( etc, 8 );
|
|
ETC_DecodeETC1Block( etc, rgba, true );
|
|
|
|
unsigned int blockrowsize = width - x;
|
|
if ( blockrowsize > 4 ) {
|
|
blockrowsize = 4;
|
|
}
|
|
blockrowsize *= 4;
|
|
for ( unsigned int blockrow = 0; blockrow < blockrows; blockrow++ )
|
|
{
|
|
memcpy( p + blockrow * stride, rgba + blockrow * 16, blockrowsize );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Image* LoadKTXBuff( PointerInputStream& istream ){
|
|
byte identifier[12];
|
|
istream.read( identifier, 12 );
|
|
if ( memcmp( identifier, "\xABKTX 11\xBB\r\n\x1A\n", 12 ) ) {
|
|
globalErrorStream() << "LoadKTX: Image has the wrong identifier\n";
|
|
return 0;
|
|
}
|
|
|
|
const bool bigEndian = ( istream_read_uint32_le( istream ) == 0x01020304 );
|
|
|
|
unsigned int type;
|
|
if ( bigEndian ) {
|
|
type = istream_read_uint32_be( istream );
|
|
}
|
|
else {
|
|
type = istream_read_uint32_le( istream );
|
|
}
|
|
|
|
// For compressed textures, the format is in glInternalFormat.
|
|
// For uncompressed textures, it's in glBaseInternalFormat.
|
|
istream.seek( ( type ? 3 : 2 ) * sizeof( uint32_t ) );
|
|
unsigned int format;
|
|
if ( bigEndian ) {
|
|
format = istream_read_uint32_be( istream );
|
|
}
|
|
else {
|
|
format = istream_read_uint32_le( istream );
|
|
}
|
|
if ( !type ) {
|
|
istream.seek( sizeof( uint32_t ) );
|
|
}
|
|
|
|
unsigned int width, height;
|
|
if ( bigEndian ) {
|
|
width = istream_read_uint32_be( istream );
|
|
height = istream_read_uint32_be( istream );
|
|
}
|
|
else {
|
|
width = istream_read_uint32_le( istream );
|
|
height = istream_read_uint32_le( istream );
|
|
}
|
|
if ( !width ) {
|
|
globalErrorStream() << "LoadKTX: Image has zero width\n";
|
|
return 0;
|
|
}
|
|
if ( !height ) {
|
|
height = 1;
|
|
}
|
|
|
|
// Skip the key/values and load the first 2D image in the texture.
|
|
// Since KTXorientation is only a hint and has no effect on the texture data and coordinates, it must be ignored.
|
|
istream.seek( 4 * sizeof( uint32_t ) );
|
|
unsigned int bytesOfKeyValueData;
|
|
if ( bigEndian ) {
|
|
bytesOfKeyValueData = istream_read_uint32_be( istream );
|
|
}
|
|
else {
|
|
bytesOfKeyValueData = istream_read_uint32_le( istream );
|
|
}
|
|
istream.seek( bytesOfKeyValueData + sizeof( uint32_t ) );
|
|
|
|
RGBAImage* image = new RGBAImage( width, height );
|
|
|
|
if ( type ) {
|
|
KTX_Decoder* decoder = NULL;
|
|
switch ( type )
|
|
{
|
|
case KTX_TYPE_UNSIGNED_BYTE:
|
|
switch ( format )
|
|
{
|
|
case KTX_FORMAT_ALPHA:
|
|
decoder = new KTX_Decoder_A8();
|
|
break;
|
|
case KTX_FORMAT_RGB:
|
|
decoder = new KTX_Decoder_RGB8();
|
|
break;
|
|
case KTX_FORMAT_RGBA:
|
|
decoder = new KTX_Decoder_RGBA8();
|
|
break;
|
|
case KTX_FORMAT_LUMINANCE:
|
|
decoder = new KTX_Decoder_L8();
|
|
break;
|
|
case KTX_FORMAT_LUMINANCE_ALPHA:
|
|
decoder = new KTX_Decoder_LA8();
|
|
break;
|
|
case KTX_FORMAT_BGR:
|
|
decoder = new KTX_Decoder_BGR8();
|
|
break;
|
|
case KTX_FORMAT_BGRA:
|
|
decoder = new KTX_Decoder_BGRA8();
|
|
break;
|
|
}
|
|
break;
|
|
case KTX_TYPE_UNSIGNED_SHORT_4_4_4_4:
|
|
if ( format == KTX_FORMAT_RGBA ) {
|
|
decoder = new KTX_Decoder_RGBA4( bigEndian );
|
|
}
|
|
break;
|
|
case KTX_TYPE_UNSIGNED_SHORT_5_5_5_1:
|
|
if ( format == KTX_FORMAT_RGBA ) {
|
|
decoder = new KTX_Decoder_RGBA5( bigEndian );
|
|
}
|
|
break;
|
|
case KTX_TYPE_UNSIGNED_SHORT_5_6_5:
|
|
if ( format == KTX_FORMAT_RGB ) {
|
|
decoder = new KTX_Decoder_RGB5( bigEndian );
|
|
}
|
|
break;
|
|
}
|
|
|
|
if ( !decoder ) {
|
|
globalErrorStream() << "LoadKTX: Image has an unsupported pixel type " << type << " or format " << format << "\n";
|
|
image->release();
|
|
return 0;
|
|
}
|
|
|
|
unsigned int inRowLength = width * decoder->GetPixelSize();
|
|
unsigned int inPadding = ( ( inRowLength + 3 ) & ~3 ) - inRowLength;
|
|
byte* out = image->getRGBAPixels();
|
|
for ( unsigned int y = 0; y < height; y++ )
|
|
{
|
|
for ( unsigned int x = 0; x < width; x++, out += 4 )
|
|
{
|
|
decoder->Decode( istream, out );
|
|
}
|
|
|
|
if ( inPadding ) {
|
|
istream.seek( inPadding );
|
|
}
|
|
}
|
|
|
|
delete decoder;
|
|
}
|
|
else {
|
|
switch ( format )
|
|
{
|
|
case KTX_FORMAT_ETC1_RGB8:
|
|
KTX_DecodeETC1( istream, *image );
|
|
break;
|
|
default:
|
|
globalErrorStream() << "LoadKTX: Image has an unsupported compressed format " << format << "\n";
|
|
image->release();
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return image;
|
|
}
|
|
|
|
Image* LoadKTX( ArchiveFile& file ){
|
|
ScopedArchiveBuffer buffer( file );
|
|
PointerInputStream istream( buffer.buffer );
|
|
return LoadKTXBuff( istream );
|
|
}
|