Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gltfpack: Experimental support for floating point position quantization #462

Merged
merged 7 commits into from
Aug 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions gltf/gltfpack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,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 ? &qp : NULL, settings.quantize ? &qt_materials[i] : NULL);
writeMaterial(json_materials, data, material, settings.quantize && !settings.pos_float ? &qp : NULL, settings.quantize ? &qt_materials[i] : NULL);
if (settings.keep_extras)
writeExtras(json_materials, extras, material.extras);
append(json_materials, "}");
Expand Down Expand Up @@ -657,7 +657,7 @@ static void process(cgltf_data* data, const char* input_path, const char* output
assert(ni.keep);
ni.meshes.push_back(node_offset);

writeMeshNode(json_nodes, mesh_offset, mesh.nodes[j], mesh.skin, data, settings.quantize ? &qp : NULL);
writeMeshNode(json_nodes, mesh_offset, mesh.nodes[j], mesh.skin, data, settings.quantize && !settings.pos_float ? &qp : NULL);

node_offset++;
}
Expand All @@ -681,7 +681,7 @@ static void process(cgltf_data* data, const char* input_path, const char* output
comma(json_roots[mesh.scene]);
append(json_roots[mesh.scene], node_offset);

writeMeshNode(json_nodes, mesh_offset, NULL, mesh.skin, data, settings.quantize ? &qp : NULL);
writeMeshNode(json_nodes, mesh_offset, NULL, mesh.skin, data, settings.quantize && !settings.pos_float ? &qp : NULL);

node_offset++;
}
Expand Down Expand Up @@ -1191,6 +1191,10 @@ int main(int argc, char** argv)
{
settings.pos_normalized = true;
}
else if (strcmp(arg, "-vpf") == 0)
{
settings.pos_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);
Expand Down Expand Up @@ -1310,6 +1314,7 @@ int main(int argc, char** argv)
}
else if (strcmp(arg, "-noq") == 0)
{
// TODO: Warn if -noq is used and suggest -vpf instead; use -noqq to silence
settings.quantize = false;
}
else if (strcmp(arg, "-i") == 0 && i + 1 < argc && !input)
Expand Down Expand Up @@ -1424,6 +1429,7 @@ int main(int argc, char** argv)
fprintf(stderr, "\t-vn N: use N-bit quantization for normals and tangents (default: 8; N should be between 1 and 16)\n");
fprintf(stderr, "\t-vc N: use N-bit quantization for colors (default: 8; N should be between 1 and 16)\n");
fprintf(stderr, "\t-vpn: use normalized attributes for positions instead of using integers\n");
fprintf(stderr, "\t-vpf: use floating point attributes for positions instead of using integers\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");
Expand Down Expand Up @@ -1487,6 +1493,18 @@ int main(int argc, char** argv)
return 1;
}

if (settings.fallback && settings.compressmore)
{
fprintf(stderr, "Option -cf can not be used together with -cc\n");
return 1;
}

if (settings.fallback && settings.pos_float)
{
fprintf(stderr, "Option -cf can not be used together with -vpf\n");
return 1;
}

return gltfpack(input, output, report, settings);
}

Expand Down
3 changes: 2 additions & 1 deletion gltf/gltfpack.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ struct Settings
int col_bits;

bool pos_normalized;
bool pos_float;

int trn_bits;
int rot_bits;
Expand Down Expand Up @@ -322,7 +323,7 @@ void decomposeTransform(float translation[3], float rotation[4], float scale[3],

QuantizationPosition prepareQuantizationPosition(const std::vector<Mesh>& meshes, const Settings& settings);
void prepareQuantizationTexture(cgltf_data* data, std::vector<QuantizationTexture>& result, std::vector<size_t>& indices, const std::vector<Mesh>& meshes, const Settings& settings);
void getPositionBounds(float min[3], float max[3], const Stream& stream, const QuantizationPosition* qp);
void getPositionBounds(float min[3], float max[3], const Stream& stream, const QuantizationPosition& qp, const Settings& settings);

StreamFormat writeVertexStream(std::string& bin, const Stream& stream, const QuantizationPosition& qp, const QuantizationTexture& qt, const Settings& settings);
StreamFormat writeIndexStream(std::string& bin, const std::vector<unsigned int>& stream);
Expand Down
224 changes: 168 additions & 56 deletions gltf/stream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ void prepareQuantizationTexture(cgltf_data* data, std::vector<QuantizationTextur
}
}

