fixed DC texconv having some weird quad issue by just rebasing from the original file and de-QTing / reconverting

This commit is contained in:
ecker 2025-08-19 00:30:34 -05:00
parent e796fee762
commit 6d4837e3cc
18 changed files with 852 additions and 473 deletions

View File

@ -78,7 +78,7 @@
"stream": { "stream": {
"tag": "worldspawn", "tag": "worldspawn",
"player": "info_player_spawn", "player": "info_player_spawn",
"enabled": false, // "auto", "enabled": "auto",
"radius": 32, "radius": 32,
"every": 1 "every": 1
} }

View File

@ -56,7 +56,11 @@ int writeTextureHeader(std::ostream& stream, int width, int height, int textureT
uint32_t combineHash(const RGBA& rgba, uint32_t seed); uint32_t combineHash(const RGBA& rgba, uint32_t seed);
class ImageContainer; class ImageContainer;
class Palette;
void convert16BPP(std::ostream& stream, const ImageContainer& images, int textureType); void convert16BPP(std::ostream& stream, const ImageContainer& images, int textureType);
Palette convertPaletted(std::ostream& stream, const ImageContainer& images, int textureType);
void convertPaletted(std::ostream& stream, const ImageContainer& images, int textureType, const uf::stl::string& palFilename); void convertPaletted(std::ostream& stream, const ImageContainer& images, int textureType, const uf::stl::string& palFilename);
bool generatePreview(const uf::stl::string& textureFilename, const uf::stl::string& paletteFilename, const uf::stl::string& previewFilename, const uf::stl::string& codeUsageFilename); bool generatePreview(const uf::stl::string& textureFilename, const uf::stl::string& paletteFilename, const uf::stl::string& previewFilename, const uf::stl::string& codeUsageFilename);

View File

@ -5,15 +5,18 @@
class Image { class Image {
public: public:
Image(); Image();
Image(int width, int height); Image(int width, int height, const uf::stl::vector<RGBA>& = {} );
bool loadFromBuffer(const uf::stl::vector<RGBA>& buffer, int width, int height);
bool loadFromFile(const uf::stl::string& path); bool loadFromFile(const uf::stl::string& path);
bool saveToFile(const uf::stl::string& path) const; bool saveToFile(const uf::stl::string& path) const;
int width() const; int width() const;
int height() const; int height() const;
const uf::stl::vector<RGBA>& pixels() const;
RGBA pixel(int x,int y) const; RGBA pixel(int x,int y) const;
void setPixel(int x,int y, RGBA pixel); void setPixel(int x,int y, RGBA pixel);
Image scaled(int newW,int newH,bool nearest) const; Image scaled(int newW,int newH,bool nearest) const;
@ -25,8 +28,8 @@ public:
bool isIndexed() const { return indexedMode; } bool isIndexed() const { return indexedMode; }
private: private:
int w,h; int w, h;
bool indexedMode; bool indexedMode;
uf::stl::vector<RGBA> pixels; uf::stl::vector<RGBA> p;
uf::stl::vector<uint8_t> indexed; uf::stl::vector<uint8_t> indexed;
}; };

View File

@ -5,7 +5,8 @@
class ImageContainer { class ImageContainer {
public: public:
bool load(const uf::stl::vector<uf::stl::string>& filenames, int textureType, int mipmapFilter); bool load( const uf::stl::vector<Image>& images, int textureType, int mipmapFilter );
bool load( const uf::stl::vector<uf::stl::string>& filenames, int textureType, int mipmapFilter );
void unloadAll(); void unloadAll();

View File

@ -19,7 +19,7 @@ public:
bool load(const uf::stl::string& filename); bool load(const uf::stl::string& filename);
bool save(const uf::stl::string& filename) const; bool save(const uf::stl::string& filename) const;
uf::stl::vector<uint8_t> encode() const;
private: private:
uf::stl::unordered_map<uint32_t,int> colorsMap; uf::stl::unordered_map<uint32_t,int> colorsMap;
uf::stl::vector<uint32_t> colorsVec; uf::stl::vector<uint32_t> colorsVec;

View File

@ -58,7 +58,6 @@ public:
float length() const; float length() const;
void setLength(float len); void setLength(float len);
void normalize(); void normalize();
void print() const;
static float distanceSquared(const Vec<N>& a, const Vec<N>& b); static float distanceSquared(const Vec<N>& a, const Vec<N>& b);
uint hash() const; uint hash() const;
void setHash(uint h) { hashVal = h; } void setHash(uint h) { hashVal = h; }
@ -177,17 +176,6 @@ inline void Vec<N>::normalize() {
v[i] *= invlen; v[i] *= invlen;
} }
template<uint N>
void Vec<N>::print() const {
uf::stl::string str = "{ ";
for (uint i=0; i<N; ++i) {
str += std::to_string(v[i]);
str += ' ';
}
str += '}';
std::cout << str;
}
template<uint N> template<uint N>
float Vec<N>::distanceSquared(const Vec<N>& a, const Vec<N>& b) { float Vec<N>::distanceSquared(const Vec<N>& a, const Vec<N>& b) {
return (a - b).lengthSquared(); return (a - b).lengthSquared();
@ -332,7 +320,7 @@ void VectorQuantizer<N>::removeUnusedCodes() {
codes.end() codes.end()
); );
if(codes.size()<oldSize){ if(codes.size()<oldSize){
std::cout<<"Removed "<<(oldSize-codes.size())<<" unused codes\n"; UF_MSG_DEBUG("Removed {} unused codes", (oldSize-codes.size()));
} }
} }
@ -399,7 +387,7 @@ void VectorQuantizer<N>::compress(const uf::stl::vector<Vec<N>>& vectors,int num
uf::stl::unordered_map<Vec<N>,int> rle; uf::stl::unordered_map<Vec<N>,int> rle;
for(const auto& v:vectors) rle[v]++; for(const auto& v:vectors) rle[v]++;
std::cout<<"RLE result: "<<vectors.size()<<" => "<<rle.size()<<"\n"; // UF_MSG_DEBUG("RLE result: {} => {}", vectors.size(), rle.size());
codes.clear(); codes.clear();
codes.resize(1); codes.resize(1);
@ -414,11 +402,11 @@ void VectorQuantizer<N>::compress(const uf::stl::vector<Vec<N>>& vectors,int num
removeUnusedCodes(); removeUnusedCodes();
if(codes.size()==before){ if(codes.size()==before){
std::cout<<"No further improvement by splitting\n"; //UF_MSG_DEBUG("No further improvement by splitting");
break; break;
} }
splits++; splits++;
std::cout<<"Split "<<splits<<" done. Codes: "<<codes.size()<<"\n"; //UF_MSG_DEBUG("Split {} done. COdes: {}", splits, codes.size());
} }
while((int)codes.size()<numCodes){ while((int)codes.size()<numCodes){
@ -431,23 +419,23 @@ void VectorQuantizer<N>::compress(const uf::stl::vector<Vec<N>>& vectors,int num
codes[idx].maxDistance=0; codes[idx].maxDistance=0;
} }
if(codes.size()==before){ if(codes.size()==before){
std::cout<<"No further improvement by repairing\n"; //UF_MSG_DEBUG("No further improvement by repairing");
break; break;
} }
place(rle); place(rle); place(rle); place(rle); place(rle); place(rle);
removeUnusedCodes(); removeUnusedCodes();
repairs++; repairs++;
std::cout<<"Repair "<<repairs<<" done. Codes: "<<codes.size()<<"\n"; //UF_MSG_DEBUG("Repair {} done. Codes: {}", repairs, codes.size());
} }
auto ms=std::chrono::duration_cast<std::chrono::milliseconds>(clock::now()-start).count(); auto ms=std::chrono::duration_cast<std::chrono::milliseconds>(clock::now()-start).count();
std::cout<<"Compression completed in "<<ms<<" ms\n"; //UF_MSG_DEBUG("Compression completed in {} ms", ms);
} }
template<uint N> template<uint N>
bool VectorQuantizer<N>::writeReportToFile(const uf::stl::string& fname){ bool VectorQuantizer<N>::writeReportToFile(const uf::stl::string& fname){
std::ofstream f(fname); std::ofstream f(fname);
if(!f.is_open()){ if(!f.is_open()){
std::cerr<<"Failed to open "<<fname<<"\n"; UF_MSG_ERROR("Failed to open: {}", fname);
return false; return false;
} }
for(int i=0;i<(int)codes.size();i++){ for(int i=0;i<(int)codes.size();i++){

View File

@ -109,7 +109,7 @@ uint16_t to16BPP(const RGBA& argb, int pixelFormat) {
case PIXELFORMAT_BUMPMAP: case PIXELFORMAT_BUMPMAP:
return toSpherical(argb); return toSpherical(argb);
default: default:
std::cerr << "Unsupported format " << pixelFormat << " in to16BPP\n"; UF_MSG_ERROR("Unsupported format {} in to16BPP", pixelFormat);
return 0xFFFF; return 0xFFFF;
} }
} }

View File

@ -8,10 +8,10 @@
#include <texconv/vqtools.h> #include <texconv/vqtools.h>
#include <texconv/common.h> #include <texconv/common.h>
static void convertAndWriteTexel(std::ostream& stream, const RGBA& texel, int pixelFormat, bool twiddled); void convertAndWriteTexel(std::ostream& stream, const RGBA& texel, int pixelFormat, bool twiddled);
static void writeStrideData(std::ostream& stream, const Image& img, int pixelFormat); void writeStrideData(std::ostream& stream, const Image& img, int pixelFormat);
static void writeUncompressedData(std::ostream& stream, const ImageContainer& images, int pixelFormat); void writeUncompressedData(std::ostream& stream, const ImageContainer& images, int pixelFormat);
static void writeCompressedData(std::ostream& stream, const ImageContainer& images, int pixelFormat); void writeCompressedData(std::ostream& stream, const ImageContainer& images, int pixelFormat);
void convert16BPP(std::ostream& stream, const ImageContainer& images, int textureType) { void convert16BPP(std::ostream& stream, const ImageContainer& images, int textureType) {
const int pixelFormat = (textureType >> PIXELFORMAT_SHIFT) & PIXELFORMAT_MASK; const int pixelFormat = (textureType >> PIXELFORMAT_SHIFT) & PIXELFORMAT_MASK;
@ -25,7 +25,7 @@ void convert16BPP(std::ostream& stream, const ImageContainer& images, int textur
} }
} }
static void convertAndWriteTexel(std::ostream& stream, const RGBA& texel, int pixelFormat, bool twiddled) { void convertAndWriteTexel(std::ostream& stream, const RGBA& texel, int pixelFormat, bool twiddled) {
if (pixelFormat == PIXELFORMAT_YUV422) { if (pixelFormat == PIXELFORMAT_YUV422) {
static int index = 0; static int index = 0;
static RGBA savedTexel[3]; static RGBA savedTexel[3];
@ -55,225 +55,296 @@ static void convertAndWriteTexel(std::ostream& stream, const RGBA& texel, int pi
} }
} }
void writeStrideData(std::ostream& stream, const Image& img, int pixelFormat) {
for (int y=0; y<img.height(); y++)
for (int x=0; x<img.width(); x++)
convertAndWriteTexel(stream, img.pixel(x, y), pixelFormat, false);
static void writeStrideData(std::ostream& stream, const Image& img, int pixelFormat) {
for (int y=0; y<img.height(); y++) {
for (int x=0; x<img.width(); x++) {
convertAndWriteTexel(stream, img.pixel(x,y), pixelFormat, false);
}
}
} }
void writeUncompressedData(std::ostream& stream, const ImageContainer& images, int pixelFormat) {
// Mipmap offset
static void writeUncompressedData(std::ostream& stream, const ImageContainer& images, int pixelFormat) {
if (images.hasMipmaps()) { if (images.hasMipmaps()) {
writeZeroes(stream, MIPMAP_OFFSET_16BPP); writeZeroes(stream, MIPMAP_OFFSET_16BPP);
} }
// Texture data, from smallest to largest mipmap
for (int i=0; i<images.imageCount(); i++) { for (int i=0; i<images.imageCount(); i++) {
const Image& img = images.getByIndex(i); const Image& img = images.getByIndex(i);
// The 1x1 mipmap level is a bit special for YUV textures. Since there's only
if (img.width()==1 && img.height()==1 && pixelFormat == PIXELFORMAT_YUV422) { // one pixel, it can't be saved as YUV422, so save it as RGB565 instead.
convertAndWriteTexel(stream, img.pixel(0,0), PIXELFORMAT_RGB565, true); if (img.width() == 1 && img.height() == 1 && pixelFormat == PIXELFORMAT_YUV422) {
convertAndWriteTexel(stream, img.pixel(0, 0), PIXELFORMAT_RGB565, true);
continue; continue;
} }
Twiddler twiddler(img.width(), img.height()); const Twiddler twiddler(img.width(), img.height());
int pixels = img.width() * img.height(); const int pixels = img.width() * img.height();
// Write all texels for this mipmap level in twiddled order
for (int j=0;j<pixels;j++) { for (int j=0; j<pixels; j++) {
int index = twiddler.index(j); const int index = twiddler.index(j);
int x = index % img.width(); const int x = index % img.width();
int y = index / img.width(); const int y = index / img.width();
convertAndWriteTexel(stream, img.pixel(x,y), pixelFormat, true); convertAndWriteTexel(stream, img.pixel(x, y), pixelFormat, true);
} }
} }
} }
// Packs a quad (2x2 16BPP texels) into a single uint64_t
uint64_t packQuad(RGBA topLeft, RGBA topRight, RGBA bottomLeft, RGBA bottomRight, int pixelFormat) {
uint64_t a, b, c, d;
static uint64_t packQuad(const RGBA& tl, const RGBA& tr,
const RGBA& bl, const RGBA& br, int pixelFormat) {
uint64_t a,b,c,d;
if (pixelFormat == PIXELFORMAT_YUV422) { if (pixelFormat == PIXELFORMAT_YUV422) {
uint16_t yuv[4]; uint16_t yuv[4];
RGBtoYUV422(tl,tr,yuv[0],yuv[1]); RGBtoYUV422(topLeft, topRight, yuv[0], yuv[1]);
RGBtoYUV422(bl,br,yuv[2],yuv[3]); RGBtoYUV422(bottomLeft, bottomRight, yuv[2], yuv[3]);
a=yuv[0]; b=yuv[1]; c=yuv[2]; d=yuv[3]; a = yuv[0];
b = yuv[1];
c = yuv[2];
d = yuv[3];
} else { } else {
a=to16BPP(tl,pixelFormat); a = to16BPP(topLeft, pixelFormat);
b=to16BPP(tr,pixelFormat); b = to16BPP(topRight, pixelFormat);
c=to16BPP(bl,pixelFormat); c = to16BPP(bottomLeft, pixelFormat);
d=to16BPP(br,pixelFormat); d = to16BPP(bottomRight, pixelFormat);
} }
return (a<<48)|(b<<32)|(c<<16)|d; return (a << 48) | (b << 32) | (c << 16) | d;
} }
static int encodeLossless(const ImageContainer& images, // This function counts how many unique 2x2 16BPP pixel blocks there are in the image.
int pixelFormat, // If there are <= maxCodes, it puts the unique blocks in 'codebook' and 'indexedImages'
uf::stl::vector<Image>& indexedImages, // will contain images that index the 'codebook' vector, resulting in quick "lossless"
uf::stl::vector<uint64_t>& codebook, // compression, if possible.
int maxCodes) { // It will keep counting blocks even if the block count exceeds maxCodes for the sole
uf::stl::unordered_map<uint64_t,int> uniqueQuads; // purpose of reporting it back to the user.
// Returns number of unique 2x2 16BPP pixel blocks in all images.
int encodeLossless(const ImageContainer& images, int pixelFormat, uf::stl::vector<Image>& indexedImages, uf::stl::vector<uint64_t>& codebook, int maxCodes) {
uf::stl::unordered_map<uint64_t, int> uniqueQuads; // Quad <=> index
for (int i=0;i<images.imageCount();i++) { for (int i=0; i<images.imageCount(); i++) {
const Image& img=images.getByIndex(i); const Image& img = images.getByIndex(i);
if (img.width()<MIN_MIPMAP_VQ || img.height()<MIN_MIPMAP_VQ) // Ignore images smaller than this
if (img.width() < MIN_MIPMAP_VQ || img.height() < MIN_MIPMAP_VQ)
continue; continue;
Image indexed(img.width()/2, img.height()/2); Image indexedImage(img.width() / 2, img.height() / 2/*, Image::Format_Indexed8*/);
indexed.allocateIndexed(256); indexedImage.allocateIndexed(256);
for (int y=0;y<img.height();y+=2) { for (int y=0;y<img.height();y+=2) {
for (int x=0;x<img.width();x+=2) { for (int x=0;x<img.width();x+=2) {
uint64_t quad = packQuad(img.pixel(x,y), RGBA tl = img.pixel(x + 0, y + 0);
img.pixel(x+1,y), RGBA tr = img.pixel(x + 1, y + 0);
img.pixel(x,y+1), RGBA bl = img.pixel(x + 0, y + 1);
img.pixel(x+1,y+1), RGBA br = img.pixel(x + 1, y + 1);
pixelFormat); uint64_t quad = packQuad(tl, tr, bl, br, pixelFormat);
if (uniqueQuads.find(quad)==uniqueQuads.end())
uniqueQuads[quad]=(int)uniqueQuads.size();
if ((int)uniqueQuads.size()<=maxCodes) if ( uniqueQuads.find(quad) == uniqueQuads.end() )
indexed.setIndexedPixel(x/2,y/2,uniqueQuads[quad]); uniqueQuads[quad] = (int) uniqueQuads.size();
if ( (int) uniqueQuads.size() <= maxCodes )
indexedImage.setIndexedPixel( x/2, y/2, uniqueQuads[quad] );
} }
} }
if ((int)uniqueQuads.size()<=maxCodes) // Only add the image if we haven't hit the code limit
indexedImages.push_back(indexed); if (uniqueQuads.size() <= maxCodes) {
indexedImages.push_back(indexedImage);
}
} }
if ((int)uniqueQuads.size()<=maxCodes) { if (uniqueQuads.size() <= maxCodes) {
// This texture can be losslessly compressed.
// Copy the unique quads over to the codebook.
// indexedImages is already done.
codebook.resize(uniqueQuads.size()); codebook.resize(uniqueQuads.size());
for (auto& kv:uniqueQuads) codebook[kv.second]=kv.first; for ( auto& kv : uniqueQuads ) codebook[kv.second] = kv.first;
} else { } else {
// This texture needs lossy compression
indexedImages.clear(); indexedImages.clear();
} }
return (int)uniqueQuads.size(); return uniqueQuads.size();
} }
// Divides the image into 2x2 pixel blocks and stores them as 12-dimensional
// vectors, (R, G, B) * 4.
void vectorizeRGB(const ImageContainer& images, uf::stl::vector<Vec<12>>& vectors) {
for (int i=0; i<images.imageCount(); i++) {
const Image& img = images.getByIndex(i);
// Ignore images smaller than this
if (img.width() < MIN_MIPMAP_VQ || img.height() < MIN_MIPMAP_VQ)
continue;
static void writeCompressedData(std::ostream& stream, const ImageContainer& images, int pixelFormat) { for (int y=0; y<img.height(); y+=2) {
for (int x=0; x<img.width(); x+=2) {
Vec<12> vec;
uint hash = 0;
int offset = 0;
for (int yy=y; yy<(y+2); yy++) {
for (int xx=x; xx<(x+2); xx++) {
RGBA pixel = img.pixel(xx, yy);
rgb2vec(packColor(pixel), vec, offset);
hash = combineHash(pixel, hash);
offset += 3;
}
}
vec.setHash(hash);
vectors.push_back(vec);
}
}
}
}
// Divides the image into 2x2 pixel blocks and stores them as 16-dimensional
// vectors, (A, R, G, B) * 4.
static void vectorizeARGB(const ImageContainer& images, uf::stl::vector<Vec<16>>& vectors) {
for (int i=0; i<images.imageCount(); i++) {
const Image& img = images.getByIndex(i);
// Ignore images smaller than this
if (img.width() < MIN_MIPMAP_VQ || img.height() < MIN_MIPMAP_VQ)
continue;
for (int y=0; y<img.height(); y+=2) {
for (int x=0; x<img.width(); x+=2) {
Vec<16> vec;
uint hash = 0;
int offset = 0;
for (int yy=y; yy<(y+2); yy++) {
for (int xx=x; xx<(x+2); xx++) {
RGBA pixel = img.pixel(xx, yy);
argb2vec(packColor(pixel), vec, offset);
hash = combineHash(pixel, hash);
offset += 4;
}
}
vec.setHash(hash);
vectors.push_back(vec);
}
}
}
}
static void devectorizeRGB(const ImageContainer& srcImages, const uf::stl::vector<Vec<12>>& vectors, const VectorQuantizer<12>& vq, int pixelFormat, uf::stl::vector<Image>& indexedImages, uf::stl::vector<uint64_t>& codebook) {
int vindex = 0;
for (int i=0; i<srcImages.imageCount(); i++) {
const auto& srcImage = srcImages.getByIndex(i);
if (srcImage.width() == 1 || srcImage.height() == 1)
continue;
Image img(srcImage.width()/2, srcImage.height()/2/*, Image::Format_Indexed8*/);
img.allocateIndexed(256);
for (int y=0; y<img.height(); y++) {
for (int x=0; x<img.width(); x++) {
const Vec<12>& vec = vectors[vindex];
int codeIndex = vq.findClosest(vec);
img.setIndexedPixel(x, y, codeIndex);
vindex++;
}
}
indexedImages.push_back(img);
}
for (int i=0; i<vq.codeCount(); i++) {
const Vec<12>& vec = vq.codeVector(i);
RGBA tl = {vec[0], vec[1], vec[2]};
RGBA tr = {vec[3], vec[4], vec[5]};
RGBA bl = {vec[6], vec[7], vec[8]};
RGBA br = {vec[9], vec[10], vec[11]};
uint64_t quad = packQuad(tl, tr, bl, br, pixelFormat);
codebook.push_back(quad);
}
}
static void devectorizeARGB(const ImageContainer& srcImages, const uf::stl::vector<Vec<16>>& vectors, const VectorQuantizer<16>& vq, int format, uf::stl::vector<Image>& indexedImages, uf::stl::vector<uint64_t>& codebook) {
int vindex = 0;
for (int i=0; i<srcImages.imageCount(); i++) {
const auto& srcImage = srcImages.getByIndex(i);
if (srcImage.width() == 1 || srcImage.height() == 1)
continue;
Image img(srcImage.width()/2, srcImage.height()/2/*, Image::Format_Indexed8*/);
img.allocateIndexed(256);
for (int y=0; y<img.height(); y++) {
for (int x=0; x<img.width(); x++) {
const Vec<16>& vec = vectors[vindex];
int codeIndex = vq.findClosest(vec);
img.setIndexedPixel(x, y, codeIndex);
vindex++;
}
}
indexedImages.push_back(img);
}
for (int i=0; i<vq.codeCount(); i++) {
const Vec<16>& vec = vq.codeVector(i);
RGBA tl = {vec[1], vec[2], vec[3], vec[0]};
RGBA tr = {vec[5], vec[6], vec[7], vec[4]};
RGBA bl = {vec[9], vec[10], vec[11], vec[8]};
RGBA br = {vec[13], vec[14], vec[15], vec[12]};
uint64_t quad = packQuad(tl, tr, bl, br, format);
codebook.push_back(quad);
}
}
void writeCompressedData(std::ostream& stream, const ImageContainer& images, int pixelFormat) {
uf::stl::vector<Image> indexedImages; uf::stl::vector<Image> indexedImages;
uf::stl::vector<uint64_t> codebook; uf::stl::vector<uint64_t> codebook;
int numQuads = encodeLossless(images, pixelFormat, indexedImages, codebook, 256); const int numQuads = encodeLossless(images, pixelFormat, indexedImages, codebook, 256);
std::cout << "Source images contain " << numQuads << " unique quads\n";
//UF_MSG_DEBUG("Source images contain {} unique quads", numQuads);
if (numQuads > 256) { if (numQuads > 256) {
if ((pixelFormat != PIXELFORMAT_ARGB1555) && (pixelFormat != PIXELFORMAT_ARGB4444)) { if ((pixelFormat != PIXELFORMAT_ARGB1555) && (pixelFormat != PIXELFORMAT_ARGB4444)) {
uf::stl::vector<Vec<12>> vectors; uf::stl::vector<Vec<12>> vectors;
VectorQuantizer<12> vq; VectorQuantizer<12> vq;
vectorizeRGB(images, vectors);
for (int i=0; i<images.imageCount(); i++) { vq.compress(vectors, 256);
const Image& img=images.getByIndex(i); devectorizeRGB(images, vectors, vq, pixelFormat, indexedImages, codebook);
if (img.width()<MIN_MIPMAP_VQ || img.height()<MIN_MIPMAP_VQ) continue;
for (int y=0;y<img.height();y+=2) {
for(int x=0;x<img.width();x+=2) {
Vec<12> vec;
int offset=0;
for(int yy=0;yy<2;yy++){
for(int xx=0;xx<2;xx++){
RGBA px=img.pixel(x+xx,y+yy);
vec[offset+0]=px.r/255.0f;
vec[offset+1]=px.g/255.0f;
vec[offset+2]=px.b/255.0f;
offset+=3;
}
}
vectors.push_back(vec);
}
}
}
vq.compress(vectors,256);
for (int i=0;i<vq.codeCount();i++) {
const Vec<12>& vec=vq.codeVector(i);
RGBA tl{(uint8_t)(vec[0]*255),(uint8_t)(vec[1]*255),(uint8_t)(vec[2]*255),255};
RGBA tr{(uint8_t)(vec[3]*255),(uint8_t)(vec[4]*255),(uint8_t)(vec[5]*255),255};
RGBA bl{(uint8_t)(vec[6]*255),(uint8_t)(vec[7]*255),(uint8_t)(vec[8]*255),255};
RGBA br{(uint8_t)(vec[9]*255),(uint8_t)(vec[10]*255),(uint8_t)(vec[11]*255),255};
codebook.push_back(packQuad(tl,tr,bl,br,pixelFormat));
}
for (int i=0; i<images.imageCount(); i++) {
const Image& src=images.getByIndex(i);
if (src.width()<MIN_MIPMAP_VQ || src.height()<MIN_MIPMAP_VQ) continue;
Image idx(src.width()/2, src.height()/2);
idx.allocateIndexed(256);
for(int y=0;y<src.height();y+=2){
for(int x=0;x<src.width();x+=2){
Vec<12> v;
int off=0;
for(int yy=0;yy<2;yy++)for(int xx=0;xx<2;xx++){
RGBA p=src.pixel(x+xx,y+yy);
v[off+0]=p.r/255.0f;
v[off+1]=p.g/255.0f;
v[off+2]=p.b/255.0f;
off+=3;
}
int codeIdx=vq.findClosest(v);
idx.setIndexedPixel(x/2,y/2,(uint8_t)codeIdx);
}
}
indexedImages.push_back(idx);
}
} else { } else {
uf::stl::vector<Vec<16>> vectors;
std::cerr<<"ARGB VQ compression not yet implemented!\n"; VectorQuantizer<16> vq;
vectorizeARGB(images, vectors);
vq.compress(vectors, 256);
devectorizeARGB(images, vectors, vq, pixelFormat, indexedImages, codebook);
} }
} }
// Build the codebook
uint16_t codes[256*4]; uint16_t codes[256 * 4];
memset(codes,0,sizeof(codes)); memset(codes, 0, 2048);
for (int i=0;i<(int)codebook.size();i++) { for (int i=0; i<codebook.size(); i++) {
uint64_t quad=codebook[i]; const uint64_t& quad = codebook[i];
codes[i*4+0]=(uint16_t)((quad>>48)&0xFFFF); codes[i * 4 + 0] = (uint16_t)((quad >> 48) & 0xFFFF);
codes[i*4+1]=(uint16_t)((quad>>32)&0xFFFF); codes[i * 4 + 1] = (uint16_t)((quad >> 16) & 0xFFFF);
codes[i*4+2]=(uint16_t)((quad>>16)&0xFFFF); codes[i * 4 + 2] = (uint16_t)((quad >> 32) & 0xFFFF);
codes[i*4+3]=(uint16_t)((quad>> 0)&0xFFFF); codes[i * 4 + 3] = (uint16_t)((quad >> 0) & 0xFFFF);
} }
// Write the codebook
for(int i=0;i<1024;i++){ for (int i=0; i<1024; i++)
stream.write((char*)&codes[i],2); stream.write( (char*) &codes[i], 2 );
}
// Write the 1x1 mipmap level
if(images.imageCount()>1) if (images.imageCount() > 1)
writeZeroes(stream,1); writeZeroes(stream, 1);
// Write all mipmap levels
for(const auto& img:indexedImages){ for (int i=0; i<indexedImages.size(); i++) {
Twiddler twiddler(img.width(),img.height()); const Image& img = indexedImages[i];
int pixels=img.width()*img.height(); const Twiddler twiddler(img.width(), img.height());
for(int j=0;j<pixels;j++){ const int pixels = img.width() * img.height();
int idx=twiddler.index(j);
uint8_t val=img.indexedPixelAt(idx%img.width(),idx/img.width()); for (int j=0; j<pixels; j++) {
stream.write((char*)&val,1); const int index = twiddler.index(j);
const int x = index % img.width();
const int y = index / img.width();
uint8_t val = img.indexedPixelAt(x, y);
stream.write( (char*) &val, 1 );
} }
} }
} }

View File

@ -10,45 +10,43 @@
#include <texconv/common.h> #include <texconv/common.h>
static void vectorizeARGB(const ImageContainer& images, uf::stl::vector<Vec<4>>& vectors) { static void vectorizeARGB(const ImageContainer& images, uf::stl::vector<Vec<4>>& vectors) {
for (int i=0;i<images.imageCount();i++) { for ( int i = 0; i < images.imageCount(); i++ ) {
const Image& img=images.getByIndex(i); const Image& img = images.getByIndex(i);
for (int y=0;y<img.height();y++) { for ( int y = 0; y < img.height(); y++ ) {
for (int x=0;x<img.width();x++) { for ( int x=0; x < img.width(); x++ ) {
RGBA px=img.pixel(x,y); RGBA px = img.pixel(x,y);
Vec<4> vec; Vec<4> vec;
vec[0]=px.a/255.f; vec[0] = px.a/255.f;
vec[1]=px.r/255.f; vec[1] = px.r/255.f;
vec[2]=px.g/255.f; vec[2] = px.g/255.f;
vec[3]=px.b/255.f; vec[3] = px.b/255.f;
vectors.push_back(vec); vectors.push_back(vec);
} }
} }
} }
} }
static void devectorizeARGB(const ImageContainer& srcImages, const uf::stl::vector<Vec<4>>& vectors, const VectorQuantizer<4>& vq, uf::stl::vector<Image>& indexedImages, Palette& palette) {
static void devectorizeARGB(const ImageContainer& srcImages, const uf::stl::vector<Vec<4>>& vectors, int vindex = 0;
const VectorQuantizer<4>& vq, uf::stl::vector<Image>& indexedImages, Palette& palette) { for ( int i = 0; i < srcImages.imageCount(); i++ ) {
int vindex=0; const Image& src = srcImages.getByIndex(i);
for (int i=0;i<srcImages.imageCount();i++) { Image dst( src.width(), src.height() );
const Image& src=srcImages.getByIndex(i);
Image dst(src.width(),src.height());
dst.allocateIndexed(256); dst.allocateIndexed(256);
for (int y=0;y<src.height();y++) { for ( int y = 0; y < src.height(); y++ ) {
for (int x=0;x<src.width();x++) { for ( int x = 0; x < src.width(); x++ ) {
const Vec<4>& vec=vectors[vindex++]; const Vec<4>& vec = vectors[vindex++];
int codeIndex=vq.findClosest(vec); int codeIndex = vq.findClosest(vec);
dst.setIndexedPixel(x,y,(uint8_t)codeIndex); dst.setIndexedPixel( x, y, (uint8_t) codeIndex );
} }
} }
indexedImages.push_back(dst); indexedImages.push_back(dst);
} }
for (int i=0;i<vq.codeCount();i++) { for ( int i = 0; i < vq.codeCount(); i++ ) {
const Vec<4>& v=vq.codeVector(i); const Vec<4>& v = vq.codeVector(i);
uint32_t color=(uint8_t)(v[0]*255)<<24 | (uint8_t)(v[1]*255)<<16 | uint32_t color = (uint8_t)( v[0]*255)<<24 | (uint8_t)(v[1]*255)<<16 |
(uint8_t)(v[2]*255)<<8 | (uint8_t)(v[3]*255); (uint8_t)( v[2]*255)<<8 | (uint8_t)(v[3]*255);
palette.insert(color); palette.insert(color);
} }
} }
@ -75,38 +73,34 @@ void writeCompressed8BPPData(std::ostream& stream, const uf::stl::vector<Image>&
* Then, using the reduced images as input, perform vector quantization * Then, using the reduced images as input, perform vector quantization
* with a vector dimension of 32 or 64 (2x4 or 4x4 pixel blocks). * with a vector dimension of 32 or 64 (2x4 or 4x4 pixel blocks).
*/ */
void convertPaletted(std::ostream& stream, const ImageContainer& images, int textureType, const uf::stl::string& palFilename) { void convertPaletted(std::ostream& stream, const ImageContainer& images, int textureType, const uf::stl::string& palFilename) {
auto palette = convertPaletted(stream, images, textureType);
palette.save( palFilename );
}
Palette convertPaletted(std::ostream& stream, const ImageContainer& images, int textureType) {
const int maxColors = isFormat(textureType, PIXELFORMAT_PAL4BPP) ? 16 : 256; const int maxColors = isFormat(textureType, PIXELFORMAT_PAL4BPP) ? 16 : 256;
Palette palette(images); Palette palette(images);
uf::stl::vector<Image> indexedImages; uf::stl::vector<Image> indexedImages;
std::cout<<"Palette contains "<<palette.colorCount()<<" colors\n"; //qDebug("Palette contains %d colors", palette.colorCount());
if (palette.colorCount()>maxColors) {
std::cout<<"Reducing palette to "<<maxColors<<" colors\n"; if (palette.colorCount() > maxColors) {
// The palette has too many colors, so perform a vector quantization to reduce
// the color count down to what we need.
//qDebug("Reducing palette to %d colors", maxColors);
palette.clear(); palette.clear();
VectorQuantizer<4> vq; VectorQuantizer<4> vq;
uf::stl::vector<Vec<4>> vectors; uf::stl::vector<Vec<4>> vectors;
vectorizeARGB(images,vectors); vectorizeARGB(images, vectors);
vq.compress(vectors,maxColors); vq.compress(vectors, maxColors);
devectorizeARGB(images,vectors,vq,indexedImages,palette); devectorizeARGB(images, vectors, vq, indexedImages, palette);
} else { } else {
// Convert the input images to indexed images so we can use the same output code
for (int i=0;i<images.imageCount();i++) { // as the reduced color images.
const Image& img=images.getByIndex(i); convertToIndexedImages(images, palette, indexedImages);
Image idx(img.width(),img.height());
idx.allocateIndexed(maxColors);
for (int y=0;y<img.height();y++)
for (int x=0;x<img.width();x++) {
uint32_t color=(img.pixel(x,y).a<<24)|(img.pixel(x,y).r<<16)|(img.pixel(x,y).g<<8)|img.pixel(x,y).b;
idx.setIndexedPixel(x,y,(uint8_t)palette.indexOf(color));
}
indexedImages.push_back(idx);
}
} }
palette.save(palFilename); // Write data
if (textureType & FLAG_COMPRESSED) { if (textureType & FLAG_COMPRESSED) {
if (isFormat(textureType, PIXELFORMAT_PAL4BPP)) if (isFormat(textureType, PIXELFORMAT_PAL4BPP))
writeCompressed4BPPData(stream, indexedImages, palette); writeCompressed4BPPData(stream, indexedImages, palette);
@ -118,106 +112,117 @@ void convertPaletted(std::ostream& stream, const ImageContainer& images, int tex
if (isFormat(textureType, PIXELFORMAT_PAL8BPP)) if (isFormat(textureType, PIXELFORMAT_PAL8BPP))
writeUncompressed8BPPData(stream, indexedImages); writeUncompressed8BPPData(stream, indexedImages);
} }
return palette;
} }
// Converts the src images to indexed images.
// The indexed images are sorted from smallest to largest.
void convertToIndexedImages(const ImageContainer& src, const Palette& pal, uf::stl::vector<Image>& dst) { void convertToIndexedImages(const ImageContainer& src, const Palette& pal, uf::stl::vector<Image>& dst) {
dst.clear();
for (int i=0; i<src.imageCount(); i++) { for (int i=0; i<src.imageCount(); i++) {
const Image& img = src.getByIndex(i); const Image& img = src.getByIndex(i);
Image dstImg(img.width(), img.height()); Image dstImg(img.width(), img.height()/*, Image::Format_ARGB32*/);
dstImg.allocateIndexed(pal.colorCount()); for (int y=0; y<img.height(); y++)
for (int y=0; y<img.height(); y++) {
for (int x=0; x<img.width(); x++) { for (int x=0; x<img.width(); x++) {
RGBA px = img.pixel(x,y); RGBA px = img.pixel(x,y);
uint32_t argb = (uint32_t(px.a)<<24)|(uint32_t(px.r)<<16)|(uint32_t(px.g)<<8)|(uint32_t)px.b; uint32_t argb = packColor(px);
uint8_t index = (uint8_t)pal.indexOf(argb); uint8_t index = (uint8_t) pal.indexOf(argb);
dstImg.setIndexedPixel(x,y,index); dstImg.setIndexedPixel( x, y, index );
} }
}
dst.push_back(dstImg); dst.push_back(dstImg);
} }
} }
void writeUncompressed4BPPData(std::ostream& stream, const uf::stl::vector<Image>& indexedImages) { void writeUncompressed4BPPData(std::ostream& stream, const uf::stl::vector<Image>& indexedImages) {
if (indexedImages.size() > 1) { // Write mipmap offset if necessary
if (indexedImages.size() > 1)
writeZeroes(stream, MIPMAP_OFFSET_4BPP); writeZeroes(stream, MIPMAP_OFFSET_4BPP);
}
for (size_t i=0;i<indexedImages.size();i++) {
const Image& img=indexedImages[i];
// Write all mipmaps from smallest to largest
if (img.width()==1) { for (int i=0; i<indexedImages.size(); i++) {
uint8_t val=img.indexedPixelAt(0,0); const Image& img = indexedImages[i];
stream.write((char*)&val,1);
// Special case. There's only one pixel in the 1x1 mipmap level,
// but it's stored by itself in one byte.
if (img.width() == 1) {
uint8_t val = img.indexedPixelAt(0,0);
stream.write( (char*) &val, 1 );
continue; continue;
} }
Twiddler twiddler(img.width(),img.height()); Twiddler twiddler(img.width(), img.height());
int pixels=img.width()*img.height(); const int pixels = img.width() * img.height();
for (int j=0;j<pixels;j+=2) {
uint8_t vals[2]; // Write all pixels in pairs
for(int k=0;k<2;k++){ // First pixel in the least significant nibble.
int tIdx=twiddler.index(j+k); // Second pixel in the most significant nibble.
int x=tIdx%img.width(); for (int j=0; j<pixels; j+=2) {
int y=tIdx/img.width(); uint8_t palindex[2];
vals[k]=img.indexedPixelAt(x,y)&0xF;
for (int k=0; k<2; k++) {
const int index = twiddler.index(j + k);
const int x = index % img.width();
const int y = index / img.width();
palindex[k] = (uint8_t) img.indexedPixelAt(x, y);
} }
uint8_t packed=(vals[1]<<4)|(vals[0]);
stream.write((char*)&packed,1); uint8_t packed = (((palindex[1] & 0xF) << 4) | (palindex[0] & 0xF));
stream.write( (char*) &packed, 1 );
} }
} }
} }
void writeUncompressed8BPPData(std::ostream& stream, const uf::stl::vector<Image>& indexedImages) { void writeUncompressed8BPPData(std::ostream& stream, const uf::stl::vector<Image>& indexedImages) {
if (indexedImages.size() > 1) { // Write mipmap offset if necessary
if (indexedImages.size() > 1)
writeZeroes(stream, MIPMAP_OFFSET_8BPP); writeZeroes(stream, MIPMAP_OFFSET_8BPP);
}
for (size_t i=0;i<indexedImages.size();i++) { // Write all mipmaps from smallest to largest
const Image& img=indexedImages[i]; for (int i=0; i<indexedImages.size(); i++) {
Twiddler twiddler(img.width(),img.height()); const Image& img = indexedImages[i];
int pixels=img.width()*img.height();
for (int j=0;j<pixels;j++) { Twiddler twiddler(img.width(), img.height());
int tIdx=twiddler.index(j); const int pixels = img.width() * img.height();
int x=tIdx%img.width();
int y=tIdx/img.width(); for (int j=0; j<pixels; j++) {
uint8_t val=img.indexedPixelAt(x,y); const int index = twiddler.index(j);
stream.write((char*)&val,1); const int x = index % img.width();
const int y = index / img.width();
uint8_t value = img.indexedPixelAt(x, y);
stream.write( (char*) &value, 1 );
} }
} }
} }
#define STORE_FULL 0 #define STORE_FULL 0 // Store the block in a full 32D vector
#define STORE_LEFT 1 #define STORE_LEFT 1 // Store the block in the left half of a 64D vector
#define STORE_RIGHT 2 #define STORE_RIGHT 2 // Store the block in the right half of a 64D vector
template<uint N> template<uint N>
static void grab2x4Block(const Image& img,const Palette& pal,int x,int y,Vec<N>& vec,int storeMethod){ static void grab2x4Block(const Image& img, const Palette& pal, const int x, const int y, Vec<N>& vec, const uint storeMethod) {
static const int indexLUT[3][8]={ static const int indexLUT[3][8] = {
{0,4,8,12,16,20,24,28}, { 0, 4, 8, 12, 16, 20, 24, 28 }, // Full 32D vector
{0,4,16,20,32,36,48,52}, { 0, 4, 16, 20, 32, 36, 48, 52 }, // Left half of 64D vector
{8,12,24,28,40,44,56,60} { 8, 12, 24, 28, 40, 44, 56, 60 } // Right half of 64D vector
}; };
int idx=0;
uint32_t seed=vec.hash(); int index = 0;
for(int yy=y;yy<y+4;yy++){ uint hash = vec.hash();
for(int xx=x;xx<x+2;xx++){
uint32_t color=pal.colorAt(img.indexedPixelAt(xx,yy)); for (int yy=y; yy<(y+4); yy++) {
RGBA c{(uint8_t)((color>>16)&0xFF),(uint8_t)((color>>8)&0xFF),(uint8_t)(color&0xFF),(uint8_t)((color>>24)&0xFF)}; for (int xx=x; xx<(x+2); xx++) {
vec[indexLUT[storeMethod][idx]+0]=c.a/255.f; uint32_t pixel = pal.colorAt(img.indexedPixelAt(xx, yy));
vec[indexLUT[storeMethod][idx]+1]=c.r/255.f; argb2vec(pixel, vec, indexLUT[storeMethod][index]);
vec[indexLUT[storeMethod][idx]+2]=c.g/255.f; RGBA color = unpackColor( pixel );
vec[indexLUT[storeMethod][idx]+3]=c.b/255.f; hash = combineHash(color, hash);
seed=combineHash(c,seed); index++;
idx++;
} }
} }
vec.setHash(seed);
vec.setHash(hash);
} }
static void vectorizePalette(const Palette& pal, uf::stl::vector<Vec<4>>& vectors) { static void vectorizePalette(const Palette& pal, uf::stl::vector<Vec<4>>& vectors) {
@ -241,147 +246,218 @@ static uint8_t findClosest(const uf::stl::vector<Vec<4>>& vectors, const Vec<4>&
return closestIndex; return closestIndex;
} }
void writeCompressed4BPPData(std::ostream& stream,const uf::stl::vector<Image>& indexedImages,const Palette& palette){ void writeCompressed4BPPData(std::ostream& stream, const uf::stl::vector<Image>& indexedImages, const Palette& palette) {
VectorQuantizer<64> vq; VectorQuantizer<64> vq;
uf::stl::vector<Vec<64>> vectors; uf::stl::vector<Vec<64>> vectors;
if(indexedImages.size()>1){ // Vectorize the input images.
Vec<64> vec; vec.zero(); // Each vector represents a pair of 2x4 pixel blocks. For single images, it's
for(size_t i=0;i<indexedImages.size();i++){ // easy since we can just grab a number of 4x4 blocks straight from the source
const Image& img=indexedImages[i]; // image. It's a bit more complicated for mipmapped images though. They're
if(img.width()<MIN_MIPMAP_PALVQ||img.height()<MIN_MIPMAP_PALVQ) continue; // essentially aligned on a nibble boundary so a single vector represents the
int blocks=(img.width()*img.height())/16; // second half of the 4x4 pixel block at twiddledIndex[n] as well as the first
Twiddler twiddler(img.width()/4,img.height()/4); // half of the 4x4 pixel block at twiddledIndex[n+1]. This makes the mipmapped
for(int j=0;j<blocks;j++){ // vectorization code a lot more complex.
int tw=twiddler.index(j); if (indexedImages.size() > 1) {
int x=(tw%(img.width()/4))*4; Vec<64> vec(0);
int y=(tw/(img.width()/4))*4;
if(vectors.empty()){ for (int i=0; i<indexedImages.size(); i++) {
grab2x4Block(img,palette,x,y,vec,STORE_LEFT); const Image& img = indexedImages[i];
// Ignore images smaller than this
if (img.width() < MIN_MIPMAP_PALVQ || img.height() < MIN_MIPMAP_PALVQ)
continue;
const int imgw = img.width();
const int imgh = img.height();
const int blocks = (imgw * imgh) / 16;
const Twiddler twiddler(imgw / 4, imgh / 4);
for (int j=0; j<blocks; j++) {
const int twidx = twiddler.index(j);
const int x = (twidx % (imgw / 4)) * 4;
const int y = (twidx / (imgw / 4)) * 4;
// If this is the first vector we're processing, the first
// half of it will be empty. So instead of leaving it empty
// and potentially mess up the encoding by introducing colors that
// don't exist in the image, we copy the second half of the vector
// to the first half.
if (vectors.empty()) {
grab2x4Block(img, palette, x, y, vec, STORE_LEFT);
} }
grab2x4Block(img,palette,x,y,vec,STORE_RIGHT);
// First half of this block is the second half of the
// vector we're currently creating.
grab2x4Block(img, palette, x, y, vec, STORE_RIGHT);
// This vector is done now, so flush it and remember to
// clear the hash for the next vector.
vectors.push_back(vec); vectors.push_back(vec);
vec.setHash(0); vec.setHash(0);
grab2x4Block(img,palette,x+2,y,vec,STORE_LEFT);
if(i==indexedImages.size()-1 && j==blocks-1){ // Second half of this block is the first half of the next
grab2x4Block(img,palette,x+2,y,vec,STORE_RIGHT); // vector we're creating.
grab2x4Block(img, palette, x + 2, y, vec, STORE_LEFT);
// If this is the last block of the last image, remember to
// fill the current vector with something good and flush it.
if ((i == (indexedImages.size() - 1)) && (j == (blocks - 1))) {
grab2x4Block(img, palette, x + 2, y, vec, STORE_RIGHT);
vectors.push_back(vec); vectors.push_back(vec);
} }
} }
} }
}else{ } else {
const Image& img=indexedImages[0]; // There's only one image, and it's on a byte boundary, so this
int blocks=(img.width()*img.height())/16; // is simple. Twiddle the data here though, since the mipmapped
Twiddler twiddler(img.width()/4,img.height()/4); // vectors need to be twiddled, so the same code can be used to
for(int j=0;j<blocks;j++){ // devectorize this as well as mipmapped stuff.
int tw=twiddler.index(j); const Image& img = indexedImages[0];
int x=(tw%(img.width()/4))*4; const int imgw = img.width();
int y=(tw/(img.width()/4))*4; const int imgh = img.height();
Vec<64> vec; vec.zero(); const int blocks = (imgw * imgh) / 16;
grab2x4Block(img,palette,x,y,vec,STORE_LEFT); const Twiddler twiddler(imgw / 4, imgh / 4);
grab2x4Block(img,palette,x+2,y,vec,STORE_RIGHT);
for (int j=0; j<blocks; j++) {
const int twidx = twiddler.index(j);
const int x = (twidx % (imgw / 4)) * 4;
const int y = (twidx / (imgw / 4)) * 4;
Vec<64> vec(0);
grab2x4Block(img, palette, x + 0, y, vec, STORE_LEFT);
grab2x4Block(img, palette, x + 2, y, vec, STORE_RIGHT);
vectors.push_back(vec); vectors.push_back(vec);
} }
} }
vq.compress(vectors,256); vq.compress(vectors, 256);
// The palette needs to be in a vector format for the next part,
uf::stl::vector<Vec<4>> paletteVecs; // since we need to be able to perform searches in it.
for(int i=0;i<palette.colorCount();i++){ uf::stl::vector<Vec<4>> vectorizedPalette;
Vec<4> v; v[0]=((palette.colorAt(i)>>24)&0xFF)/255.f; vectorizePalette(palette, vectorizedPalette);
v[1]=((palette.colorAt(i)>>16)&0xFF)/255.f;
v[2]=((palette.colorAt(i)>>8)&0xFF)/255.f;
v[3]=((palette.colorAt(i)>>0)&0xFF)/255.f;
paletteVecs.push_back(v);
}
uint8_t codebook[2048]; memset(codebook,0,sizeof(codebook)); // Build the codebook
Twiddler nibbleLUT(4,4); uint8_t codebook[2048];
for(int i=0;i<vq.codeCount();i++){ memset(codebook, 0, 2048);
const Vec<64>& vec=vq.codeVector(i); const Twiddler nibbleLUT(4, 4);
for(int j=0;j<16;j++){ for (int i=0; i<vq.codeCount(); i++) {
Vec<4> col; const Vec<64>& vec = vq.codeVector(i);
int base=nibbleLUT.index(j)*4;
for(int k=0;k<4;k++) col[k]=vec[base+k]; for (int j=0; j<16; j++) {
Vec<4> color;
int closest=0; color.set(0, vec[nibbleLUT.index(j) * 4 + 0]);
float best=Vec<4>::distanceSquared(paletteVecs[0],col); color.set(1, vec[nibbleLUT.index(j) * 4 + 1]);
for(size_t pi=1;pi<paletteVecs.size();pi++){ color.set(2, vec[nibbleLUT.index(j) * 4 + 2]);
float d=Vec<4>::distanceSquared(paletteVecs[pi],col); color.set(3, vec[nibbleLUT.index(j) * 4 + 3]);
if(d<best){best=d;closest=(int)pi;}
} // Search the vectorized palette for the closest index
int byte=j/2, nib=j%2; uint8_t closestIndex = findClosest(vectorizedPalette, color);
if(nib==1) codebook[i*8+byte]|=(closest&0xF)<<4;
else codebook[i*8+byte]|=(closest&0xF); const int byte = j / 2;
const int nibble = j % 2;
if (nibble == 1)
codebook[i*8+byte] |= ((closestIndex & 0xF) << 4);
else
codebook[i*8+byte] |= (closestIndex & 0xF);
} }
} }
stream.write((char*)codebook,2048); // Write the codebook
stream.write( (char*) codebook, 2048 );
// Don't write out a zero for the 1x1 mipmap like we would usually
for(const auto& vec:vectors){ // do for mipmapped VQ textures. The reason for this is that it's
int codeIdx=vq.findClosest(vec); // represented by a single nibble in PAL4BPPVQMM textures. And that
uint8_t c=(uint8_t)codeIdx; // nibble is part of the first index byte, which will be written next.
stream.write((char*)&c,1); //if (indexedImages.size() > 1)
// writeZeroes(stream, 1);
// Write the index data
for (int i=0; i<vectors.size(); i++) {
const Vec<64>& srcvec = vectors.at(i);
const int c = vq.findClosest(srcvec);
stream << (uint8_t)c;
} }
} }
void writeCompressed8BPPData(std::ostream& stream,const uf::stl::vector<Image>& indexedImages,const Palette& palette){ void writeCompressed8BPPData(std::ostream& stream, const uf::stl::vector<Image>& indexedImages, const Palette& palette) {
VectorQuantizer<32> vq; VectorQuantizer<32> vq;
uf::stl::vector<Vec<32>> vectors; uf::stl::vector<Vec<32>> vectors;
for(const auto& img:indexedImages){ // Vectorize the input images.
if(img.width()<MIN_MIPMAP_PALVQ||img.height()<MIN_MIPMAP_PALVQ) continue; // Each vector represents a 2x4 pixel block.
int blocks=(img.width()*img.height())/16; // Grab the data as twiddled, it's simpler than twiddling it
Twiddler twiddler(img.width()/4,img.height()/4); // when we write it to file.
for(int j=0;j<blocks;j++){ for (int i=0; i<indexedImages.size(); i++) {
int tw=twiddler.index(j); const Image& img = indexedImages[i];
int x=(tw%(img.width()/4))*4; int y=(tw/(img.width()/4))*4;
Vec<32> v1; v1.zero(); grab2x4Block(img,palette,x,y,v1,STORE_FULL); vectors.push_back(v1); // Ignore images smaller than this
Vec<32> v2; v2.zero(); grab2x4Block(img,palette,x+2,y,v2,STORE_FULL); vectors.push_back(v2); if (img.width() < MIN_MIPMAP_PALVQ || img.height() < MIN_MIPMAP_PALVQ)
continue;
const int imgw = img.width();
const int imgh = img.height();
const int blocks = (imgw * imgh) / 16;
const Twiddler twiddler(imgw / 4, imgh / 4);
for (int j=0; j<blocks; j++) {
const int twidx = twiddler.index(j);
const int x = (twidx % (imgw / 4)) * 4;
const int y = (twidx / (imgw / 4)) * 4;
Vec<32> vec;
grab2x4Block(img, palette, x + 0, y, vec, STORE_FULL);
vectors.push_back(vec);
grab2x4Block(img, palette, x + 2, y, vec, STORE_FULL);
vectors.push_back(vec);
} }
} }
vq.compress(vectors,256);
vq.compress(vectors, 256);
uf::stl::vector<Vec<4>> paletteVecs;
for(int i=0;i<palette.colorCount();i++){
Vec<4> v; v[0]=((palette.colorAt(i)>>24)&0xFF)/255.f;
v[1]=((palette.colorAt(i)>>16)&0xFF)/255.f;
v[2]=((palette.colorAt(i)>>8)&0xFF)/255.f;
v[3]=((palette.colorAt(i)>>0)&0xFF)/255.f;
paletteVecs.push_back(v);
}
uint8_t codebook[2048]; memset(codebook,0,sizeof(codebook)); // The palette needs to be in a vector format for the next part,
Twiddler nibbleLUT(2,4); // since we need to be able to perform searches in it.
for(int i=0;i<vq.codeCount();i++){ uf::stl::vector<Vec<4>> vectorizedPalette;
const Vec<32>& vec=vq.codeVector(i); vectorizePalette(palette, vectorizedPalette);
for(int j=0;j<8;j++){
Vec<4> col; // Build the codebook
int base=nibbleLUT.index(j)*4; uint8_t codebook[2048];
for(int k=0;k<4;k++) col[k]=vec[base+k]; memset(codebook, 0, 2048);
int closest=0; const Twiddler nibbleLUT(2, 4);
float best=Vec<4>::distanceSquared(paletteVecs[0],col); for (int i=0; i<vq.codeCount(); i++) {
for(size_t pi=1;pi<paletteVecs.size();pi++){ const Vec<32>& vec = vq.codeVector(i);
float d=Vec<4>::distanceSquared(paletteVecs[pi],col);
if(d<best){best=d;closest=(int)pi;} for (int j=0; j<8; j++) {
} Vec<4> color;
codebook[i*8+j]=(uint8_t)closest; color.set(0, vec[nibbleLUT.index(j) * 4 + 0]);
color.set(1, vec[nibbleLUT.index(j) * 4 + 1]);
color.set(2, vec[nibbleLUT.index(j) * 4 + 2]);
color.set(3, vec[nibbleLUT.index(j) * 4 + 3]);
// Search the palette for the closest index
codebook[i * 8 + j] = findClosest(vectorizedPalette, color);
} }
} }
stream.write((char*)codebook,2048);
// Write the codebook
if(indexedImages.size()>1) writeZeroes(stream,1); stream.write( (char*) codebook, 2048 );
// Write the 1x1 mipmap level
for(const auto& vec:vectors){ if (indexedImages.size() > 1)
uint8_t idx=(uint8_t)vq.findClosest(vec); writeZeroes(stream, 1);
stream.write((char*)&idx,1);
// Write the index data
for (int i=0; i<vectors.size(); i++) {
const Vec<32>& srcvec = vectors.at(i);
const int c = vq.findClosest(srcvec);
stream << (uint8_t)c;
} }
} }
#endif #endif

View File

@ -9,48 +9,60 @@
#include <texconv/image.h> #include <texconv/image.h>
Image::Image() : w(0), h(0), indexedMode(false) {} Image::Image() : w(0), h(0), indexedMode(false) {}
Image::Image(int width, int height) : w(width), h(height), indexedMode(false) { Image::Image(int width, int height, const uf::stl::vector<RGBA>& pixels) : w(width), h(height), p(pixels), indexedMode(false) {
pixels.resize(w*h); p.resize(w*h);
} }
bool Image::loadFromBuffer(const uf::stl::vector<RGBA>& pixels, int width, int height) {
w = width;
h = height;
p = pixels;
return true;
}
bool Image::loadFromFile(const uf::stl::string& path ) { bool Image::loadFromFile(const uf::stl::string& path ) {
int channels; int channels;
uint8_t* buffer = stbi_load(path.c_str(), &w, &h, &channels, STBI_rgb_alpha); uint8_t* pixels = stbi_load(path.c_str(), &w, &h, &channels, STBI_rgb_alpha);
if (!buffer) {
if ( !pixels ) {
std::cerr<<"[ERROR] Failed to load image: "<<path<<"\n"; std::cerr<<"[ERROR] Failed to load image: "<<path<<"\n";
return false; return false;
} }
indexedMode=false; indexedMode=false;
pixels.resize(w*h); p.resize(w*h);
std::memcpy(pixels.data(), buffer, w*h*4); std::memcpy(p.data(), pixels, w*h*4);
stbi_image_free(buffer); stbi_image_free(pixels);
return true; return true;
} }
bool Image::saveToFile(const uf::stl::string& path) const { bool Image::saveToFile(const uf::stl::string& path) const {
uf::stl::vector<uint8_t> buffer(w*h*4); uf::stl::vector<uint8_t> pixels(w*h*4);
for(int y=0;y<h;y++) for(int x=0;x<w;x++) { for(int y=0;y<h;y++) for(int x=0;x<w;x++) {
RGBA c=pixel(x,y); RGBA c=pixel(x,y);
int idx=(y*w+x)*4; int idx=(y*w+x)*4;
buffer[idx+0]=c.r; pixels[idx+0]=c.r;
buffer[idx+1]=c.g; pixels[idx+1]=c.g;
buffer[idx+2]=c.b; pixels[idx+2]=c.b;
buffer[idx+3]=c.a; pixels[idx+3]=c.a;
} }
return stbi_write_png(path.c_str(),w,h,4,buffer.data(),w*4)!=0; return stbi_write_png(path.c_str(),w,h,4,pixels.data(),w*4)!=0;
} }
int Image::width() const { return w; } int Image::width() const { return w; }
int Image::height() const { return h; } int Image::height() const { return h; }
const uf::stl::vector<RGBA>& Image::pixels() const { return p; }
RGBA Image::pixel(int x,int y) const { RGBA Image::pixel(int x,int y) const {
return pixels[y*w+x]; return p[y*w+x];
} }
void Image::setPixel(int x,int y, RGBA pixel) { void Image::setPixel(int x,int y, RGBA pixel) {
if (!indexedMode) { if (!indexedMode) {
pixels[y*w+x] = pixel; p[y*w+x] = pixel;
} }
} }
@ -61,7 +73,7 @@ Image Image::scaled(int newW,int newH,bool nearest) const {
for (int x=0;x<newW;x++) { for (int x=0;x<newW;x++) {
int srcX = x * w / newW; int srcX = x * w / newW;
int srcY = y * h / newH; int srcY = y * h / newH;
out.pixels[y*newW+x] = pixel(srcX,srcY); out.p[y*newW+x] = pixel(srcX,srcY);
} }
} }
} else { } else {
@ -90,7 +102,7 @@ Image Image::scaled(int newW,int newH,bool nearest) const {
lerp(c01.g,c11.g,dx), lerp(c01.g,c11.g,dx),
lerp(c01.b,c11.b,dx), lerp(c01.b,c11.b,dx),
lerp(c01.a,c11.a,dx)}; lerp(c01.a,c11.a,dx)};
out.pixels[y*newW+x]=RGBA{ out.p[y*newW+x]=RGBA{
lerp(top.r,bottom.r,dy), lerp(top.r,bottom.r,dy),
lerp(top.g,bottom.g,dy), lerp(top.g,bottom.g,dy),
lerp(top.b,bottom.b,dy), lerp(top.b,bottom.b,dy),

View File

@ -7,32 +7,23 @@
#include <texconv/imagecontainer.h> #include <texconv/imagecontainer.h>
#include <texconv/common.h> #include <texconv/common.h>
bool ImageContainer::load(const uf::stl::vector<uf::stl::string>& filenames, int textureType, int mipmapFilter) { bool ImageContainer::load(const uf::stl::vector<Image>& _images, int textureType, int mipmapFilter) {
bool mipmapped = (textureType & FLAG_MIPMAPPED); bool mipmapped = (textureType & FLAG_MIPMAPPED);
if ((filenames.size() > 1) && !mipmapped) { if ((_images.size() > 1) && !mipmapped) {
std::cerr << "[ERROR] Only one input file may be specified if no mipmap flag is set.\n"; UF_MSG_ERROR("Only one input file may be specified if no mipmap flag is set.");
return false; return false;
} }
for (const auto& filename : filenames) { for (const auto& img : _images) {
Image img;
if (!img.loadFromFile(filename)) {
std::cerr << "[ERROR] Failed to load image: " << filename << "\n";
return false;
}
if (!isValidSize(img.width(), img.height(), textureType)) { if (!isValidSize(img.width(), img.height(), textureType)) {
std::cerr << "[ERROR] Image " << filename UF_MSG_ERROR("Image has invalid texture size {}x{}", img.width(), img.height());
<< " has invalid texture size "
<< img.width() << "x" << img.height() << "\n";
return false; return false;
} }
if (mipmapped && img.width() != img.height()) { if (mipmapped && img.width() != img.height()) {
std::cerr << "[ERROR] Image " << filename UF_MSG_ERROR("Image is not square. Mipmapped textures require square images.");
<< " is not square. Mipmapped textures require square images.\n";
return false; return false;
} }
@ -40,14 +31,14 @@ bool ImageContainer::load(const uf::stl::vector<uf::stl::string>& filenames, int
textureHeight = std::max(textureHeight, img.height()); textureHeight = std::max(textureHeight, img.height());
images[img.width()] = img; images[img.width()] = img;
std::cout << "[INFO] Loaded image " << filename << "\n"; //UF_MSG_DEBUG("[INFO] Loaded image");
} }
if (mipmapped) { if (mipmapped) {
if (mipmapFilter == 0) { if (mipmapFilter == 0) {
std::cout << "[INFO] Using nearest-neighbor filtering for mipmaps\n"; //UF_MSG_DEBUG("[INFO] Using nearest-neighbor filtering for mipmaps");
} else { } else {
std::cout << "[INFO] Using bilinear filtering for mipmaps\n"; //UF_MSG_DEBUG("[INFO] Using bilinear filtering for mipmaps");
} }
@ -56,13 +47,13 @@ bool ImageContainer::load(const uf::stl::vector<uf::stl::string>& filenames, int
Image mipmap = images[size*2].scaled(size, size, Image mipmap = images[size*2].scaled(size, size,
mipmapFilter == 0); mipmapFilter == 0);
images[size] = mipmap; images[size] = mipmap;
std::cout << "[INFO] Generated " << size << "x" << size << " mipmap\n"; //UF_MSG_DEBUG("[INFO] Generated {}x{} mipmap", size, size);
} }
} }
} }
if (textureWidth < TEXTURE_SIZE_MIN || textureHeight < TEXTURE_SIZE_MIN) { if (textureWidth < TEXTURE_SIZE_MIN || textureHeight < TEXTURE_SIZE_MIN) {
std::cerr << "[ERROR] At least one input image must be 8x8 or larger.\n"; UF_MSG_ERROR("At least one input image must be 8x8 or larger.");
return false; return false;
} }
@ -73,6 +64,19 @@ bool ImageContainer::load(const uf::stl::vector<uf::stl::string>& filenames, int
return true; return true;
} }
bool ImageContainer::load(const uf::stl::vector<uf::stl::string>& filenames, int textureType, int mipmapFilter) {
uf::stl::vector<Image> images;
for ( const auto& filename : filenames ) {
Image& img = images.emplace_back();
if (!img.loadFromFile(filename)) {
UF_MSG_ERROR("Failed to load image: {}", filename);
return false;
}
}
return load( images, textureType, mipmapFilter );
}
void ImageContainer::unloadAll() { void ImageContainer::unloadAll() {
textureWidth = 0; textureWidth = 0;

View File

@ -7,6 +7,8 @@
#include <texconv/palette.h> #include <texconv/palette.h>
#include <texconv/imagecontainer.h> #include <texconv/imagecontainer.h>
#include <uf/utils/memory/vector_stream.h>
Palette::Palette(const ImageContainer& images) { Palette::Palette(const ImageContainer& images) {
for (int i=0;i<images.imageCount();i++) { for (int i=0;i<images.imageCount();i++) {
const Image& img=images.getByIndex(i); const Image& img=images.getByIndex(i);
@ -20,7 +22,7 @@ void Palette::insert(uint32_t color) {
if (colorsMap.find(color)==colorsMap.end()) { if (colorsMap.find(color)==colorsMap.end()) {
int idx=(int)colorsVec.size(); int idx=(int)colorsVec.size();
colorsMap[color]=idx; colorsMap[color]=idx;
colorsVec.push_back(color); colorsVec.emplace_back(color);
} }
} }
@ -38,7 +40,7 @@ uint32_t Palette::colorAt(int index) const {
bool Palette::save(const uf::stl::string& filename) const { bool Palette::save(const uf::stl::string& filename) const {
std::ofstream out(filename,std::ios::binary); std::ofstream out(filename,std::ios::binary);
if (!out.is_open()) { if (!out.is_open()) {
std::cerr<<"[ERROR] Failed to open "<<filename<<" for writing\n"; UF_MSG_ERROR("Failed to open {} for writing", filename);
return false; return false;
} }
@ -58,13 +60,13 @@ bool Palette::save(const uf::stl::string& filename) const {
bool Palette::load(const uf::stl::string& filename) { bool Palette::load(const uf::stl::string& filename) {
std::ifstream in(filename,std::ios::binary); std::ifstream in(filename,std::ios::binary);
if (!in.is_open()) { if (!in.is_open()) {
std::cerr<<"[ERROR] Failed to open "<<filename<<" for reading\n"; UF_MSG_ERROR("Failed to open {} for reading", filename);
return false; return false;
} }
char magic[4]; char magic[4];
in.read(magic,4); in.read(magic,4);
if (memcmp(magic,PALETTE_MAGIC,4)!=0) { if (memcmp(magic,PALETTE_MAGIC,4)!=0) {
std::cerr<<"[ERROR] "<<filename<<" is not a valid palette file\n"; UF_MSG_ERROR("{} is not a valid palette file", filename);
return false; return false;
} }
int32_t numColors=0; int32_t numColors=0;
@ -78,4 +80,19 @@ bool Palette::load(const uf::stl::string& filename) {
} }
return true; return true;
} }
uf::stl::vector<uint8_t> Palette::encode() const {
uf::stl::vector<uint8_t> data;
uf::stl::vector_stream out(data);
out.write(TEXTURE_MAGIC,4);
int32_t n = colorCount();
out.write((char*)&n,sizeof(int32_t));
for (uint32_t c:colorsVec)
out.write((char*)&c,sizeof(uint32_t));
return data;
}
#endif #endif

View File

@ -84,14 +84,17 @@ bool generatePreview(const uf::stl::string& texFile,
bool genUsage=!codeUsageFile.empty(); bool genUsage=!codeUsageFile.empty();
std::ifstream in(texFile,std::ios::binary); std::ifstream in(texFile,std::ios::binary);
if(!in.is_open()) {std::cerr<<"[ERROR] Cannot open "<<texFile<<"\n";return false;} if(!in.is_open()) {
UF_MSG_ERROR("Cannot open {}", texFile);
return false;
}
in.read(magic,4); in.read(magic,4);
in.read((char*)&width,2); in.read((char*)&width,2);
in.read((char*)&height,2); in.read((char*)&height,2);
in.read((char*)&textureType,4); in.read((char*)&textureType,4);
in.read((char*)&textureSize,4); in.read((char*)&textureSize,4);
if(std::memcmp(magic,TEXTURE_MAGIC,4)!=0) { if(std::memcmp(magic,TEXTURE_MAGIC,4)!=0) {
std::cerr<<"Bad texture magic\n"; return false; UF_MSG_ERROR("Bad texture magic");
} }
uf::stl::vector<uint8_t> data(textureSize); uf::stl::vector<uint8_t> data(textureSize);
in.read((char*)data.data(),textureSize); in.read((char*)data.data(),textureSize);

View File

@ -5,23 +5,46 @@
#include <uf/utils/memory/string.h> #include <uf/utils/memory/string.h>
#include <uf/utils/memory/vector.h> #include <uf/utils/memory/vector.h>
#include <uf/utils/image/image.h>
namespace pod {
struct TextureOptions {
uf::stl::string format = "ARGB1555";
bool mipmap = false;
bool compress = false;
bool stride = false;
bool nearest = false;
bool bilinear = false;
bool verbose = false;
uf::stl::string input;
uf::stl::string output;
uf::stl::string previewFile;
uf::stl::string codeUsageFile;
};
// in case palette data needs to be saved
struct Dtex {
uf::stl::vector<uint8_t> imageData;
uf::stl::vector<uint8_t> paletteData;
};
}
namespace ext { namespace ext {
namespace texconv { namespace texconv {
struct TextureOptions { // reads from memory
uf::stl::vector<uf::stl::string> inputs; pod::Dtex UF_API convert( const uf::Image& image, const uf::stl::string& format = "auto" );
uf::stl::string output; // reads from disk
uf::stl::string format; inline pod::Dtex UF_API convert( const uf::stl::string& input, const uf::stl::string& format = "auto" ) {
bool mipmap = false; uf::Image image;
bool compress = false; image.open( input );
bool stride = false; return convert( image, format );
bool nearest = false; }
bool bilinear = false; // CLI-like
bool verbose = false; bool UF_API convert( const pod::TextureOptions& opts );
uf::stl::string previewFile; // dumps to disk
uf::stl::string codeUsageFile; bool UF_API save( const pod::Dtex&, const uf::stl::string&, bool = false );
};
bool UF_API convertTexture( const TextureOptions& opts );
} }
} }

View File

@ -0,0 +1,35 @@
#pragma once
#include <uf/config.h>
#include <streambuf>
#include "vector.h"
// cringe......
namespace uf {
namespace stl {
class vector_streambuf : public std::streambuf {
uf::stl::vector<uint8_t>& buffer;
public:
explicit vector_streambuf( uf::stl::vector<uint8_t>& buf ) : buffer(buf) {}
protected:
int_type overflow(int_type ch) override {
if ( ch != EOF ) {
buffer.emplace_back(ch);
}
return ch;
}
std::streamsize xsputn(const char* s, std::streamsize count) override {
buffer.insert(buffer.end(), s, s + count);
return count;
}
};
class vector_stream : public std::ostream {
vector_streambuf buf;
public:
explicit vector_stream( uf::stl::vector<uint8_t>& vec) : std::ostream(&buf), buf(vec) {}
};
}
}

View File

@ -11,6 +11,7 @@
#include <uf/engine/asset/asset.h> #include <uf/engine/asset/asset.h>
#include <uf/utils/graphic/graphic.h> #include <uf/utils/graphic/graphic.h>
#include <uf/ext/xatlas/xatlas.h> #include <uf/ext/xatlas/xatlas.h>
#include <uf/ext/texconv/texconv.h>
#include "../light/behavior.h" #include "../light/behavior.h"
#include "../scene/behavior.h" #include "../scene/behavior.h"
@ -174,6 +175,13 @@ SAVE: {
uf::stl::string filename = uf::string::replace( metadata.output, "%i", std::to_string(i) ); uf::stl::string filename = uf::string::replace( metadata.output, "%i", std::to_string(i) );
bool status = image.save(filename); bool status = image.save(filename);
UF_MSG_DEBUG("Writing to {}: {}", filename, status); UF_MSG_DEBUG("Writing to {}: {}", filename, status);
// export DC's .dtex
#if UF_USE_DC_TEXCONV
auto converted = image.scale( {128, 128}, true );
auto dtex = ext::texconv::convert( converted, "ARGB4444" );
ext::texconv::save( dtex, uf::string::replace( filename, ".png", "" ), false );
#endif
}); });
} }
uf::thread::execute( tasks ); uf::thread::execute( tasks );

View File

@ -8,6 +8,7 @@
#include <uf/utils/camera/camera.h> #include <uf/utils/camera/camera.h>
#include <uf/utils/math/physics.h> #include <uf/utils/math/physics.h>
#include <uf/ext/xatlas/xatlas.h> #include <uf/ext/xatlas/xatlas.h>
#include <uf/ext/texconv/texconv.h>
#if !UF_ENV_DREAMCAST #if !UF_ENV_DREAMCAST
namespace { namespace {
@ -378,8 +379,16 @@ uf::stl::string uf::graph::save( const pod::Graph& graph, const uf::stl::string&
auto& name = graph.images[i]; auto& name = graph.images[i];
auto& image = /*graph.storage*/storage.images.map.at(name); auto& image = /*graph.storage*/storage.images.map.at(name);
uf::stl::string f = "image."+std::to_string(i)+".png"; uf::stl::string f = "image."+std::to_string(i)+".png";
image.save(directory + "/" + f); image.save(directory + "/" + f);
// export DC's .dtex
#if UF_USE_DC_TEXCONV
// to-do: properly scale per my script
auto converted = image.scale( {32, 32}, true );
auto dtex = ext::texconv::convert( converted );
ext::texconv::save( dtex, directory + "/image."+std::to_string(i) );
#endif
uf::Serializer json; uf::Serializer json;
json["name"] = name; json["name"] = name;
json["filename"] = f; json["filename"] = f;

View File

@ -8,11 +8,14 @@
#include <algorithm> #include <algorithm>
#include <texconv/common.h> #include <texconv/common.h>
#include <texconv/palette.h>
#include <texconv/imagecontainer.h> #include <texconv/imagecontainer.h>
#include <uf/ext/texconv/texconv.h> #include <uf/ext/texconv/texconv.h>
bool ext::texconv::convertTexture( const ext::texconv::TextureOptions& opts ) { #include <uf/utils/memory/vector_stream.h>
namespace {
uf::stl::unordered_map<uf::stl::string,int> formats = { uf::stl::unordered_map<uf::stl::string,int> formats = {
{"ARGB1555",PIXELFORMAT_ARGB1555}, {"ARGB1555",PIXELFORMAT_ARGB1555},
{"RGB565", PIXELFORMAT_RGB565}, {"RGB565", PIXELFORMAT_RGB565},
@ -23,7 +26,129 @@ bool ext::texconv::convertTexture( const ext::texconv::TextureOptions& opts )
{"PAL8BPP", PIXELFORMAT_PAL8BPP} {"PAL8BPP", PIXELFORMAT_PAL8BPP}
}; };
if ( opts.inputs.empty() ) { Image convert(const uf::Image& image) {
auto size = image.getDimensions();
auto& pixels = image.getPixels();
uf::stl::vector<RGBA> buffer(size.x * size.y);
for ( auto i = 0; i < buffer.size(); ++i ) {
buffer[i] = RGBA{
pixels[i * 4 + 0],
pixels[i * 4 + 1],
pixels[i * 4 + 2],
pixels[i * 4 + 3]
};
}
return Image{ size.x, size.y, std::move(buffer) };
}
uf::Image convert(const Image& image) {
auto width = image.width();
auto height = image.height();
auto& pixels = image.pixels();
uf::Image::container_t buffer;
buffer.reserve(width * height * 4);
for (auto& pixel : pixels) {
buffer.emplace_back(pixel.r);
buffer.emplace_back(pixel.g);
buffer.emplace_back(pixel.b);
buffer.emplace_back(pixel.a);
}
return uf::Image{ std::move(buffer), { width, height } };
}
}
pod::Dtex ext::texconv::convert( const uf::Image& _image, const uf::stl::string& _format ) {
pod::TextureOptions opts; // temp cringe
pod::Dtex dtex;
uf::stl::vector_stream out(dtex.imageData);
auto format = _format;
// automatically deduce best format (for now just pick ARGB4444 if there's transparency)
if ( format == "auto" ) {
format = "RGB565";
auto& pixels = _image.getPixels();
for ( auto i = 0; i < pixels.size(); i += 4 ) {
if ( pixels[i+3] == 0xFF ) continue;
format = "ARGB4444";
break;
}
opts.compress = true; // to mimic my script since flags for compression
}
int pixFmt = -1;
auto it = ::formats.find( format );
if ( it != ::formats.end() ) pixFmt = it->second;
if ( pixFmt == -1 ){
UF_MSG_ERROR( "Unsupported format: {}", format );
return dtex;
}
int textureType = ( pixFmt << PIXELFORMAT_SHIFT );
if ( opts.mipmap ) textureType |= FLAG_MIPMAPPED;
if ( opts.compress ) textureType |= FLAG_COMPRESSED;
if ( opts.stride ) textureType |= ( FLAG_STRIDED | FLAG_NONTWIDDLED );
int filter = ( isPaletted( textureType ) || opts.nearest ) ? 0 : 1;
if ( opts.bilinear ) filter = 1;
Image image = ::convert( _image );
ImageContainer images;
if ( !images.load( {image}, textureType, filter ) ) return dtex;
if ( textureType & FLAG_STRIDED ){
int stride = images.width() / 32;
textureType |= stride;
}
int expectedSize = writeTextureHeader( out, images.width(), images.height(), textureType );
auto before = out.tellp();
if ( isPaletted( textureType ) ){
auto palette = convertPaletted( out, images, textureType );
dtex.paletteData = palette.encode();
} else {
convert16BPP( out, images, textureType );
}
auto after = out.tellp();
int padding = expectedSize - ( (int) after - (int) before );
if ( padding > 0 ){
writeZeroes( out, padding );
}
return dtex;
}
bool UF_API ext::texconv::save( const pod::Dtex& dtex, const uf::stl::string& filename, bool preview ) {
uf::stl::string outputFilename = filename + ".dtex";
uf::stl::string paletteFilename = filename + ".pal";
uf::stl::string previewFilename = filename + ".preview.png";
if ( !dtex.imageData.empty() ) {
uf::io::write( outputFilename, dtex.imageData );
}
if ( !dtex.paletteData.empty() ) {
uf::io::write( outputFilename, dtex.paletteData );
}
if ( preview ) {
generatePreview( outputFilename, paletteFilename, previewFilename, "" );
}
return true;
}
// maintains original main()
bool ext::texconv::convert( const pod::TextureOptions& opts ) {
if ( opts.input.empty() ) {
UF_MSG_ERROR( "No input file(s) specified" ); UF_MSG_ERROR( "No input file(s) specified" );
return false; return false;
} }
@ -33,8 +158,8 @@ bool ext::texconv::convertTexture( const ext::texconv::TextureOptions& opts )
} }
int pixFmt = -1; int pixFmt = -1;
auto it = formats.find( opts.format ); auto it = ::formats.find( opts.format );
if ( it != formats.end() ) pixFmt = it->second; if ( it != ::formats.end() ) pixFmt = it->second;
if ( pixFmt == -1 ){ if ( pixFmt == -1 ){
UF_MSG_ERROR( "Unsupported format: {}", opts.format ); UF_MSG_ERROR( "Unsupported format: {}", opts.format );
return false; return false;
@ -42,19 +167,19 @@ bool ext::texconv::convertTexture( const ext::texconv::TextureOptions& opts )
uf::stl::string palFile = opts.output + ".pal"; uf::stl::string palFile = opts.output + ".pal";
int textureType = ( pixFmt<<PIXELFORMAT_SHIFT ); int textureType = ( pixFmt << PIXELFORMAT_SHIFT );
if ( opts.mipmap ) textureType |= FLAG_MIPMAPPED; if ( opts.mipmap ) textureType |= FLAG_MIPMAPPED;
if ( opts.compress ) textureType |= FLAG_COMPRESSED; if ( opts.compress ) textureType |= FLAG_COMPRESSED;
if ( opts.stride ) textureType |= ( FLAG_STRIDED|FLAG_NONTWIDDLED ); if ( opts.stride ) textureType |= ( FLAG_STRIDED | FLAG_NONTWIDDLED );
int filter = ( isPaletted( textureType ) || opts.nearest ) ? 0 : 1; int filter = ( isPaletted( textureType ) || opts.nearest ) ? 0 : 1;
if ( opts.bilinear ) filter = 1; if ( opts.bilinear ) filter = 1;
ImageContainer images; ImageContainer images;
if ( !images.load( opts.inputs, textureType, filter ) ) return false; if ( !images.load( {opts.input}, textureType, filter ) ) return false;
if ( textureType&FLAG_STRIDED ){ if ( textureType & FLAG_STRIDED ){
int stride = images.width()/32; int stride = images.width() / 32;
textureType |= stride; textureType |= stride;
} }
@ -83,8 +208,8 @@ bool ext::texconv::convertTexture( const ext::texconv::TextureOptions& opts )
UF_MSG_INFO( "Wrote texture {}", opts.output ); UF_MSG_INFO( "Wrote texture {}", opts.output );
// Preview & Code usage // Preview & Code usage
if ( !opts.previewFile.empty() || ( !opts.codeUsageFile.empty() && ( textureType&FLAG_COMPRESSED ) ) ){ if ( !opts.previewFile.empty() || ( !opts.codeUsageFile.empty() && ( textureType & FLAG_COMPRESSED ) ) ){
if ( generatePreview( opts.output,palFile,opts.previewFile,opts.codeUsageFile ) ){ if ( generatePreview( opts.output, palFile, opts.previewFile, opts.codeUsageFile ) ){
if ( !opts.previewFile.empty() ) UF_MSG_INFO( "Saved preview {}", opts.previewFile ); if ( !opts.previewFile.empty() ) UF_MSG_INFO( "Saved preview {}", opts.previewFile );
if ( !opts.codeUsageFile.empty() ) UF_MSG_INFO( "Saved code usage {}", opts.codeUsageFile ); if ( !opts.codeUsageFile.empty() ) UF_MSG_INFO( "Saved code usage {}", opts.codeUsageFile );
} else { } else {