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": {
"tag": "worldspawn",
"player": "info_player_spawn",
"enabled": false, // "auto",
"enabled": "auto",
"radius": 32,
"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);
class ImageContainer;
class Palette;
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);
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 {
public:
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 saveToFile(const uf::stl::string& path) const;
int width() const;
int height() const;
const uf::stl::vector<RGBA>& pixels() const;
RGBA pixel(int x,int y) const;
void setPixel(int x,int y, RGBA pixel);
Image scaled(int newW,int newH,bool nearest) const;
@ -25,8 +28,8 @@ public:
bool isIndexed() const { return indexedMode; }
private:
int w,h;
int w, h;
bool indexedMode;
uf::stl::vector<RGBA> pixels;
uf::stl::vector<RGBA> p;
uf::stl::vector<uint8_t> indexed;
};

View File

@ -5,7 +5,8 @@
class ImageContainer {
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();

View File

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

View File

@ -58,7 +58,6 @@ public:
float length() const;
void setLength(float len);
void normalize();
void print() const;
static float distanceSquared(const Vec<N>& a, const Vec<N>& b);
uint hash() const;
void setHash(uint h) { hashVal = h; }
@ -177,17 +176,6 @@ inline void Vec<N>::normalize() {
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>
float Vec<N>::distanceSquared(const Vec<N>& a, const Vec<N>& b) {
return (a - b).lengthSquared();
@ -332,7 +320,7 @@ void VectorQuantizer<N>::removeUnusedCodes() {
codes.end()
);
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;
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.resize(1);
@ -414,11 +402,11 @@ void VectorQuantizer<N>::compress(const uf::stl::vector<Vec<N>>& vectors,int num
removeUnusedCodes();
if(codes.size()==before){
std::cout<<"No further improvement by splitting\n";
//UF_MSG_DEBUG("No further improvement by splitting");
break;
}
splits++;
std::cout<<"Split "<<splits<<" done. Codes: "<<codes.size()<<"\n";
//UF_MSG_DEBUG("Split {} done. COdes: {}", splits, codes.size());
}
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;
}
if(codes.size()==before){
std::cout<<"No further improvement by repairing\n";
//UF_MSG_DEBUG("No further improvement by repairing");
break;
}
place(rle); place(rle); place(rle);
removeUnusedCodes();
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();
std::cout<<"Compression completed in "<<ms<<" ms\n";
//UF_MSG_DEBUG("Compression completed in {} ms", ms);
}
template<uint N>
bool VectorQuantizer<N>::writeReportToFile(const uf::stl::string& fname){
std::ofstream f(fname);
if(!f.is_open()){
std::cerr<<"Failed to open "<<fname<<"\n";
UF_MSG_ERROR("Failed to open: {}", fname);
return false;
}
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:
return toSpherical(argb);
default:
std::cerr << "Unsupported format " << pixelFormat << " in to16BPP\n";
UF_MSG_ERROR("Unsupported format {} in to16BPP", pixelFormat);
return 0xFFFF;
}
}

View File

