From 6d4837e3cc2cfd47de85c7d86f54c47ebe5a0f28 Mon Sep 17 00:00:00 2001 From: ecker Date: Tue, 19 Aug 2025 00:30:34 -0500 Subject: [PATCH] fixed DC texconv having some weird quad issue by just rebasing from the original file and de-QTing / reconverting --- bin/data/entities/model.json | 2 +- dep/include/texconv/common.h | 4 + dep/include/texconv/image.h | 9 +- dep/include/texconv/imagecontainer.h | 3 +- dep/include/texconv/palette.h | 2 +- dep/include/texconv/vqtools.h | 28 +- dep/src/texconv/common.cpp | 2 +- dep/src/texconv/conv16bpp.cpp | 395 +++++++++------- dep/src/texconv/convpal.cpp | 504 ++++++++++++--------- dep/src/texconv/image.cpp | 46 +- dep/src/texconv/imagecontainer.cpp | 44 +- dep/src/texconv/palette.cpp | 25 +- dep/src/texconv/preview.cpp | 7 +- engine/inc/uf/ext/texconv/texconv.h | 53 ++- engine/inc/uf/utils/memory/vector_stream.h | 35 ++ engine/src/engine/ext/baking/behavior.cpp | 8 + engine/src/engine/graph/encode.cpp | 11 +- engine/src/ext/texconv/texconv.cpp | 147 +++++- 18 files changed, 852 insertions(+), 473 deletions(-) create mode 100644 engine/inc/uf/utils/memory/vector_stream.h diff --git a/bin/data/entities/model.json b/bin/data/entities/model.json index 05eb4cf7..a7fa9332 100644 --- a/bin/data/entities/model.json +++ b/bin/data/entities/model.json @@ -78,7 +78,7 @@ "stream": { "tag": "worldspawn", "player": "info_player_spawn", - "enabled": false, // "auto", + "enabled": "auto", "radius": 32, "every": 1 } diff --git a/dep/include/texconv/common.h b/dep/include/texconv/common.h index 9750a844..29331b31 100644 --- a/dep/include/texconv/common.h +++ b/dep/include/texconv/common.h @@ -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); \ No newline at end of file diff --git a/dep/include/texconv/image.h b/dep/include/texconv/image.h index 30c6c50f..f429fa91 100644 --- a/dep/include/texconv/image.h +++ b/dep/include/texconv/image.h @@ -5,15 +5,18 @@ class Image { public: Image(); - Image(int width, int height); + Image(int width, int height, const uf::stl::vector& = {} ); + bool loadFromBuffer(const uf::stl::vector& 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& 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 pixels; + uf::stl::vector p; uf::stl::vector indexed; }; \ No newline at end of file diff --git a/dep/include/texconv/imagecontainer.h b/dep/include/texconv/imagecontainer.h index 09cd7d13..20d5ef10 100644 --- a/dep/include/texconv/imagecontainer.h +++ b/dep/include/texconv/imagecontainer.h @@ -5,7 +5,8 @@ class ImageContainer { public: - bool load(const uf::stl::vector& filenames, int textureType, int mipmapFilter); + bool load( const uf::stl::vector& images, int textureType, int mipmapFilter ); + bool load( const uf::stl::vector& filenames, int textureType, int mipmapFilter ); void unloadAll(); diff --git a/dep/include/texconv/palette.h b/dep/include/texconv/palette.h index 9562ffae..f425c6c4 100644 --- a/dep/include/texconv/palette.h +++ b/dep/include/texconv/palette.h @@ -19,7 +19,7 @@ public: bool load(const uf::stl::string& filename); bool save(const uf::stl::string& filename) const; - + uf::stl::vector encode() const; private: uf::stl::unordered_map colorsMap; uf::stl::vector colorsVec; diff --git a/dep/include/texconv/vqtools.h b/dep/include/texconv/vqtools.h index 0fb3940a..ad91cb4b 100644 --- a/dep/include/texconv/vqtools.h +++ b/dep/include/texconv/vqtools.h @@ -58,7 +58,6 @@ public: float length() const; void setLength(float len); void normalize(); - void print() const; static float distanceSquared(const Vec& a, const Vec& b); uint hash() const; void setHash(uint h) { hashVal = h; } @@ -177,17 +176,6 @@ inline void Vec::normalize() { v[i] *= invlen; } -template -void Vec::print() const { - uf::stl::string str = "{ "; - for (uint i=0; i float Vec::distanceSquared(const Vec& a, const Vec& b) { return (a - b).lengthSquared(); @@ -332,7 +320,7 @@ void VectorQuantizer::removeUnusedCodes() { codes.end() ); if(codes.size()::compress(const uf::stl::vector>& vectors,int num uf::stl::unordered_map,int> rle; for(const auto& v:vectors) rle[v]++; - std::cout<<"RLE result: "< "< {}", vectors.size(), rle.size()); codes.clear(); codes.resize(1); @@ -414,11 +402,11 @@ void VectorQuantizer::compress(const uf::stl::vector>& 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 "<::compress(const uf::stl::vector>& 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 "<(clock::now()-start).count(); - std::cout<<"Compression completed in "< bool VectorQuantizer::writeReportToFile(const uf::stl::string& fname){ std::ofstream f(fname); if(!f.is_open()){ - std::cerr<<"Failed to open "< #include -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& indexedImages, - uf::stl::vector& codebook, - int maxCodes) { - uf::stl::unordered_map 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& indexedImages, uf::stl::vector& codebook, int maxCodes) { + uf::stl::unordered_map uniqueQuads; // Quad <=> index - for (int i=0;i>& vectors) { + for (int i=0; i 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>& vectors) { + for (int i=0; i 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>& vectors, const VectorQuantizer<12>& vq, int pixelFormat, uf::stl::vector& indexedImages, uf::stl::vector& codebook) { + int vindex = 0; + + for (int i=0; i& vec = vectors[vindex]; + int codeIndex = vq.findClosest(vec); + img.setIndexedPixel(x, y, codeIndex); + vindex++; + } + } + indexedImages.push_back(img); + } + + for (int i=0; i& 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>& vectors, const VectorQuantizer<16>& vq, int format, uf::stl::vector& indexedImages, uf::stl::vector& codebook) { + int vindex = 0; + + for (int i=0; i& vec = vectors[vindex]; + int codeIndex = vq.findClosest(vec); + img.setIndexedPixel(x, y, codeIndex); + vindex++; + } + } + indexedImages.push_back(img); + } + + for (int i=0; i& 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 indexedImages; uf::stl::vector 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> vectors; VectorQuantizer<12> vq; - - for (int i=0; i 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& 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 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> 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> 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 static void vectorizeARGB(const ImageContainer& images, uf::stl::vector>& vectors) { - for (int i=0;i 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>& vectors, - const VectorQuantizer<4>& vq, uf::stl::vector& indexedImages, Palette& palette) { - int vindex=0; - for (int i=0;i>& vectors, const VectorQuantizer<4>& vq, uf::stl::vector& 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& 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& 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& * 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 indexedImages; - std::cout<<"Palette contains "<maxColors) { - std::cout<<"Reducing palette to "< 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> 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& dst) { - dst.clear(); for (int i=0; i& 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) { - if (indexedImages.size() > 1) { + // Write mipmap offset if necessary + if (indexedImages.size() > 1) writeZeroes(stream, MIPMAP_OFFSET_8BPP); - } - for (size_t i=0;i -static void grab2x4Block(const Image& img,const Palette& pal,int x,int y,Vec& 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& 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>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>& vectors) { @@ -241,147 +246,218 @@ static uint8_t findClosest(const uf::stl::vector>& vectors, const Vec<4>& return closestIndex; } -void writeCompressed4BPPData(std::ostream& stream,const uf::stl::vector& indexedImages,const Palette& palette){ +void writeCompressed4BPPData(std::ostream& stream, const uf::stl::vector& indexedImages, const Palette& palette) { VectorQuantizer<64> vq; uf::stl::vector> vectors; - if(indexedImages.size()>1){ - Vec<64> vec; vec.zero(); - for(size_t i=0;i 1) { + Vec<64> vec(0); + + for (int i=0; i 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 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> paletteVecs; - for(int i=0;i 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> vectorizedPalette; + vectorizePalette(palette, vectorizedPalette); - uint8_t codebook[2048]; memset(codebook,0,sizeof(codebook)); - Twiddler nibbleLUT(4,4); - for(int i=0;i& 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::distanceSquared(paletteVecs[pi],col); - if(d& 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& srcvec = vectors.at(i); + const int c = vq.findClosest(srcvec); + stream << (uint8_t)c; } } -void writeCompressed8BPPData(std::ostream& stream,const uf::stl::vector& indexedImages,const Palette& palette){ +void writeCompressed8BPPData(std::ostream& stream, const uf::stl::vector& indexedImages, const Palette& palette) { VectorQuantizer<32> vq; uf::stl::vector> vectors; - for(const auto& img:indexedImages){ - if(img.width() 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 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> paletteVecs; - for(int i=0;i 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& 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::distanceSquared(paletteVecs[pi],col); - if(d> 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& 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& srcvec = vectors.at(i); + const int c = vq.findClosest(srcvec); + stream << (uint8_t)c; } } + #endif \ No newline at end of file diff --git a/dep/src/texconv/image.cpp b/dep/src/texconv/image.cpp index 27320068..d52fb35b 100644 --- a/dep/src/texconv/image.cpp +++ b/dep/src/texconv/image.cpp @@ -9,48 +9,60 @@ #include 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& pixels) : w(width), h(height), p(pixels), indexedMode(false) { + p.resize(w*h); } +bool Image::loadFromBuffer(const uf::stl::vector& 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: "< buffer(w*h*4); + uf::stl::vector pixels(w*h*4); for(int y=0;y& 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 #include -bool ImageContainer::load(const uf::stl::vector& filenames, int textureType, int mipmapFilter) { +bool ImageContainer::load(const uf::stl::vector& _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& 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& 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& filenames, int return true; } +bool ImageContainer::load(const uf::stl::vector& filenames, int textureType, int mipmapFilter) { + uf::stl::vector 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; diff --git a/dep/src/texconv/palette.cpp b/dep/src/texconv/palette.cpp index 19c02375..667b90f7 100644 --- a/dep/src/texconv/palette.cpp +++ b/dep/src/texconv/palette.cpp @@ -7,6 +7,8 @@ #include #include +#include + Palette::Palette(const ImageContainer& images) { for (int i=0;i Palette::encode() const { + uf::stl::vector 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 \ No newline at end of file diff --git a/dep/src/texconv/preview.cpp b/dep/src/texconv/preview.cpp index ab1015aa..ce9481aa 100644 --- a/dep/src/texconv/preview.cpp +++ b/dep/src/texconv/preview.cpp @@ -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 "< data(textureSize); in.read((char*)data.data(),textureSize); diff --git a/engine/inc/uf/ext/texconv/texconv.h b/engine/inc/uf/ext/texconv/texconv.h index 81b28acb..a0e66054 100644 --- a/engine/inc/uf/ext/texconv/texconv.h +++ b/engine/inc/uf/ext/texconv/texconv.h @@ -5,23 +5,46 @@ #include #include +#include + +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 imageData; + uf::stl::vector paletteData; + }; +} + namespace ext { namespace texconv { - struct TextureOptions { - uf::stl::vector 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 ); } } \ No newline at end of file diff --git a/engine/inc/uf/utils/memory/vector_stream.h b/engine/inc/uf/utils/memory/vector_stream.h new file mode 100644 index 00000000..43fbcc4e --- /dev/null +++ b/engine/inc/uf/utils/memory/vector_stream.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +#include "vector.h" + +// cringe...... +namespace uf { + namespace stl { + class vector_streambuf : public std::streambuf { + uf::stl::vector& buffer; + public: + explicit vector_streambuf( uf::stl::vector& 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& vec) : std::ostream(&buf), buf(vec) {} + }; + } +} \ No newline at end of file diff --git a/engine/src/engine/ext/baking/behavior.cpp b/engine/src/engine/ext/baking/behavior.cpp index 94ab1509..ce2e275b 100644 --- a/engine/src/engine/ext/baking/behavior.cpp +++ b/engine/src/engine/ext/baking/behavior.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #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 ); diff --git a/engine/src/engine/graph/encode.cpp b/engine/src/engine/graph/encode.cpp index 8bbbede2..259640a8 100644 --- a/engine/src/engine/graph/encode.cpp +++ b/engine/src/engine/graph/encode.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #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; diff --git a/engine/src/ext/texconv/texconv.cpp b/engine/src/ext/texconv/texconv.cpp index 1cab60c4..3cab8806 100644 --- a/engine/src/ext/texconv/texconv.cpp +++ b/engine/src/ext/texconv/texconv.cpp @@ -8,11 +8,14 @@ #include #include +#include #include #include -bool ext::texconv::convertTexture( const ext::texconv::TextureOptions& opts ) { +#include + +namespace { uf::stl::unordered_map 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 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<