DS2Revival/mods/modloader/gltf_image.cpp

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