Skip to content

Commit 3cf1c3c

Browse files
committed
Add ETC2 support (Pocket Edition textures)
1 parent 58bea0f commit 3cf1c3c

File tree

6 files changed

+132
-37
lines changed

6 files changed

+132
-37
lines changed

CMakeLists.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ project(Stex
1313

1414
# Get helper scripts
1515
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchOBCMake.cmake)
16-
fetch_ob_cmake("v0.3.7")
16+
fetch_ob_cmake("4640fe55c5ececb736331d5c379a2113760e9236")
1717

1818
# Initialize project according to standard rules
1919
include(OB/Project)
@@ -70,6 +70,10 @@ ob_fetch_qx(
7070
include(OB/FetchLibSquish)
7171
ob_fetch_modern_libsquish("8a6aad970285a536170de9b3fc63735a72674561")
7272

73+
# Fetch etc2comp
74+
include(OB/FetchEtc2Comp)
75+
ob_fetch_etc2comp("v1.0")
76+
7377
# Process Targets
7478
set(APP_TARGET_NAME ${PROJECT_NAMESPACE_LC}_${PROJECT_NAMESPACE_LC})
7579
set(APP_ALIAS_NAME ${PROJECT_NAMESPACE})

app/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ ob_add_standard_executable(${APP_TARGET_NAME}
4646
Qx::Io
4747
Qx::Xml
4848
libsquish::Squish
49+
Etc::Etc
4950
CONFIG STANDARD
5051
)
5152

app/src/command/tex-command.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class TexCommand : public Command
6363
{u"dxt5"_s, KTex::Header::PixelFormat::DXT5},
6464
{u"rgb"_s, KTex::Header::PixelFormat::RGB},
6565
{u"rgba"_s, KTex::Header::PixelFormat::RGBA},
66+
{u"etc2eac"_s, KTex::Header::PixelFormat::ETC2EAC}
6667
};
6768

6869
// Messages

app/src/conversion.cpp

Lines changed: 122 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
#include <cstring>
66

77
// Squish Includes
8-
#include "squish/squish.h"
8+
#include <squish/squish.h>
9+
10+
// etc2comp Includes
11+
#include <Etc/EtcImage.h>
912

1013
// NOTE: The Qt image formats (QImage::Format) used here are all byte ordered based on the host system, yet squish (and DST?)
1114
// expect the input data to always be RGB(A), which means these won't work on Big Endian ordered systems;
@@ -84,6 +87,7 @@ QVector<QImage> ToTexConverter::generateMipMaps(const QImage& baseImage)
8487

