377 lines
13 KiB
C++
377 lines
13 KiB
C++
// Images for the glTF import (gltf.h): decoding (stb_image, public domain / MIT, third_party/stb_image.h), Pillow's
|
|
// LANCZOS resize and the DXT5 DDS writer of tools/ds2mesh.py (write_dds, _dxt5_level, _dxt_color_block,
|
|
// _dxt5_alpha_block), ported operation by operation so that the bytes are the same.
|
|
// Why DXT5 with a full mip chain: the game's DDS reader steps through mip levels by the DXT size of a level for every
|
|
// format, so uncompressed files get broken mips (docs/formats/asp.md, "Textures and names").
|
|
#include "gltf.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
|
|
#if defined(_MSC_VER) && !defined(__clang__)
|
|
#pragma warning(push, 0)
|
|
#pragma warning(disable : 4244 4245 4456 4457 4701 4702 4996)
|
|
#elif defined(__clang__)
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Weverything"
|
|
#endif
|
|
#define STB_IMAGE_IMPLEMENTATION
|
|
#define STB_IMAGE_STATIC
|
|
#define STBI_NO_STDIO
|
|
#define STBI_ONLY_PNG
|
|
#define STBI_ONLY_JPEG
|
|
#define STBI_MAX_DIMENSIONS 16384
|
|
#define STBI_NO_SIMD // the SSE2 JPEG path needs -msse2 under clang-cl (CI); decoding runs once per change
|
|
#include "third_party/stb_image.h"
|
|
#if defined(_MSC_VER) && !defined(__clang__)
|
|
#pragma warning(pop)
|
|
#elif defined(__clang__)
|
|
#pragma clang diagnostic pop
|
|
#endif
|
|
|
|
namespace gltf {
|
|
|
|
bool DecodeImage(const uint8_t* data, size_t size, Image& out, std::string& error) {
|
|
if (!data || size == 0 || size > 0x7fffffff) {
|
|
error = "empty image";
|
|
return false;
|
|
}
|
|
int w = 0, h = 0, n = 0;
|
|
stbi_uc* px = stbi_load_from_memory(data, static_cast<int>(size), &w, &h, &n, 4);
|
|
if (!px) {
|
|
const char* why = stbi_failure_reason();
|
|
error = std::string("image not decoded (PNG and JPEG are supported): ") + (why ? why : "?");
|
|
return false;
|
|
}
|
|
out.w = w;
|
|
out.h = h;
|
|
out.rgba.assign(px, px + static_cast<size_t>(w) * h * 4);
|
|
stbi_image_free(px);
|
|
return true;
|
|
}
|
|
|
|
bool Inflate(const uint8_t* data, size_t size, size_t expected, Bytes& out) {
|
|
if (size > 0x7fffffff || expected > 0x7fffffff) return false;
|
|
out.assign(expected ? expected : 1, 0);
|
|
int n = stbi_zlib_decode_buffer(reinterpret_cast<char*>(out.data()), static_cast<int>(out.size()),
|
|
reinterpret_cast<const char*>(data), static_cast<int>(size));
|
|
if (n < 0) return false;
|
|
out.resize(static_cast<size_t>(n));
|
|
return true;
|
|
}
|
|
|
|
// --- Pillow's resample (Resample.c), LANCZOS, 8 bits per channel ----------------------------------------------------
|
|
namespace {
|
|
|
|
const int kPrecision = 32 - 8 - 2;
|
|
|
|
double Sinc(double x) {
|
|
if (x == 0.0) return 1.0;
|
|
x = x * 3.14159265358979323846;
|
|
return std::sin(x) / x;
|
|
}
|
|
|
|
double Lanczos(double x) {
|
|
if (-3.0 <= x && x < 3.0) return Sinc(x) * Sinc(x / 3);
|
|
return 0.0;
|
|
}
|
|
|
|
struct Coeffs {
|
|
std::vector<int> start, count;
|
|
std::vector<std::vector<int>> k;
|
|
};
|
|
|
|
Coeffs Precompute(int inSize, int outSize) {
|
|
Coeffs c;
|
|
double scale = static_cast<double>(inSize) / outSize, filterscale = scale;
|
|
if (filterscale < 1.0) filterscale = 1.0;
|
|
double support = 3.0 * filterscale;
|
|
for (int xx = 0; xx < outSize; ++xx) {
|
|
double center = (xx + 0.5) * scale, ww = 0.0, ss = 1.0 / filterscale;
|
|
int xmin = static_cast<int>(center - support + 0.5);
|
|
if (xmin < 0) xmin = 0;
|
|
int xmax = static_cast<int>(center + support + 0.5);
|
|
if (xmax > inSize) xmax = inSize;
|
|
xmax -= xmin;
|
|
std::vector<double> w(static_cast<size_t>(std::max(xmax, 0)));
|
|
for (int x = 0; x < xmax; ++x) {
|
|
w[x] = Lanczos((x + xmin - center + 0.5) * ss);
|
|
ww += w[x];
|
|
}
|
|
std::vector<int> ik(w.size());
|
|
for (size_t x = 0; x < w.size(); ++x) {
|
|
double v = ww != 0.0 ? w[x] / ww : w[x];
|
|
ik[x] = v < 0 ? static_cast<int>(-0.5 + v * (1 << kPrecision)) : static_cast<int>(0.5 + v * (1 << kPrecision));
|
|
}
|
|
c.start.push_back(xmin);
|
|
c.count.push_back(xmax);
|
|
c.k.push_back(ik);
|
|
}
|
|
return c;
|
|
}
|
|
|
|
inline uint8_t Clip8(int v) {
|
|
v >>= kPrecision;
|
|
return static_cast<uint8_t>(v < 0 ? 0 : v > 255 ? 255 : v);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Image ResizeLanczos(const Image& in, int w, int h) {
|
|
// RGBA -> premultiplied (Pillow converts to "RGBa" before resizing and back afterwards)
|
|
std::vector<uint8_t> px(in.rgba);
|
|
for (size_t i = 0; i + 3 < px.size(); i += 4) {
|
|
unsigned a = px[i + 3];
|
|
for (int c = 0; c < 3; ++c) {
|
|
unsigned t = px[i + c] * a + 128;
|
|
px[i + c] = static_cast<uint8_t>(((t >> 8) + t) >> 8);
|
|
}
|
|
}
|
|
int cw = in.w, ch = in.h;
|
|
if (w != in.w) { // horizontal pass (all rows: the vertical pass, if any, uses them all for these sizes)
|
|
Coeffs cx = Precompute(in.w, w);
|
|
int y0 = 0, y1 = in.h;
|
|
if (h != in.h) {
|
|
Coeffs cy = Precompute(in.h, h);
|
|
y0 = cy.start.front();
|
|
y1 = cy.start.back() + cy.count.back();
|
|
}
|
|
std::vector<uint8_t> tmp(static_cast<size_t>(w) * (y1 - y0) * 4);
|
|
for (int y = y0; y < y1; ++y) {
|
|
const uint8_t* row = &px[static_cast<size_t>(y) * in.w * 4];
|
|
for (int xx = 0; xx < w; ++xx) {
|
|
int s[4] = {1 << (kPrecision - 1), 1 << (kPrecision - 1), 1 << (kPrecision - 1), 1 << (kPrecision - 1)};
|
|
const std::vector<int>& k = cx.k[xx];
|
|
for (int x = 0; x < cx.count[xx]; ++x)
|
|
for (int c = 0; c < 4; ++c) s[c] += row[(x + cx.start[xx]) * 4 + c] * k[x];
|
|
for (int c = 0; c < 4; ++c) tmp[(static_cast<size_t>(y - y0) * w + xx) * 4 + c] = Clip8(s[c]);
|
|
}
|
|
}
|
|
// rows of tmp are source rows y0.. ; shift for the vertical pass
|
|
if (h != in.h) {
|
|
Coeffs cy = Precompute(in.h, h);
|
|
std::vector<uint8_t> out(static_cast<size_t>(w) * h * 4);
|
|
for (int yy = 0; yy < h; ++yy)
|
|
for (int x = 0; x < w; ++x) {
|
|
int s[4] = {1 << (kPrecision - 1), 1 << (kPrecision - 1), 1 << (kPrecision - 1), 1 << (kPrecision - 1)};
|
|
for (int y = 0; y < cy.count[yy]; ++y)
|
|
for (int c = 0; c < 4; ++c)
|
|
s[c] += tmp[(static_cast<size_t>(cy.start[yy] - y0 + y) * w + x) * 4 + c] * cy.k[yy][y];
|
|
for (int c = 0; c < 4; ++c) out[(static_cast<size_t>(yy) * w + x) * 4 + c] = Clip8(s[c]);
|
|
}
|
|
px.swap(out);
|
|
ch = h;
|
|
} else {
|
|
px.swap(tmp);
|
|
}
|
|
cw = w;
|
|
} else if (h != in.h) {
|
|
Coeffs cy = Precompute(in.h, h);
|
|
std::vector<uint8_t> out(static_cast<size_t>(cw) * h * 4);
|
|
for (int yy = 0; yy < h; ++yy)
|
|
for (int x = 0; x < cw; ++x) {
|
|
int s[4] = {1 << (kPrecision - 1), 1 << (kPrecision - 1), 1 << (kPrecision - 1), 1 << (kPrecision - 1)};
|
|
for (int y = 0; y < cy.count[yy]; ++y)
|
|
for (int c = 0; c < 4; ++c)
|
|
s[c] += px[(static_cast<size_t>(cy.start[yy] + y) * cw + x) * 4 + c] * cy.k[yy][y];
|
|
for (int c = 0; c < 4; ++c) out[(static_cast<size_t>(yy) * cw + x) * 4 + c] = Clip8(s[c]);
|
|
}
|
|
px.swap(out);
|
|
ch = h;
|
|
}
|
|
// premultiplied -> straight alpha ("RGBa" -> "RGBA")
|
|
for (size_t i = 0; i + 3 < px.size(); i += 4) {
|
|
unsigned a = px[i + 3];
|
|
if (a == 255 || a == 0) continue;
|
|
for (int c = 0; c < 3; ++c) {
|
|
unsigned v = 255u * px[i + c] / a;
|
|
px[i + c] = static_cast<uint8_t>(v > 255 ? 255 : v);
|
|
}
|
|
}
|
|
Image out;
|
|
out.w = cw;
|
|
out.h = ch;
|
|
out.rgba.swap(px);
|
|
return out;
|
|
}
|
|
|
|
// --- DXT5 (BC3) -------------------------------------------------------------------------------------------------------
|
|
namespace {
|
|
|
|
void Put16(Bytes& b, unsigned v) {
|
|
b.push_back(static_cast<uint8_t>(v));
|
|
b.push_back(static_cast<uint8_t>(v >> 8));
|
|
}
|
|
void Put32(Bytes& b, uint32_t v) {
|
|
for (int i = 0; i < 4; ++i) b.push_back(static_cast<uint8_t>(v >> (8 * i)));
|
|
}
|
|
|
|
// Python's sum() of floats (CPython 3.12+): 0 + the first, then Neumaier-compensated (see PySum in gltf_convert.cpp).
|
|
double FloatSum(const double* x, int n) {
|
|
double f = 0.0 + x[0], c = 0.0;
|
|
for (int k = 1; k < n; ++k) {
|
|
double t = f + x[k];
|
|
if (std::fabs(f) >= std::fabs(x[k])) c += (f - t) + x[k];
|
|
else c += (x[k] - t) + f;
|
|
f = t;
|
|
}
|
|
if (c != 0 && std::isfinite(c)) f += c;
|
|
return f;
|
|
}
|
|
|
|
// 16 RGB pixels -> 8-byte BC1 colour block, 4-colour mode, endpoints on the bounding box diagonal.
|
|
void ColorBlock(const int px[16][3], Bytes& out) {
|
|
int lo[3], hi[3];
|
|
double mean[3];
|
|
for (int k = 0; k < 3; ++k) {
|
|
lo[k] = hi[k] = px[0][k];
|
|
int sum = 0;
|
|
for (int i = 0; i < 16; ++i) {
|
|
lo[k] = std::min(lo[k], px[i][k]);
|
|
hi[k] = std::max(hi[k], px[i][k]);
|
|
sum += px[i][k];
|
|
}
|
|
mean[k] = sum / 16.0;
|
|
}
|
|
int ref = 0;
|
|
for (int k = 1; k < 3; ++k)
|
|
if (hi[k] - lo[k] > hi[ref] - lo[ref]) ref = k;
|
|
for (int k = 0; k < 3; ++k) {
|
|
if (k == ref) continue;
|
|
double terms[16];
|
|
for (int i = 0; i < 16; ++i) terms[i] = (px[i][k] - mean[k]) * (px[i][ref] - mean[ref]);
|
|
if (FloatSum(terms, 16) < 0) std::swap(lo[k], hi[k]);
|
|
}
|
|
auto q = [](const int c[3]) {
|
|
return (((c[0] * 31 + 127) / 255) << 11) | (((c[1] * 63 + 127) / 255) << 5) | ((c[2] * 31 + 127) / 255);
|
|
};
|
|
auto dq = [](int v, double o[3]) {
|
|
int r = (v >> 11) & 31, g = (v >> 5) & 63, b = v & 31;
|
|
o[0] = (r << 3) | (r >> 2);
|
|
o[1] = (g << 2) | (g >> 4);
|
|
o[2] = (b << 3) | (b >> 2);
|
|
};
|
|
int c0 = q(hi), c1 = q(lo);
|
|
if (c0 < c1) std::swap(c0, c1);
|
|
if (c0 == c1) {
|
|
Put16(out, c0);
|
|
Put16(out, c1);
|
|
Put32(out, 0);
|
|
return;
|
|
}
|
|
double pal[4][3];
|
|
dq(c0, pal[0]);
|
|
dq(c1, pal[1]);
|
|
for (int k = 0; k < 3; ++k) {
|
|
pal[2][k] = (2 * pal[0][k] + pal[1][k]) / 3.0;
|
|
pal[3][k] = (pal[0][k] + 2 * pal[1][k]) / 3.0;
|
|
}
|
|
uint32_t bits = 0;
|
|
for (int i = 0; i < 16; ++i) {
|
|
int best = 0;
|
|
double bestD = 0;
|
|
for (int j = 0; j < 4; ++j) {
|
|
double e[3];
|
|
for (int k = 0; k < 3; ++k) e[k] = (px[i][k] - pal[j][k]) * (px[i][k] - pal[j][k]);
|
|
double d = j < 2 ? e[0] + e[1] + e[2] : FloatSum(e, 3); // j < 2: Python ints, exact
|
|
if (j == 0 || d < bestD) {
|
|
best = j;
|
|
bestD = d;
|
|
}
|
|
}
|
|
bits |= static_cast<uint32_t>(best) << (2 * i);
|
|
}
|
|
Put16(out, c0);
|
|
Put16(out, c1);
|
|
Put32(out, bits);
|
|
}
|
|
|
|
void AlphaBlock(const int al[16], Bytes& out) {
|
|
int a0 = al[0], a1 = al[0];
|
|
for (int i = 1; i < 16; ++i) {
|
|
a0 = std::max(a0, al[i]);
|
|
a1 = std::min(a1, al[i]);
|
|
}
|
|
out.push_back(static_cast<uint8_t>(a0));
|
|
out.push_back(static_cast<uint8_t>(a1));
|
|
if (a0 == a1) {
|
|
for (int i = 0; i < 6; ++i) out.push_back(0);
|
|
return;
|
|
}
|
|
double pal[8] = {static_cast<double>(a0), static_cast<double>(a1)};
|
|
for (int j = 1; j < 7; ++j) pal[j + 1] = ((7 - j) * a0 + j * a1) / 7.0;
|
|
uint64_t bits = 0;
|
|
for (int i = 0; i < 16; ++i) {
|
|
int best = 0;
|
|
double bestD = 0;
|
|
for (int j = 0; j < 8; ++j) {
|
|
double d = std::fabs(al[i] - pal[j]);
|
|
if (j == 0 || d < bestD) {
|
|
best = j;
|
|
bestD = d;
|
|
}
|
|
}
|
|
bits |= static_cast<uint64_t>(best) << (3 * i);
|
|
}
|
|
for (int i = 0; i < 6; ++i) out.push_back(static_cast<uint8_t>(bits >> (8 * i)));
|
|
}
|
|
|
|
Bytes Dxt5Level(const Image& im) {
|
|
Bytes out;
|
|
int w = im.w, h = im.h;
|
|
for (int by = 0; by < std::max(h, 4); by += 4)
|
|
for (int bx = 0; bx < std::max(w, 4); bx += 4) {
|
|
int px[16][3], al[16];
|
|
for (int i = 0; i < 16; ++i) {
|
|
int x = std::min(bx + i % 4, w - 1), y = std::min(by + i / 4, h - 1);
|
|
const uint8_t* p = &im.rgba[(static_cast<size_t>(y) * w + x) * 4];
|
|
px[i][0] = p[0];
|
|
px[i][1] = p[1];
|
|
px[i][2] = p[2];
|
|
al[i] = p[3];
|
|
}
|
|
AlphaBlock(al, out);
|
|
ColorBlock(px, out);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
int Pow2(int n, int maxSize) {
|
|
int bits = 0;
|
|
for (unsigned v = static_cast<unsigned>(n - 1); v; v >>= 1) ++bits; // (n - 1).bit_length()
|
|
return std::min(maxSize, 1 << bits);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Bytes WriteDds(Image im, int maxSize) {
|
|
int w = Pow2(im.w, maxSize), h = Pow2(im.h, maxSize);
|
|
if (w != im.w || h != im.h) im = ResizeLanczos(im, w, h);
|
|
std::vector<Bytes> levels;
|
|
for (;;) {
|
|
levels.push_back(Dxt5Level(im));
|
|
if (im.w == 1 && im.h == 1) break;
|
|
im = ResizeLanczos(im, std::max(1, im.w / 2), std::max(1, im.h / 2));
|
|
}
|
|
Bytes out;
|
|
out.insert(out.end(), {'D', 'D', 'S', ' '});
|
|
Put32(out, 124);
|
|
Put32(out, 0x1 | 0x2 | 0x4 | 0x1000 | 0x20000 | 0x80000);
|
|
Put32(out, static_cast<uint32_t>(h));
|
|
Put32(out, static_cast<uint32_t>(w));
|
|
Put32(out, static_cast<uint32_t>(levels[0].size()));
|
|
Put32(out, 0);
|
|
Put32(out, static_cast<uint32_t>(levels.size()));
|
|
out.insert(out.end(), 44, 0);
|
|
Put32(out, 32);
|
|
Put32(out, 0x4);
|
|
out.insert(out.end(), {'D', 'X', 'T', '5'});
|
|
for (int i = 0; i < 5; ++i) Put32(out, 0);
|
|
Put32(out, 0x1000 | 0x8 | 0x400000);
|
|
for (int i = 0; i < 4; ++i) Put32(out, 0);
|
|
for (const Bytes& l : levels) out.insert(out.end(), l.begin(), l.end());
|
|
return out;
|
|
}
|
|
|
|
} // namespace gltf
|