Skip to content

Commit

Permalink
Merge pull request zeux#462 from zeux/gltf-vpf
Browse files Browse the repository at this point in the history
gltfpack: Experimental support for floating point position quantization
  • Loading branch information
zeux committed Aug 18, 2022
2 parents 0f520b7 + 4c42e04 commit 29a6b8c
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 64 deletions.
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

0 comments on commit 29a6b8c

Please sign in to comment.