8588
QVector<KTex::MipMapImage> ToTexConverter::convertToTargetFormat(const QVector<QImage>& images)
8689
{
90+
auto pxFormat = mOptions.pixelFormat;
8791
QVector<KTex::MipMapImage> outputImages;
8892

8993
for(const QImage& image : images)
@@ -94,21 +98,72 @@ QVector<KTex::MipMapImage> ToTexConverter::convertToTargetFormat(const QVector<Q
9498
mipMap.setWidth(image.width());
9599
mipMap.setHeight(image.height());
96100

97-
// Uncompressed steps
98-
if(mOptions.pixelFormat == KTex::Header::PixelFormat::RGBA ||
99-
mOptions.pixelFormat == KTex::Header::PixelFormat::RGB)
100-
{
101-
mipMap.setPitch(image.bytesPerLine());
102-
mipMap.setImageDataSize(image.sizeInBytes());
103-
std::memcpy(mipMap.imageData().data(), image.bits(), image.sizeInBytes());
104-
}
105-
else // Compressed steps
101+
// Encoder specific steps
102+
switch(pxFormat) // Use variants, inheritance, or other functions for this if many more types are added
106103
{
107-
int squishFlag = getSquishCompressionFlag(mOptions.pixelFormat);
108-
mipMap.setPitch(squish::GetStorageRequirements(image.width(), 1, squishFlag));
109-
mipMap.setImageDataSize(squish::GetStorageRequirements(image.width(), image.height(), squishFlag));
110-
squish::CompressImage(image.bits(), image.width(), image.height(), image.bytesPerLine(),
111-
mipMap.imageData().data(), squishFlag);
104+
using enum KTex::Header::PixelFormat;
105+
106+
case RGB:
107+
case RGBA:
108+
mipMap.setPitch(image.bytesPerLine());
109+
mipMap.setImageDataSize(image.sizeInBytes());
110+
std::memcpy(mipMap.imageData().data(), image.bits(), image.sizeInBytes());
111+
break;
112+
113+
case DXT1:
114+
case DXT3:
115+
case DXT5:
116+
{
117+
int squishFlag = getSquishCompressionFlag(pxFormat);
118+
mipMap.setPitch(squish::GetStorageRequirements(image.width(), 1, squishFlag)); // Space for one row of blocks
119+
mipMap.setImageDataSize(squish::GetStorageRequirements(image.width(), image.height(), squishFlag));
120+
squish::CompressImage(image.bits(), image.width(), image.height(), image.bytesPerLine(),
121+
mipMap.imageData().data(), squishFlag);
122+
}
123+
124+
case ETC2EAC:
125+
{
126+
/* The pitch calculation below boils down to:
127+
* p = blocks_per_row * block_size
128+
* where
129+
* block_size varies with format
130+
* blocks_per_row = ceil(width/4)
131+
*/
132+
133+
// Get texture info
134+
auto etcFormat = Etc::Image::Format::RGBA8; // Make function for this, like for squish, if more ETC formats are supported
135+
136+
// Create ETC image
137+
auto errMetric = image.isGrayscale() ? Etc::ErrorMetric::GRAY : Etc::ErrorMetric::NUMERIC; // Could try the Rec 709 option for color
138+
/* The standard allows viewing a POD struct as a sequence of bytes (e.g. auto data = reinterpret_cast<uchar*>(myStruct)),
139+
* but does not allow the other way around; however, in the case of a very simple struct of ints, almost no known compiler
140+
* inserts padding between the members, so here we're gonna try what is technically UB, casting an array to a struct, since
141+
* it generally works and this application is non-critical. This is possible because each struct member is exactly 1-byte in
142+
* size and is laid out in the correct R-G-B-A order.
143+
*
144+
* This lib has a really strange interface, as it was hacked together by someone else after its initial creation.
145+
* You make an image with uncompressed pixel data, despite the type (Etc::Image) being named like you already have
146+
* a compressed image, and then call Encode.
147+
*/
148+
Etc::Image etcImage(etcFormat, (const Etc::ColorR8G8B8A8*)image.bits(), image.width(), image.height(), errMetric);
149+
150+
// Prepare mip-map
151+
mipMap.setPitch(etcImage.GetNumberOfBlockColumns() * etcImage.GetBlockSize()); // Space for one row of blocks
152+
mipMap.setImageDataSize(etcImage.GetEncodingBitsBytes());
153+
154+
// Encode
155+
constexpr float quality = 90; /* (0-100) could add flag to allow adjusting, but awkward since its just for this format, though we could just
156+
* say "for formats where it applies" and for now it's only this one. Kram uses 49 by default and states that
157+
* unity uses "80".
158+
*/
159+
auto status = etcImage.EncodeSinglepass(quality, reinterpret_cast<uchar*>(mipMap.imageData().data()));
160+
if(status != Etc::Image::SUCCESS)
161+
qWarning("Unexpected ETC2 encode error: 0x%x", status);
162+
break;
163+
}
164+
165+
default:
166+
qCritical("Unhandled encoding pixel format!");
112167
}
113168

114169
outputImages.append(mipMap);
@@ -166,31 +221,63 @@ const KTex::MipMapImage& FromTexConverter::getMainImage() { return mSourceTex.mi
166221

167222
QImage FromTexConverter::convertToStandardFormat(const KTex::MipMapImage& mainImage)
168223
{
169-
QByteArray rawData;
170-
quint16 pitch;
171-
QImage::Format rawFormat = mSourceTex.header().pixelFormat() == KTex::Header::PixelFormat::RGB ? QImage::Format_RGB888 :
172-
mOptions.demultiplyAlpha ? QImage::Format_RGBA8888_Premultiplied : QImage::Format_RGBA8888;
173-
174-
// Uncompressed steps
175-
if(mSourceTex.header().pixelFormat() == KTex::Header::PixelFormat::RGBA ||
176-
mSourceTex.header().pixelFormat() == KTex::Header::PixelFormat::RGB)
177-
{
178-
pitch = mainImage.pitch();
179-
rawData = mainImage.imageData(); // Implicit sharing avoids copy
180-
}
181-
else // Compressed steps
224+
QByteArray decodedData;
225+
quint16 decodedPitch{};
226+
QImage::Format decodedFormat{};
227+
228+
auto pxFormat = mSourceTex.header().pixelFormat();
229+
switch(pxFormat) // Use variants, inheritance, or other functions for this if many more types are added
182230
{
183-
// Always outputs in RGBA
184-
int squishFlag = getSquishCompressionFlag(mSourceTex.header().pixelFormat());
185-
pitch = mainImage.width() * 4;
186-
rawData.resize(mainImage.width() * mainImage.height() * 4);
187-
squish::DecompressImage(reinterpret_cast<uchar*>(rawData.data()), mainImage.width(), mainImage.height(), pitch,
188-
mainImage.imageData().data(), squishFlag);
231+
using enum KTex::Header::PixelFormat;
232+
233+
case RGB:
234+
decodedFormat = QImage::Format_RGB888;
235+
decodedPitch = mainImage.pitch();
236+
decodedData = mainImage.imageData(); // Implicit sharing avoids copy
237+
break;
238+
239+
case RGBA:
240+
decodedFormat = mOptions.demultiplyAlpha ? QImage::Format_RGBA8888_Premultiplied : QImage::Format_RGBA8888;
241+
decodedPitch = mainImage.pitch();
242+
decodedData = mainImage.imageData(); // Implicit sharing avoids copy
243+
break;
244+
245+
case DXT1:
246+
case DXT3:
247+
case DXT5:
248+
{
249+
decodedFormat = mOptions.demultiplyAlpha ? QImage::Format_RGBA8888_Premultiplied : QImage::Format_RGBA8888;
250+
decodedPitch = mainImage.width() * 4;
251+
int squishFlag = getSquishCompressionFlag(pxFormat);
252+
decodedData.resize(mainImage.width() * mainImage.height() * 4);
253+
squish::DecompressImage(reinterpret_cast<uchar*>(decodedData.data()), mainImage.width(), mainImage.height(), decodedPitch,
254+
mainImage.imageData().data(), squishFlag);
255+
break;
256+
}
257+
258+
case ETC2EAC:
259+
{
260+
decodedFormat = mOptions.demultiplyAlpha ? QImage::Format_RGBA8888_Premultiplied : QImage::Format_RGBA8888;
261+
decodedPitch = mainImage.width() * 4;
262+
auto etcFormat = Etc::Image::Format::RGBA8; // Make function for this, like for squish, if more ETC formats are supported
263+
decodedData.resize(mainImage.width() * mainImage.height() * 4);
264+
/* This lib has a really strange interface, as it was hacked together by someone else after its initial creation.
265+
* You make an image with no pixel data set and then pass the pixel data as part of the Encode call
266+
*/
267+
Etc::Image etcImage(etcFormat, nullptr, 1024, 1024, Etc::ErrorMetric::NUMERIC);
268+
auto status = etcImage.Decode(reinterpret_cast<const uchar*>(mainImage.imageData().data()),
269+
reinterpret_cast<uchar*>(decodedData.data()));
270+
if(status != Etc::Image::SUCCESS)
271+
qWarning("Unexpected ETC2 decode error: 0x%x", status);
272+
break;
273+
}
189274

275+
default:
276+
qCritical("Unhandled decoding pixel format!");
190277
}
191278

192279
// Create QImage from buffer
193-
QImage bufferedImage = QImage(reinterpret_cast<uchar*>(rawData.data()), mainImage.width(), mainImage.height(), pitch, rawFormat);
280+
QImage bufferedImage = QImage(reinterpret_cast<uchar*>(decodedData.data()), mainImage.width(), mainImage.height(), decodedPitch, decodedFormat);
194281

195282
// Copy and detach image so it isn't reliant on buffer that will be destroyed
196283
QImage standaloneImage = bufferedImage.copy();

app/src/klei/k-tex.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ bool KTex::supportedPixelFormat(quint8 pixelFormatVal)
115115
case static_cast<quint8>(Header::PixelFormat::DXT5):
116116
case static_cast<quint8>(Header::PixelFormat::RGB):
117117
case static_cast<quint8>(Header::PixelFormat::RGBA):
118+
case static_cast<quint8>(Header::PixelFormat::ETC2EAC): // GL_COMPRESSED_RGBA8_ETC2_EAC
118119
return true;
119120
default:
120121
return false;

app/src/klei/k-tex.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ class KTex
2727
DXT3 = 0x01,
2828
DXT5 = 0x02,
2929
RGBA = 0x04,
30-
RGB = 0x05
30+
RGB = 0x05,
31+
ETC2EAC = 0x11
3132
};
3233
enum class TextureType : quint8{
3334
OneD = 0x00,

0 commit comments

Comments
 (0)