void getPositionBounds(float min[3], float max[3], const Stream& stream, const QuantizationPosition* qp)
void getPositionBounds(float min[3], float max[3], const Stream& stream, const QuantizationPosition& qp, const Settings& settings)
{
assert(stream.type == cgltf_attribute_type_position);
assert(stream.data.size() > 0);
Expand All @@ -194,21 +194,32 @@ void getPositionBounds(float min[3], float max[3], const Stream& stream, const Q
}
}

if (qp)
if (settings.quantize)
{
float pos_rscale = qp->scale == 0.f ? 0.f : 1.f / qp->scale * (stream.target > 0 && qp->normalized ? 32767.f / 65535.f : 1.f);

for (int k = 0; k < 3; ++k)
if (settings.pos_float)
{
if (stream.target == 0)
for (int k = 0; k < 3; ++k)
{
min[k] = float(meshopt_quantizeUnorm((min[k] - qp->offset[k]) * pos_rscale, qp->bits));
max[k] = float(meshopt_quantizeUnorm((max[k] - qp->offset[k]) * pos_rscale, qp->bits));
min[k] = meshopt_quantizeFloat(min[k], qp.bits);
max[k] = meshopt_quantizeFloat(max[k], qp.bits);
}
else
}
else
{
float pos_rscale = qp.scale == 0.f ? 0.f : 1.f / qp.scale * (stream.target > 0 && qp.normalized ? 32767.f / 65535.f : 1.f);

for (int k = 0; k < 3; ++k)
{
min[k] = (min[k] >= 0.f ? 1.f : -1.f) * float(meshopt_quantizeUnorm(fabsf(min[k]) * pos_rscale, qp->bits));
max[k] = (max[k] >= 0.f ? 1.f : -1.f) * float(meshopt_quantizeUnorm(fabsf(max[k]) * pos_rscale, qp->bits));
if (stream.target == 0)
{
min[k] = float(meshopt_quantizeUnorm((min[k] - qp.offset[k]) * pos_rscale, qp.bits));
max[k] = float(meshopt_quantizeUnorm((max[k] - qp.offset[k]) * pos_rscale, qp.bits));
}
else
{
min[k] = (min[k] >= 0.f ? 1.f : -1.f) * float(meshopt_quantizeUnorm(fabsf(min[k]) * pos_rscale, qp.bits));
max[k] = (max[k] >= 0.f ? 1.f : -1.f) * float(meshopt_quantizeUnorm(fabsf(max[k]) * pos_rscale, qp.bits));
}
}
}
}
Expand Down Expand Up @@ -247,6 +258,115 @@ static void encodeOct(int& fu, int& fv, float nx, float ny, float nz, int bits)
fv = meshopt_quantizeSnorm(v, bits);
}

static void encodeQuat(int16_t v[4], const Attr& a, int bits)
{
const float scaler = sqrtf(2.f);

// establish maximum quaternion component
int qc = 0;
qc = fabsf(a.f[1]) > fabsf(a.f[qc]) ? 1 : qc;
qc = fabsf(a.f[2]) > fabsf(a.f[qc]) ? 2 : qc;
qc = fabsf(a.f[3]) > fabsf(a.f[qc]) ? 3 : qc;

// we use double-cover properties to discard the sign
float sign = a.f[qc] < 0.f ? -1.f : 1.f;

// note: we always encode a cyclical swizzle to be able to recover the order via rotation
v[0] = int16_t(meshopt_quantizeSnorm(a.f[(qc + 1) & 3] * scaler * sign, bits));
v[1] = int16_t(meshopt_quantizeSnorm(a.f[(qc + 2) & 3] * scaler * sign, bits));
v[2] = int16_t(meshopt_quantizeSnorm(a.f[(qc + 3) & 3] * scaler * sign, bits));
v[3] = int16_t((meshopt_quantizeSnorm(1.f, bits) & ~3) | qc);
}