@ -8,10 +8,10 @@
#include <texconv/vqtools.h>
#include <texconv/common.h>
static void convertAndWriteTexel(std::ostream& stream, const RGBA& texel, int pixelFormat, bool twiddled);
static void writeStrideData(std::ostream& stream, const Image& img, int pixelFormat);
static void writeUncompressedData(std::ostream& stream, const ImageContainer& images, int pixelFormat);
static void writeCompressedData(std::ostream& stream, const ImageContainer& images, int pixelFormat);
void convertAndWriteTexel(std::ostream& stream, const RGBA& texel, int pixelFormat, bool twiddled);
void writeStrideData(std::ostream& stream, const Image& img, int pixelFormat);
void writeUncompressedData(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) {
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) {
static int index = 0;
static RGBA savedTexel[3];
@ -55,225 +55,296 @@ static void convertAndWriteTexel(std::ostream& stream, const RGBA& texel, int pi
}
}
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 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 writeUncompressedData(std::ostream& stream, const ImageContainer& images, int pixelFormat) {
void writeUncompressedData(std::ostream& stream, const ImageContainer& images, int pixelFormat) {
// Mipmap offset
if (images.hasMipmaps()) {
writeZeroes(stream, MIPMAP_OFFSET_16BPP);
}
// Texture data, from smallest to largest mipmap
for (int i=0; i<images.imageCount(); i++) {
const Image& img = images.getByIndex(i);
if (img.width()==1 && img.height()==1 && pixelFormat == PIXELFORMAT_YUV422) {
convertAndWriteTexel(stream, img.pixel(0,0), PIXELFORMAT_RGB565, true);
// The 1x1 mipmap level is a bit special for YUV textures. Since there's only
// one pixel, it can't be saved as YUV422, so save it as RGB565 instead.
if (img.width() == 1 && img.height() == 1 && pixelFormat == PIXELFORMAT_YUV422) {
convertAndWriteTexel(stream, img.pixel(0, 0), PIXELFORMAT_RGB565, true);
continue;
}
Twiddler twiddler(img.width(), img.height());
int pixels = img.width() * img.height();
const Twiddler twiddler(img.width(), img.height());
const int pixels = img.width() * img.height();
for (int j=0;j<pixels;j++) {
int index = twiddler.index(j);
int x = index % img.width();
int y = index / img.width();
convertAndWriteTexel(stream, img.pixel(x,y), pixelFormat, true);
// Write all texels for this mipmap level in twiddled order
for (int j=0; j<pixels; j++) {
const int index = twiddler.index(j);
const int x = index % img.width();
const int y = index / img.width();
convertAndWriteTexel(stream, img.pixel(x, y), pixelFormat, true);
}
}
}
static uint64_t packQuad(const RGBA& tl, const RGBA& tr,
const RGBA& bl, const RGBA& br, int pixelFormat) {
uint64_t a,b,c,d;
// 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;
if (pixelFormat == PIXELFORMAT_YUV422) {
uint16_t yuv[4];
RGBtoYUV422(tl,tr,yuv[0],yuv[1]);
RGBtoYUV422(bl,br,yuv[2],yuv[3]);
a=yuv[0]; b=yuv[1]; c=yuv[2]; d=yuv[3];
RGBtoYUV422(topLeft, topRight, yuv[0], yuv[1]);
RGBtoYUV422(bottomLeft, bottomRight, yuv[2], yuv[3]);
a = yuv[0];
b = yuv[1];
c = yuv[2];
d = yuv[3];
} else {
a=to16BPP(tl,pixelFormat);
b=to16BPP(tr,pixelFormat);
c=to16BPP(bl,pixelFormat);
d=to16BPP(br,pixelFormat);
a = to16BPP(topLeft, pixelFormat);
b = to16BPP(topRight, pixelFormat);
c = to16BPP(bottomLeft, 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,
int pixelFormat,
uf::stl::vector<Image>& indexedImages,
uf::stl::vector<uint64_t>& codebook,
int maxCodes) {
uf::stl::unordered_map<uint64_t,int> uniqueQuads;
// This function counts how many unique 2x2 16BPP pixel blocks there are in the image.
// If there are <= maxCodes, it puts the unique blocks in 'codebook' and 'indexedImages'
// will contain images that index the 'codebook' vector, resulting in quick "lossless"
// compression, if possible.
// It will keep counting blocks even if the block count exceeds maxCodes for the sole
// 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++) {
const Image& img=images.getByIndex(i);
for (int i=0; i<images.imageCount(); 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;
Image indexed(img.width()/2, img.height()/2);
indexed.allocateIndexed(256);
Image indexedImage(img.width() / 2, img.height() / 2/*, Image::Format_Indexed8*/);
indexedImage.allocateIndexed(256);
for (int y=0;y<img.height();y+=2) {
for (int x=0;x<img.width();x+=2) {
uint64_t quad = packQuad(img.pixel(x,y),
img.pixel(x+1,y),
img.pixel(x,y+1),
img.pixel(x+1,y+1),
pixelFormat);
if (uniqueQuads.find(quad)==uniqueQuads.end())
uniqueQuads[quad]=(int)uniqueQuads.size();
RGBA tl = img.pixel(x + 0, y + 0);
RGBA tr = img.pixel(x + 1, y + 0);
RGBA bl = img.pixel(x + 0, y + 1);
RGBA br = img.pixel(x + 1, y + 1);
uint64_t quad = packQuad(tl, tr, bl, br, pixelFormat);
if ((int)uniqueQuads.size()<=maxCodes)
indexed.setIndexedPixel(x/2,y/2,uniqueQuads[quad]);
if ( uniqueQuads.find(quad) == uniqueQuads.end() )
uniqueQuads[quad] = (int) uniqueQuads.size();
if ( (int) uniqueQuads.size() <= maxCodes )
indexedImage.setIndexedPixel( x/2, y/2, uniqueQuads[quad] );
}
}
if ((int)uniqueQuads.size()<=maxCodes)
indexedImages.push_back(indexed);
// Only add the image if we haven't hit the code limit
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());
for (auto& kv:uniqueQuads) codebook[kv.second]=kv.first;
for ( auto& kv : uniqueQuads ) codebook[kv.second] = kv.first;
} else {
// This texture needs lossy compression
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<uint64_t> codebook;
int numQuads = encodeLossless(images, pixelFormat, indexedImages, codebook, 256);
std::cout << "Source images contain " << numQuads << " unique quads\n";
const int numQuads = encodeLossless(images, pixelFormat, indexedImages, codebook, 256);
//UF_MSG_DEBUG("Source images contain {} unique quads", numQuads);
if (numQuads > 256) {
if ((pixelFormat != PIXELFORMAT_ARGB1555) && (pixelFormat != PIXELFORMAT_ARGB4444)) {
uf::stl::vector<Vec<12>> vectors;
VectorQuantizer<12> vq;
for (int i=0; i<images.imageCount(); i++) {
const Image& img=images.getByIndex(i);
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);
}
vectorizeRGB(images, vectors);
vq.compress(vectors, 256);
devectorizeRGB(images, vectors, vq, pixelFormat, indexedImages, codebook);
} else {
std::cerr<<"ARGB VQ compression not yet implemented!\n";
uf::stl::vector<Vec<16>> vectors;
VectorQuantizer<16> vq;
vectorizeARGB(images, vectors);
vq.compress(vectors, 256);
devectorizeARGB(images, vectors, vq, pixelFormat, indexedImages, codebook);
}
}
uint16_t codes[256*4];
memset(codes,0,sizeof(codes));
for (int i=0;i<(int)codebook.size();i++) {
uint64_t quad=codebook[i];
codes[i*4+0]=(uint16_t)((quad>>48)&0xFFFF);
codes[i*4+1]=(uint16_t)((quad>>32)&0xFFFF);
codes[i*4+2]=(uint16_t)((quad>>16)&0xFFFF);
codes[i*4+3]=(uint16_t)((quad>> 0)&0xFFFF);
// Build the codebook
uint16_t codes[256 * 4];
memset(codes, 0, 2048);
for (int i=0; i<codebook.size(); i++) {
const uint64_t& quad = codebook[i];
codes[i * 4 + 0] = (uint16_t)((quad >> 48) & 0xFFFF);
codes[i * 4 + 1] = (uint16_t)((quad >> 16) & 0xFFFF);
codes[i * 4 + 2] = (uint16_t)((quad >> 32) & 0xFFFF);
codes[i * 4 + 3] = (uint16_t)((quad >> 0) & 0xFFFF);
}
for(int i=0;i<1024;i++){
stream.write((char*)&codes[i],2);
}
// Write the codebook
for (int i=0; i<1024; i++)
stream.write( (char*) &codes[i], 2 );
if(images.imageCount()>1)
writeZeroes(stream,1);
// Write the 1x1 mipmap level
if (images.imageCount() > 1)
writeZeroes(stream, 1);
for(const auto& img:indexedImages){
Twiddler twiddler(img.width(),img.height());
int pixels=img.width()*img.height();
for(int j=0;j<pixels;j++){
int idx=twiddler.index(j);
uint8_t val=img.indexedPixelAt(idx%img.width(),idx/img.width());
stream.write((char*)&val,1);
// Write all mipmap levels
for (int i=0; i<indexedImages.size(); i++) {
const Image& img = indexedImages[i];
const Twiddler twiddler(img.width(), img.height());
const int pixels = img.width() * img.height();
for (int j=0; j<pixels; j++) {
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>
static void vectorizeARGB(const ImageContainer& images, uf::stl::vector<Vec<4>>& vectors) {
for (int i=0;i<images.imageCount();i++) {
const Image& img=images.getByIndex(i);
for (int y=0;y<img.height();y++) {
for (int x=0;x<img.width();x++) {
RGBA px=img.pixel(x,y);
for ( int i = 0; i < images.imageCount(); i++ ) {
const Image& img = images.getByIndex(i);
for ( int y = 0; y < img.height(); y++ ) {
for ( int x=0; x < img.width(); x++ ) {
RGBA px = img.pixel(x,y);
Vec<4> vec;
vec[0]=px.a/255.f;
vec[1]=px.r/255.f;
vec[2]=px.g/255.f;
vec[3]=px.b/255.f;
vec[0] = px.a/255.f;
vec[1] = px.r/255.f;
vec[2] = px.g/255.f;
vec[3] = px.b/255.f;
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) {
int vindex=0;
for (int i=0;i<srcImages.imageCount();i++) {
const Image& src=srcImages.getByIndex(i);
Image dst(src.width(),src.height());
static void devectorizeARGB(const ImageContainer& srcImages, const uf::stl::vector<Vec<4>>& vectors, const VectorQuantizer<4>& vq, uf::stl::vector<Image>& indexedImages, Palette& palette) {
int vindex = 0;
for ( int i = 0; i < srcImages.imageCount(); i++ ) {
const Image& src = srcImages.getByIndex(i);
Image dst( src.width(), src.height() );
dst.allocateIndexed(256);
for (int y=0;y<src.height();y++) {
for (int x=0;x<src.width();x++) {
const Vec<4>& vec=vectors[vindex++];
int codeIndex=vq.findClosest(vec);
dst.setIndexedPixel(x,y,(uint8_t)codeIndex);
for ( int y = 0; y < src.height(); y++ ) {
for ( int x = 0; x < src.width(); x++ ) {
const Vec<4>& vec = vectors[vindex++];
int codeIndex = vq.findClosest(vec);
dst.setIndexedPixel( x, y, (uint8_t) codeIndex );
}
}
indexedImages.push_back(dst);
}
for (int i=0;i<vq.codeCount();i++) {
const Vec<4>& v=vq.codeVector(i);
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);
for ( int i = 0; i < vq.codeCount(); i++ ) {
const Vec<4>& v = vq.codeVector(i);
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);
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
* 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) {
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;
Palette palette(images);
uf::stl::vector<Image> indexedImages;
std::cout<<"Palette contains "<<palette.colorCount()<<" colors\n";
if (palette.colorCount()>maxColors) {
std::cout<<"Reducing palette to "<<maxColors<<" colors\n";
//qDebug("Palette contains %d colors", palette.colorCount());
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();
VectorQuantizer<4> vq;
uf::stl::vector<Vec<4>> vectors;
vectorizeARGB(images,vectors);
vq.compress(vectors,maxColors);
devectorizeARGB(images,vectors,vq,indexedImages,palette);
vectorizeARGB(images, vectors);
vq.compress(vectors, maxColors);
devectorizeARGB(images, vectors, vq, indexedImages, palette);
} else {
for (int i=0;i<images.imageCount();i++) {
const Image& img=images.getByIndex(i);
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);
}
// Convert the input images to indexed images so we can use the same output code
// as the reduced color images.
convertToIndexedImages(images, palette, indexedImages);
}
palette.save(palFilename);
// Write data
if (textureType & FLAG_COMPRESSED) {
if (isFormat(textureType, PIXELFORMAT_PAL4BPP))
writeCompressed4BPPData(stream, indexedImages, palette);
@ -118,106 +112,117 @@ void convertPaletted(std::ostream& stream, const ImageContainer& images, int tex
if (isFormat(textureType, PIXELFORMAT_PAL8BPP))
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) {
dst.clear();
for (int i=0; i<src.imageCount(); i++) {
const Image& img = src.getByIndex(i);
Image dstImg(img.width(), img.height());
dstImg.allocateIndexed(pal.colorCount());
for (int y=0; y<img.height(); y++) {
Image dstImg(img.width(), img.height()/*, Image::Format_ARGB32*/);
for (int y=0; y<img.height(); y++)
for (int x=0; x<img.width(); x++) {
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;
uint8_t index = (uint8_t)pal.indexOf(argb);
dstImg.setIndexedPixel(x,y,index);
uint32_t argb = packColor(px);
uint8_t index = (uint8_t) pal.indexOf(argb);
dstImg.setIndexedPixel( x, y, index );
}
}
dst.push_back(dstImg);
}
}
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);
}
for (size_t i=0;i<indexedImages.size();i++) {
const Image& img=indexedImages[i];
if (img.width()==1) {
uint8_t val=img.indexedPixelAt(0,0);
stream.write((char*)&val,1);
// Write all mipmaps from smallest to largest
for (int i=0; i<indexedImages.size(); i++) {
const Image& img = indexedImages[i];
// 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;
}
Twiddler twiddler(img.width(),img.height());
int pixels=img.width()*img.height();
for (int j=0;j<pixels;j+=2) {
uint8_t vals[2];
for(int k=0;k<2;k++){
int tIdx=twiddler.index(j+k);
int x=tIdx%img.width();
int y=tIdx/img.width();
vals[k]=img.indexedPixelAt(x,y)&0xF;
Twiddler twiddler(img.width(), img.height());
const int pixels = img.width() * img.height();
// Write all pixels in pairs
// First pixel in the least significant nibble.
// Second pixel in the most significant nibble.
for (int j=0; j<pixels; j+=2) {
uint8_t palindex[2];
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) {
if (indexedImages.size() > 1) {
// Write mipmap offset if necessary
if (indexedImages.size() > 1)
writeZeroes(stream, MIPMAP_OFFSET_8BPP);
}
for (size_t i=0;i<indexedImages.size();i++) {
const Image& img=indexedImages[i];
Twiddler twiddler(img.width(),img.height());
int pixels=img.width()*img.height();
for (int j=0;j<pixels;j++) {
int tIdx=twiddler.index(j);
int x=tIdx%img.width();
int y=tIdx/img.width();
uint8_t val=img.indexedPixelAt(x,y);
stream.write((char*)&val,1);
// Write all mipmaps from smallest to largest
for (int i=0; i<indexedImages.size(); i++) {
const Image& img = indexedImages[i];
Twiddler twiddler(img.width(), img.height());
const int pixels = img.width() * img.height();
for (int j=0; j<pixels; j++) {
const int index = twiddler.index(j);
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_LEFT 1
#define STORE_RIGHT 2
#define STORE_FULL 0 // Store the block in a full 32D vector
#define STORE_LEFT 1 // Store the block in the left half of a 64D vector
#define STORE_RIGHT 2 // Store the block in the right half of a 64D vector
template<uint N>
static void grab2x4Block(const Image& img,const Palette& pal,int x,int y,Vec<N>& vec,int storeMethod){
static const int indexLUT[3][8]={
{0,4,8,12,16,20,24,28},
{0,4,16,20,32,36,48,52},
{8,12,24,28,40,44,56,60}
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] = {
{ 0, 4, 8, 12, 16, 20, 24, 28 }, // Full 32D vector
{ 0, 4, 16, 20, 32, 36, 48, 52 }, // Left half of 64D vector
{ 8, 12, 24, 28, 40, 44, 56, 60 } // Right half of 64D vector
};
int idx=0;
uint32_t seed=vec.hash();
for(int yy=y;yy<y+4;yy++){
for(int xx=x;xx<x+2;xx++){
uint32_t color=pal.colorAt(img.indexedPixelAt(xx,yy));
RGBA c{(uint8_t)((color>>16)&0xFF),(uint8_t)((color>>8)&0xFF),(uint8_t)(color&0xFF),(uint8_t)((color>>24)&0xFF)};
vec[indexLUT[storeMethod][idx]+0]=c.a/255.f;
vec[indexLUT[storeMethod][idx]+1]=c.r/255.f;
vec[indexLUT[storeMethod][idx]+2]=c.g/255.f;
vec[indexLUT[storeMethod][idx]+3]=c.b/255.f;
seed=combineHash(c,seed);
idx++;
int index = 0;
uint hash = vec.hash();
for (int yy=y; yy<(y+4); yy++) {
for (int xx=x; xx<(x+2); xx++) {
uint32_t pixel = pal.colorAt(img.indexedPixelAt(xx, yy));
argb2vec(pixel, vec, indexLUT[storeMethod][index]);
RGBA color = unpackColor( pixel );
hash = combineHash(color, hash);
index++;
}
}
vec.setHash(seed);
vec.setHash(hash);
}
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;
}
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;
uf::stl::vector<Vec<64>> vectors;
if(indexedImages.size()>1){
Vec<64> vec; vec.zero();
for(size_t i=0;i<indexedImages.size();i++){
const Image& img=indexedImages[i];
if(img.width()<MIN_MIPMAP_PALVQ||img.height()<MIN_MIPMAP_PALVQ) continue;
int blocks=(img.width()*img.height())/16;
Twiddler twiddler(img.width()/4,img.height()/4);
for(int j=0;j<blocks;j++){
int tw=twiddler.index(j);
int x=(tw%(img.width()/4))*4;
int y=(tw/(img.width()/4))*4;
if(vectors.empty()){
grab2x4Block(img,palette,x,y,vec,STORE_LEFT);
// Vectorize the input images.
// Each vector represents a pair of 2x4 pixel blocks. For single images, it's
// easy since we can just grab a number of 4x4 blocks straight from the source
// image. It's a bit more complicated for mipmapped images though. They're
// essentially aligned on a nibble boundary so a single vector represents the
// second half of the 4x4 pixel block at twiddledIndex[n] as well as the first
// half of the 4x4 pixel block at twiddledIndex[n+1]. This makes the mipmapped
// vectorization code a lot more complex.
if (indexedImages.size() > 1) {
Vec<64> vec(0);
for (int i=0; i<indexedImages.size(); i++) {
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);
vec.setHash(0);
grab2x4Block(img,palette,x+2,y,vec,STORE_LEFT);
if(i==indexedImages.size()-1 && j==blocks-1){
grab2x4Block(img,palette,x+2,y,vec,STORE_RIGHT);
// Second half of this block is the first half of the next
// 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);
}
}
}
}else{
const Image& img=indexedImages[0];
int blocks=(img.width()*img.height())/16;
Twiddler twiddler(img.width()/4,img.height()/4);
for(int j=0;j<blocks;j++){
int tw=twiddler.index(j);
int x=(tw%(img.width()/4))*4;
int y=(tw/(img.width()/4))*4;
Vec<64> vec; vec.zero();
grab2x4Block(img,palette,x,y,vec,STORE_LEFT);
grab2x4Block(img,palette,x+2,y,vec,STORE_RIGHT);
} else {
// There's only one image, and it's on a byte boundary, so this
// is simple. Twiddle the data here though, since the mipmapped
// vectors need to be twiddled, so the same code can be used to
// devectorize this as well as mipmapped stuff.
const Image& img = indexedImages[0];
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<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);
}
}
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);
}
// The palette needs to be in a vector format for the next part,
// since we need to be able to perform searches in it.
uf::stl::vector<Vec<4>> vectorizedPalette;
vectorizePalette(palette, vectorizedPalette);
uint8_t codebook[2048]; memset(codebook,0,sizeof(codebook));
Twiddler nibbleLUT(4,4);
for(int i=0;i<vq.codeCount();i++){
const Vec<64>& vec=vq.codeVector(i);
for(int j=0;j<16;j++){
Vec<4> col;
int base=nibbleLUT.index(j)*4;
for(int k=0;k<4;k++) col[k]=vec[base+k];
int closest=0;
float best=Vec<4>::distanceSquared(paletteVecs[0],col);
for(size_t pi=1;pi<paletteVecs.size();pi++){
float d=Vec<4>::distanceSquared(paletteVecs[pi],col);
if(d<best){best=d;closest=(int)pi;}
}
int byte=j/2, nib=j%2;
if(nib==1) codebook[i*8+byte]|=(closest&0xF)<<4;
else codebook[i*8+byte]|=(closest&0xF);
// Build the codebook
uint8_t codebook[2048];
memset(codebook, 0, 2048);
const Twiddler nibbleLUT(4, 4);
for (int i=0; i<vq.codeCount(); i++) {
const Vec<64>& vec = vq.codeVector(i);
for (int j=0; j<16; j++) {
Vec<4> color;
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 vectorized palette for the closest index
uint8_t closestIndex = findClosest(vectorizedPalette, color);
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 );
for(const auto& vec:vectors){
int codeIdx=vq.findClosest(vec);
uint8_t c=(uint8_t)codeIdx;
stream.write((char*)&c,1);
// Don't write out a zero for the 1x1 mipmap like we would usually
// do for mipmapped VQ textures. The reason for this is that it's
// represented by a single nibble in PAL4BPPVQMM textures. And that
// nibble is part of the first index byte, which will be written next.
//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;
uf::stl::vector<Vec<32>> vectors;
for(const auto& img:indexedImages){
if(img.width()<MIN_MIPMAP_PALVQ||img.height()<MIN_MIPMAP_PALVQ) continue;
int blocks=(img.width()*img.height())/16;
Twiddler twiddler(img.width()/4,img.height()/4);
for(int j=0;j<blocks;j++){
int tw=twiddler.index(j);
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);
Vec<32> v2; v2.zero(); grab2x4Block(img,palette,x+2,y,v2,STORE_FULL); vectors.push_back(v2);
// Vectorize the input images.
// Each vector represents a 2x4 pixel block.
// Grab the data as twiddled, it's simpler than twiddling it
// when we write it to file.
for (int i=0; i<indexedImages.size(); i++) {
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;
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);
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);
}
vq.compress(vectors, 256);
uint8_t codebook[2048]; memset(codebook,0,sizeof(codebook));
Twiddler nibbleLUT(2,4);
for(int i=0;i<vq.codeCount();i++){
const Vec<32>& vec=vq.codeVector(i);
for(int j=0;j<8;j++){
Vec<4> col;
int base=nibbleLUT.index(j)*4;
for(int k=0;k<4;k++) col[k]=vec[base+k];
int closest=0;
float best=Vec<4>::distanceSquared(paletteVecs[0],col);
for(size_t pi=1;pi<paletteVecs.size();pi++){
float d=Vec<4>::distanceSquared(paletteVecs[pi],col);
if(d<best){best=d;closest=(int)pi;}
}
codebook[i*8+j]=(uint8_t)closest;
// The palette needs to be in a vector format for the next part,
// since we need to be able to perform searches in it.
uf::stl::vector<Vec<4>> vectorizedPalette;
vectorizePalette(palette, vectorizedPalette);
// Build the codebook
uint8_t codebook[2048];
memset(codebook, 0, 2048);
const Twiddler nibbleLUT(2, 4);
for (int i=0; i<vq.codeCount(); i++) {
const Vec<32>& vec = vq.codeVector(i);
for (int j=0; j<8; j++) {
Vec<4> color;
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);
if(indexedImages.size()>1) writeZeroes(stream,1);
// Write the codebook
stream.write( (char*) codebook, 2048 );
for(const auto& vec:vectors){
uint8_t idx=(uint8_t)vq.findClosest(vec);
stream.write((char*)&idx,1);
// Write the 1x1 mipmap level
if (indexedImages.size() > 1)
writeZeroes(stream, 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

View File

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

View File

@ -7,32 +7,23 @@
#include <texconv/imagecontainer.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);
if ((filenames.size() > 1) && !mipmapped) {
std::cerr << "[ERROR] Only one input file may be specified if no mipmap flag is set.\n";
if ((_images.size() > 1) && !mipmapped) {
UF_MSG_ERROR("Only one input file may be specified if no mipmap flag is set.");
return false;
}
for (const auto& filename : filenames) {
Image img;
if (!img.loadFromFile(filename)) {
std::cerr << "[ERROR] Failed to load image: " << filename << "\n";
return false;
}
for (const auto& img : _images) {
if (!isValidSize(img.width(), img.height(), textureType)) {
std::cerr << "[ERROR] Image " << filename
<< " has invalid texture size "
<< img.width() << "x" << img.height() << "\n";
UF_MSG_ERROR("Image has invalid texture size {}x{}", img.width(), img.height());
return false;
}
if (mipmapped && img.width() != img.height()) {
std::cerr << "[ERROR] Image " << filename
<< " is not square. Mipmapped textures require square images.\n";
UF_MSG_ERROR("Image is not square. Mipmapped textures require square images.");
return false;
}
@ -40,14 +31,14 @@ bool ImageContainer::load(const uf::stl::vector<uf::stl::string>& filenames, int
textureHeight = std::max(textureHeight, img.height());
images[img.width()] = img;
std::cout << "[INFO] Loaded image " << filename << "\n";
//UF_MSG_DEBUG("[INFO] Loaded image");
}
if (mipmapped) {
if (mipmapFilter == 0) {
std::cout << "[INFO] Using nearest-neighbor filtering for mipmaps\n";
//UF_MSG_DEBUG("[INFO] Using nearest-neighbor filtering for mipmaps");
} 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,
mipmapFilter == 0);
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) {
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;
}
@ -73,6 +64,19 @@ bool ImageContainer::load(const uf::stl::vector<uf::stl::string>& filenames, int
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() {
textureWidth = 0;

View File

@ -7,6 +7,8 @@
#include <texconv/palette.h>
#include <texconv/imagecontainer.h>
#include <uf/utils/memory/vector_stream.h>
Palette::Palette(const ImageContainer& images) {
for (int i=0;i<images.imageCount();i++) {
const Image& img=images.getByIndex(i);
@ -20,7 +22,7 @@ void Palette::insert(uint32_t color) {
if (colorsMap.find(color)==colorsMap.end()) {
int idx=(int)colorsVec.size();
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 {
std::ofstream out(filename,std::ios::binary);
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;
}
@ -58,13 +60,13 @@ bool Palette::save(const uf::stl::string& filename) const {
bool Palette::load(const uf::stl::string& filename) {
std::ifstream in(filename,std::ios::binary);
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;
}
char magic[4];
in.read(magic,4);
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;
}
int32_t numColors=0;
@ -78,4 +80,19 @@ bool Palette::load(const uf::stl::string& filename) {
}
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

View File

@ -84,14 +84,17 @@ bool generatePreview(const uf::stl::string& texFile,
bool genUsage=!codeUsageFile.empty();
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((char*)&width,2);
in.read((char*)&height,2);
in.read((char*)&textureType,4);
in.read((char*)&textureSize,4);
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);
in.read((char*)data.data(),textureSize);

View File

@ -5,23 +5,46 @@
#include <uf/utils/memory/string.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 texconv {
struct TextureOptions {
uf::stl::vector<uf::stl::string> inputs;
uf::stl::string output;
uf::stl::string format;
bool mipmap = false;
bool compress = false;
bool stride = false;
bool nearest = false;
bool bilinear = false;
bool verbose = false;
// reads from memory
pod::Dtex UF_API convert( const uf::Image& image, const uf::stl::string& format = "auto" );
// reads from disk
inline pod::Dtex UF_API convert( const uf::stl::string& input, const uf::stl::string& format = "auto" ) {
uf::Image image;
image.open( input );
return convert( image, format );
}
// CLI-like
bool UF_API convert( const pod::TextureOptions& opts );
uf::stl::string previewFile;
uf::stl::string codeUsageFile;
};
bool UF_API convertTexture( const TextureOptions& opts );
// dumps to disk
bool UF_API save( const pod::Dtex&, const uf::stl::string&, bool = false );
}
}

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/utils/graphic/graphic.h>
#include <uf/ext/xatlas/xatlas.h>
#include <uf/ext/texconv/texconv.h>
#include "../light/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) );
bool status = image.save(filename);
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 );

View File

@ -8,6 +8,7 @@
#include <uf/utils/camera/camera.h>
#include <uf/utils/math/physics.h>
#include <uf/ext/xatlas/xatlas.h>
#include <uf/ext/texconv/texconv.h>
#if !UF_ENV_DREAMCAST
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& image = /*graph.storage*/storage.images.map.at(name);
uf::stl::string f = "image."+std::to_string(i)+".png";
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;
json["name"] = name;
json["filename"] = f;

View File

@ -8,11 +8,14 @@
#include <algorithm>
#include <texconv/common.h>
#include <texconv/palette.h>
#include <texconv/imagecontainer.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 = {
{"ARGB1555",PIXELFORMAT_ARGB1555},
{"RGB565", PIXELFORMAT_RGB565},
@ -23,7 +26,129 @@ bool ext::texconv::convertTexture( const ext::texconv::TextureOptions& opts )
{"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" );
return false;
}
@ -33,8 +158,8 @@ bool ext::texconv::convertTexture( const ext::texconv::TextureOptions& opts )
}
int pixFmt = -1;
auto it = formats.find( opts.format );
if ( it != formats.end() ) pixFmt = it->second;
auto it = ::formats.find( opts.format );
if ( it != ::formats.end() ) pixFmt = it->second;
if ( pixFmt == -1 ){
UF_MSG_ERROR( "Unsupported format: {}", opts.format );
return false;
@ -42,19 +167,19 @@ bool ext::texconv::convertTexture( const ext::texconv::TextureOptions& opts )
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.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;
if ( opts.bilinear ) filter = 1;
ImageContainer images;
if ( !images.load( opts.inputs, textureType, filter ) ) return false;
if ( !images.load( {opts.input}, textureType, filter ) ) return false;
if ( textureType&FLAG_STRIDED ){
int stride = images.width()/32;
if ( textureType & FLAG_STRIDED ){
int stride = images.width() / 32;
textureType |= stride;
}
@ -83,8 +208,8 @@ bool ext::texconv::convertTexture( const ext::texconv::TextureOptions& opts )
UF_MSG_INFO( "Wrote texture {}", opts.output );
// Preview & Code usage
if ( !opts.previewFile.empty() || ( !opts.codeUsageFile.empty() && ( textureType&FLAG_COMPRESSED ) ) ){
if ( generatePreview( opts.output,palFile,opts.previewFile,opts.codeUsageFile ) ){
if ( !opts.previewFile.empty() || ( !opts.codeUsageFile.empty() && ( textureType & FLAG_COMPRESSED ) ) ){
if ( generatePreview( opts.output, palFile, opts.previewFile, opts.codeUsageFile ) ){
if ( !opts.previewFile.empty() ) UF_MSG_INFO( "Saved preview {}", opts.previewFile );
if ( !opts.codeUsageFile.empty() ) UF_MSG_INFO( "Saved code usage {}", opts.codeUsageFile );
} else {