gltfpack: Implement support for floating-point texture coordinate quantization

Similarly to position quantization, we can use quantized floating point
values to represent texture coordinates. This can be useful as it trades
off a little bit of memory and compression efficiency for convenience
and interoperability - without the use of texture transforms it becomes
easy to swap textures, reuse materials, and avoid edge cases in
quantization when UV bounds are very large.
This commit is contained in:
Arseny Kapoulkine
2023-07-23 21:00:20 -07:00
parent 1a6ebfc200
commit cc2278469c
3 changed files with 110 additions and 20 deletions
+15 -4
View File
@@ -508,7 +508,7 @@ static void process(cgltf_data* data, const char* input_path, const char* output
comma(json_materials);
append(json_materials, "{");
writeMaterial(json_materials, data, material, settings.quantize && !settings.pos_float ? &qp : NULL, settings.quantize ? &qt_materials[i] : NULL);
writeMaterial(json_materials, data, material, settings.quantize && !settings.pos_float ? &qp : NULL, settings.quantize && !settings.tex_float ? &qt_materials[i] : NULL);
if (settings.keep_extras)
writeExtras(json_materials, material.extras);
append(json_materials, "}");
@@ -804,7 +804,7 @@ static void process(cgltf_data* data, const char* input_path, const char* output
const ExtensionInfo extensions[] = {
{"KHR_mesh_quantization", settings.quantize, true},
{"EXT_meshopt_compression", settings.compress, !settings.fallback},
{"KHR_texture_transform", (settings.quantize && !json_textures.empty()) || ext_texture_transform, false},
{"KHR_texture_transform", (settings.quantize && !settings.tex_float && !json_textures.empty()) || ext_texture_transform, false},
{"KHR_materials_pbrSpecularGlossiness", ext_pbr_specular_glossiness, false},
{"KHR_materials_clearcoat", ext_clearcoat, false},
{"KHR_materials_transmission", ext_transmission, false},
@@ -1213,6 +1213,14 @@ int main(int argc, char** argv)
{
settings.pos_float = true;
}
else if (strcmp(arg, "-vtn") == 0)
{
settings.tex_float = false;
}
else if (strcmp(arg, "-vtf") == 0)
{
settings.tex_float = true;
}
else if (strcmp(arg, "-at") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
{
settings.trn_bits = clamp(atoi(argv[++i]), 1, 24);
@@ -1455,6 +1463,9 @@ int main(int argc, char** argv)
fprintf(stderr, "\t-vpi: use integer attributes for positions (default)\n");
fprintf(stderr, "\t-vpn: use normalized attributes for positions\n");
fprintf(stderr, "\t-vpf: use floating point attributes for positions\n");
fprintf(stderr, "\nTexture coordinates:\n");
fprintf(stderr, "\t-vtn: use normalized attributes for texture coordinates (default)\n");
fprintf(stderr, "\t-vtf: use floating point attributes for texture coordinates\n");
fprintf(stderr, "\nAnimations:\n");
fprintf(stderr, "\t-at N: use N-bit quantization for translations (default: 16; N should be between 1 and 24)\n");
fprintf(stderr, "\t-ar N: use N-bit quantization for rotations (default: 12; N should be between 4 and 16)\n");
@@ -1524,9 +1535,9 @@ int main(int argc, char** argv)
return 1;
}
if (settings.fallback && settings.pos_float)
if (settings.fallback && (settings.pos_float || settings.tex_float))
{
fprintf(stderr, "Option -cf can not be used together with -vpf\n");
fprintf(stderr, "Option -cf can not be used together with -vpf or -tpf\n");
return 1;
}
+1
View File
@@ -112,6 +112,7 @@ struct Settings
bool pos_normalized;
bool pos_float;
bool tex_float;
int trn_bits;
int rot_bits;
+94 -16
View File
@@ -321,7 +321,7 @@ static uint32_t encodeExpOne(float v, int bits)
return (m & mmask) | (unsigned(exp) << 24);
}
static void encodeExpParallel(std::string& bin, const Attr* data, size_t count, int bits)
static void encodeExpParallel3(std::string& bin, const Attr* data, size_t count, int bits)
{
int expx = -128, expy = -128, expz = -128;
@@ -367,6 +367,47 @@ static void encodeExpParallel(std::string& bin, const Attr* data, size_t count,
}
}
static void encodeExpParallel2(std::string& bin, const Attr* data, size_t count, int bits)
{
int expx = -128, expy = -128;
for (size_t i = 0; i < count; ++i)
{
const Attr& a = data[i];
// get exponents from all components
int ex, ey;
frexp(a.f[0], &ex);
frexp(a.f[1], &ey);
// use maximum exponent to encode values; this guarantees that mantissa is [-1, 1]
expx = std::max(expx, ex);
expy = std::max(expy, ey);
}
// scale the mantissa to make it a K-bit signed integer (K-1 bits for magnitude)
expx -= (bits - 1);
expy -= (bits - 1);
for (size_t i = 0; i < count; ++i)
{
const Attr& a = data[i];
// compute renormalized rounded mantissas
int mx = int(ldexp(a.f[0], -expx) + (a.f[0] >= 0 ? 0.5f : -0.5f));
int my = int(ldexp(a.f[1], -expy) + (a.f[1] >= 0 ? 0.5f : -0.5f));
int mmask = (1 << 24) - 1;
// encode exponent & mantissa
uint32_t v[2];
v[0] = (mx & mmask) | (unsigned(expx) << 24);
v[1] = (my & mmask) | (unsigned(expy) << 24);
bin.append(reinterpret_cast<const char*>(v), sizeof(v));
}
}
static StreamFormat writeVertexStreamRaw(std::string& bin, const Stream& stream, cgltf_type type, size_t components)
{
assert(components >= 1 && components <= 4);
@@ -405,7 +446,7 @@ StreamFormat writeVertexStream(std::string& bin, const Stream& stream, const Qua
if (settings.compressmore)
{
encodeExpParallel(bin, &stream.data[0], stream.data.size(), qp.bits + 1);
encodeExpParallel3(bin, &stream.data[0], stream.data.size(), qp.bits + 1);
}
else
{
@@ -511,24 +552,61 @@ StreamFormat writeVertexStream(std::string& bin, const Stream& stream, const Qua
if (!settings.quantize)
return writeVertexStreamRaw(bin, stream, cgltf_type_vec2, 2);
float uv_rscale[2] = {
qt.scale[0] == 0.f ? 0.f : 1.f / qt.scale[0],
qt.scale[1] == 0.f ? 0.f : 1.f / qt.scale[1],
};
for (size_t i = 0; i < stream.data.size(); ++i)
if (settings.tex_float)
{
const Attr& a = stream.data[i];
StreamFormat::Filter filter = settings.compress ? StreamFormat::Filter_Exp : StreamFormat::Filter_None;
uint16_t v[2] = {
uint16_t(meshopt_quantizeUnorm((a.f[0] - qt.offset[0]) * uv_rscale[0], qt.bits)),
uint16_t(meshopt_quantizeUnorm((a.f[1] - qt.offset[1]) * uv_rscale[1], qt.bits)),
};
bin.append(reinterpret_cast<const char*>(v), sizeof(v));
if (settings.compressmore)
{
encodeExpParallel2(bin, &stream.data[0], stream.data.size(), qt.bits + 1);
}
else
{
for (size_t i = 0; i < stream.data.size(); ++i)
{
const Attr& a = stream.data[i];
if (filter == StreamFormat::Filter_Exp)
{
uint32_t v[2];
v[0] = encodeExpOne(a.f[0], qt.bits + 1);
v[1] = encodeExpOne(a.f[1], qt.bits + 1);
bin.append(reinterpret_cast<const char*>(v), sizeof(v));
}
else
{
float v[2] = {
meshopt_quantizeFloat(a.f[0], qt.bits),
meshopt_quantizeFloat(a.f[1], qt.bits)};
bin.append(reinterpret_cast<const char*>(v), sizeof(v));
}
}
}
StreamFormat format = {cgltf_type_vec2, cgltf_component_type_r_32f, false, 8, filter};
return format;
}
else
{
float uv_rscale[2] = {
qt.scale[0] == 0.f ? 0.f : 1.f / qt.scale[0],
qt.scale[1] == 0.f ? 0.f : 1.f / qt.scale[1],
};
StreamFormat format = {cgltf_type_vec2, cgltf_component_type_r_16u, qt.normalized, 4};
return format;
for (size_t i = 0; i < stream.data.size(); ++i)
{
const Attr& a = stream.data[i];
uint16_t v[2] = {
uint16_t(meshopt_quantizeUnorm((a.f[0] - qt.offset[0]) * uv_rscale[0], qt.bits)),
uint16_t(meshopt_quantizeUnorm((a.f[1] - qt.offset[1]) * uv_rscale[1], qt.bits)),
};
bin.append(reinterpret_cast<const char*>(v), sizeof(v));
}
StreamFormat format = {cgltf_type_vec2, cgltf_component_type_r_16u, qt.normalized, 4};
return format;
}
}
else if (stream.type == cgltf_attribute_type_normal)
{