static void encodeExpShared(uint32_t v[3], const Attr& a, int bits)
{
// get exponents from all components
int ex, ey, ez;
frexp(a.f[0], &ex);
frexp(a.f[1], &ey);
frexp(a.f[2], &ez);

// use maximum exponent to encode values; this guarantees that mantissa is [-1, 1]
// note that we additionally scale the mantissa to make it a K-bit signed integer (K-1 bits for magnitude)
int exp = std::max(ex, std::max(ey, ez)) - (bits - 1);

// compute renormalized rounded mantissas for each component
int mx = int(ldexp(a.f[0], -exp) + (a.f[0] >= 0 ? 0.5f : -0.5f));
int my = int(ldexp(a.f[1], -exp) + (a.f[1] >= 0 ? 0.5f : -0.5f));
int mz = int(ldexp(a.f[2], -exp) + (a.f[2] >= 0 ? 0.5f : -0.5f));

int mmask = (1 << 24) - 1;

// encode exponent & mantissa into each resulting value
v[0] = (mx & mmask) | (unsigned(exp) << 24);
v[1] = (my & mmask) | (unsigned(exp) << 24);
v[2] = (mz & mmask) | (unsigned(exp) << 24);
}

static uint32_t encodeExpOne(float v, int bits)
{
// extract exponent
int e;
frexp(v, &e);

// scale the mantissa to make it a K-bit signed integer (K-1 bits for magnitude)
int exp = e - (bits - 1);

// compute renormalized rounded mantissa
int m = int(ldexp(v, -exp) + (v >= 0 ? 0.5f : -0.5f));

int mmask = (1 << 24) - 1;

// encode exponent & mantissa
return (m & mmask) | (unsigned(exp) << 24);
}

static void encodeExpParallel(std::string& bin, const Attr* data, size_t count, int bits)
{
int expx = -128, expy = -128, expz = -128;

for (size_t i = 0; i < count; ++i)
{
const Attr& a = data[i];

// get exponents from all components
int ex, ey, ez;
frexp(a.f[0], &ex);
frexp(a.f[1], &ey);
frexp(a.f[2], &ez);

// use maximum exponent to encode values; this guarantees that mantissa is [-1, 1]
expx = std::max(expx, ex);
expy = std::max(expy, ey);
expz = std::max(expz, ez);
}

// scale the mantissa to make it a K-bit signed integer (K-1 bits for magnitude)
expx -= (bits - 1);
expy -= (bits - 1);
expz -= (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 mz = int(ldexp(a.f[2], -expz) + (a.f[2] >= 0 ? 0.5f : -0.5f));

int mmask = (1 << 24) - 1;

// encode exponent & mantissa
uint32_t v[3];
v[0] = (mx & mmask) | (unsigned(expx) << 24);
v[1] = (my & mmask) | (unsigned(expy) << 24);
v[2] = (mz & mmask) | (unsigned(expz) << 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);
Expand Down Expand Up @@ -279,6 +399,43 @@ StreamFormat writeVertexStream(std::string& bin, const Stream& stream, const Qua
if (!settings.quantize)
return writeVertexStreamRaw(bin, stream, cgltf_type_vec3, 3);

if (settings.pos_float)
{
StreamFormat::Filter filter = settings.compress ? StreamFormat::Filter_Exp : StreamFormat::Filter_None;

if (settings.compressmore)
{
encodeExpParallel(bin, &stream.data[0], stream.data.size(), qp.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[3];
v[0] = encodeExpOne(a.f[0], qp.bits + 1);
v[1] = encodeExpOne(a.f[1], qp.bits + 1);
v[2] = encodeExpOne(a.f[2], qp.bits + 1);
bin.append(reinterpret_cast<const char*>(v), sizeof(v));
}
else
{
float v[3] = {
meshopt_quantizeFloat(a.f[0], qp.bits),
meshopt_quantizeFloat(a.f[1], qp.bits),
meshopt_quantizeFloat(a.f[2], qp.bits)};
bin.append(reinterpret_cast<const char*>(v), sizeof(v));
}
}
}

StreamFormat format = {cgltf_type_vec3, cgltf_component_type_r_32f, false, 12, filter};
return format;
}

if (stream.target == 0)
{
float pos_rscale = qp.scale == 0.f ? 0.f : 1.f / qp.scale;
Expand Down Expand Up @@ -650,51 +807,6 @@ StreamFormat writeTimeStream(std::string& bin, const std::vector<float>& data)
return format;
}

static void encodeQuat(int16_t v[4], const Attr& a, int bits)
{
const float scaler = sqrtf(2.f);

// establish maximum quaternion component
int qc = 0;
qc = fabsf(a.f[1]) > fabsf(a.f[qc]) ? 1 : qc;
qc = fabsf(a.f[2]) > fabsf(a.f[qc]) ? 2 : qc;
qc = fabsf(a.f[3]) > fabsf(a.f[qc]) ? 3 : qc;

// we use double-cover properties to discard the sign
float sign = a.f[qc] < 0.f ? -1.f : 1.f;

// note: we always encode a cyclical swizzle to be able to recover the order via rotation
v[0] = int16_t(meshopt_quantizeSnorm(a.f[(qc + 1) & 3] * scaler * sign, bits));
v[1] = int16_t(meshopt_quantizeSnorm(a.f[(qc + 2) & 3] * scaler * sign, bits));
v[2] = int16_t(meshopt_quantizeSnorm(a.f[(qc + 3) & 3] * scaler * sign, bits));
v[3] = int16_t((meshopt_quantizeSnorm(1.f, bits) & ~3) | qc);
}

static void encodeExpShared(uint32_t v[3], const Attr& a, int bits)
{
// get exponents from all components
int ex, ey, ez;
frexp(a.f[0], &ex);
frexp(a.f[1], &ey);
frexp(a.f[2], &ez);

// use maximum exponent to encode values; this guarantess that mantissa is [-1, 1]
// note that we additionally scale the mantissa to make it a K-bit signed integer (K-1 bits for magnitude)
int exp = std::max(ex, std::max(ey, ez)) - (bits - 1);

// compute renormalized rounded mantissas for each component
int mx = int(ldexp(a.f[0], -exp) + (a.f[0] >= 0 ? 0.5f : -0.5f));
int my = int(ldexp(a.f[1], -exp) + (a.f[1] >= 0 ? 0.5f : -0.5f));
int mz = int(ldexp(a.f[2], -exp) + (a.f[2] >= 0 ? 0.5f : -0.5f));

int mmask = (1 << 24) - 1;

// encode exponent & mantissa into each resulting value
v[0] = (mx & mmask) | (unsigned(exp) << 24);
v[1] = (my & mmask) | (unsigned(exp) << 24);
v[2] = (mz & mmask) | (unsigned(exp) << 24);
}

StreamFormat writeKeyframeStream(std::string& bin, cgltf_animation_path_type type, const std::vector<Attr>& data, const Settings& settings)
{
if (type == cgltf_animation_path_type_rotation)
Expand Down
6 changes: 3 additions & 3 deletions gltf/write.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,7 @@ void writeMeshAttributes(std::string& json, std::vector<BufferView>& views, std:
{
float min[3] = {};
float max[3] = {};
getPositionBounds(min, max, stream, settings.quantize ? &qp : NULL);
getPositionBounds(min, max, stream, qp, settings);

writeAccessor(json_accessors, view, offset, format.type, format.component_type, format.normalized, stream.data.size(), min, max, 3);
}
Expand Down Expand Up @@ -1026,7 +1026,7 @@ size_t writeJointBindMatrices(std::vector<BufferView>& views, std::string& json_
cgltf_accessor_read_float(skin.inverse_bind_matrices, j, transform, 16);
}

if (settings.quantize)
if (settings.quantize && !settings.pos_float)
{
float node_scale = qp.scale / float((1 << qp.bits) - 1) * (qp.normalized ? 65535.f : 1.f);

Expand Down Expand Up @@ -1083,7 +1083,7 @@ size_t writeInstances(std::vector<BufferView>& views, std::string& json_accessor
{
decomposeTransform(position[i].f, rotation[i].f, scale[i].f, transforms[i].data);

if (settings.quantize)
if (settings.quantize && !settings.pos_float)
{
const float* transform = transforms[i].data;

Expand Down
2 changes: 1 addition & 1 deletion src/vertexfilter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -931,7 +931,7 @@ void meshopt_encodeFilterExp(void* destination_, size_t count, size_t stride, in
const float* v = &data[i * stride_float];
unsigned int* d = &destination[i * stride_float];

// use maximum exponent to encode values; this guarantess that mantissa is [-1, 1]
// use maximum exponent to encode values; this guarantees that mantissa is [-1, 1]
int exp = -100;

for (size_t j = 0; j < stride_float; ++j)
Expand Down