vendored meshoptimizer 1.2(submodule 环境故障改直接 vendor)。Mesh 多 LOD:各级索引连续拼进同一 index buffer(共享顶点,LodRange 区间 选择),解析期 meshopt_simplify 生成链(LOD0=原始,每级目标减半, 相对误差 1e-2,小网格复用上级);都在 worker 线程,加载零额外卡顿。 SceneRenderer 按 (mesh, LOD) 聚合 batch(lookup key = 指针<<8|lod): 主视图按屏占比近似 radius/dist 选级(阈值 10/20 倍半径);阴影级联 按级联序降级(越远越简)。Draw 按 batch.lod 选索引区间。 A/B 对比(100 背包 10×10,RTX 5060 Laptop,vsync off): - 纯背包:LOD 开 93-95 fps / 关 90-107 fps —— 无显著差异 - 加 SunTemple:LOD 开 52-56 / 关 51-55 —— 无显著差异 结论:当前场景瓶颈不在顶点数(5060 顶点吞吐富余),LOD 基础设施 保留(几何复杂资产/低端 GPU/更大场景生效),帧率问题转向 Tracy GPU 时间线定位真实瓶颈。 坑:LodRange 记录"LOD 不足复用最后一级"时 first 不得继续累加 (buffer 无重复数据,累加必越界——VUID-08798 抓的)。
914 lines
27 KiB
C++
914 lines
27 KiB
C++
// This file is part of gltfpack; see gltfpack.h for version/license details
|
|
#include "gltfpack.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include <float.h>
|
|
#include <limits.h>
|
|
#include <math.h>
|
|
#include <stdint.h>
|
|
|
|
#include "../src/meshoptimizer.h"
|
|
|
|
struct Bounds
|
|
{
|
|
Attr min, max;
|
|
|
|
Bounds()
|
|
{
|
|
min.f[0] = min.f[1] = min.f[2] = min.f[3] = +FLT_MAX;
|
|
max.f[0] = max.f[1] = max.f[2] = max.f[3] = -FLT_MAX;
|
|
}
|
|
|
|
bool isValid() const
|
|
{
|
|
return min.f[0] <= max.f[0] && min.f[1] <= max.f[1] && min.f[2] <= max.f[2] && min.f[3] <= max.f[3];
|
|
}
|
|
|
|
float getExtent() const
|
|
{
|
|
return std::max(max.f[0] - min.f[0], std::max(max.f[1] - min.f[1], max.f[2] - min.f[2]));
|
|
}
|
|
|
|
void merge(const Bounds& other)
|
|
{
|
|
for (int k = 0; k < 4; ++k)
|
|
{
|
|
min.f[k] = std::min(min.f[k], other.min.f[k]);
|
|
max.f[k] = std::max(max.f[k], other.max.f[k]);
|
|
}
|
|
}
|
|
};
|
|
|
|
static Bounds computeBounds(const Mesh& mesh, cgltf_attribute_type type)
|
|
{
|
|
Bounds b;
|
|
Attr pad = {};
|
|
|
|
for (size_t j = 0; j < mesh.streams.size(); ++j)
|
|
{
|
|
const Stream& s = mesh.streams[j];
|
|
|
|
if (s.type == type)
|
|
{
|
|
if (s.target == 0)
|
|
{
|
|
for (size_t k = 0; k < s.data.size(); ++k)
|
|
{
|
|
const Attr& a = s.data[k];
|
|
|
|
b.min.f[0] = std::min(b.min.f[0], a.f[0]);
|
|
b.min.f[1] = std::min(b.min.f[1], a.f[1]);
|
|
b.min.f[2] = std::min(b.min.f[2], a.f[2]);
|
|
b.min.f[3] = std::min(b.min.f[3], a.f[3]);
|
|
|
|
b.max.f[0] = std::max(b.max.f[0], a.f[0]);
|
|
b.max.f[1] = std::max(b.max.f[1], a.f[1]);
|
|
b.max.f[2] = std::max(b.max.f[2], a.f[2]);
|
|
b.max.f[3] = std::max(b.max.f[3], a.f[3]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (size_t k = 0; k < s.data.size(); ++k)
|
|
{
|
|
const Attr& a = s.data[k];
|
|
|
|
pad.f[0] = std::max(pad.f[0], fabsf(a.f[0]));
|
|
pad.f[1] = std::max(pad.f[1], fabsf(a.f[1]));
|
|
pad.f[2] = std::max(pad.f[2], fabsf(a.f[2]));
|
|
pad.f[3] = std::max(pad.f[3], fabsf(a.f[3]));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int k = 0; k < 4; ++k)
|
|
{
|
|
b.min.f[k] -= pad.f[k];
|
|
b.max.f[k] += pad.f[k];
|
|
}
|
|
|
|
return b;
|
|
}
|
|
|
|
static float computeUvArea(const Mesh& mesh)
|
|
{
|
|
if (mesh.indices.empty() || mesh.type != cgltf_primitive_type_triangles)
|
|
return 0.f;
|
|
|
|
float result = 0.f;
|
|
|
|
for (size_t j = 0; j < mesh.streams.size(); ++j)
|
|
{
|
|
const Stream& s = mesh.streams[j];
|
|
|
|
if (s.type != cgltf_attribute_type_texcoord)
|
|
continue;
|
|
|
|
float uvarea = 0.f;
|
|
|
|
for (size_t i = 0; i < mesh.indices.size(); i += 3)
|
|
{
|
|
unsigned int a = mesh.indices[i + 0];
|
|
unsigned int b = mesh.indices[i + 1];
|
|
unsigned int c = mesh.indices[i + 2];
|
|
|
|
const Attr& va = s.data[a];
|
|
const Attr& vb = s.data[b];
|
|
const Attr& vc = s.data[c];
|
|
|
|
uvarea += fabsf((vb.f[0] - va.f[0]) * (vc.f[1] - va.f[1]) - (vc.f[0] - va.f[0]) * (vb.f[1] - va.f[1]));
|
|
}
|
|
|
|
result = std::max(result, uvarea / float(mesh.indices.size() / 3));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
QuantizationPosition prepareQuantizationPosition(const std::vector<Mesh>& meshes, const Settings& settings)
|
|
{
|
|
QuantizationPosition result = {};
|
|
|
|
result.bits = settings.pos_bits;
|
|
result.normalized = settings.pos_normalized;
|
|
|
|
std::vector<Bounds> bounds(meshes.size());
|
|
|
|
for (size_t i = 0; i < meshes.size(); ++i)
|
|
bounds[i] = computeBounds(meshes[i], cgltf_attribute_type_position);
|
|
|
|
Bounds b;
|
|
for (size_t i = 0; i < meshes.size(); ++i)
|
|
b.merge(bounds[i]);
|
|
|
|
if (b.isValid())
|
|
{
|
|
result.offset[0] = b.min.f[0];
|
|
result.offset[1] = b.min.f[1];
|
|
result.offset[2] = b.min.f[2];
|
|
result.scale = b.getExtent();
|
|
}
|
|
|
|
if (b.isValid() && settings.quantize && !settings.pos_float)
|
|
{
|
|
float error = result.scale * 0.5f / (1 << (result.bits - 1));
|
|
float max_rel_error = 0;
|
|
|
|
for (size_t i = 0; i < meshes.size(); ++i)
|
|
if (bounds[i].isValid() && bounds[i].getExtent() > 1e-2f)
|
|
max_rel_error = std::max(max_rel_error, error / bounds[i].getExtent());
|
|
|
|
if (max_rel_error > 5e-2f)
|
|
fprintf(stderr, "Warning: position data has significant error (%.0f%%); consider using floating-point quantization (-vpf) or more bits (-vp N)\n", max_rel_error * 100);
|
|
}
|
|
|
|
result.node_scale = result.scale / float((1 << result.bits) - 1) * (result.normalized ? 65535.f : 1.f);
|
|
|
|
return result;
|
|
}
|
|
|
|
static size_t follow(std::vector<size_t>& parents, size_t index)
|
|
{
|
|
while (index != parents[index])
|
|
{
|
|
size_t parent = parents[index];
|
|
|
|
parents[index] = parents[parent];
|
|
index = parent;
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
void prepareQuantizationTexture(cgltf_data* data, std::vector<QuantizationTexture>& result, std::vector<size_t>& indices, const std::vector<Mesh>& meshes, const Settings& settings)
|
|
{
|
|
// use union-find to associate each material with a canonical material
|
|
// this is necessary because any set of materials that are used on the same mesh must use the same quantization
|
|
std::vector<size_t> parents(result.size());
|
|
|
|
for (size_t i = 0; i < parents.size(); ++i)
|
|
parents[i] = i;
|
|
|
|
for (size_t i = 0; i < meshes.size(); ++i)
|
|
{
|
|
const Mesh& mesh = meshes[i];
|
|
|
|
if (!mesh.material && mesh.variants.empty())
|
|
continue;
|
|
|
|
size_t root = follow(parents, (mesh.material ? mesh.material : mesh.variants[0].material) - data->materials);
|
|
|
|
for (size_t j = 0; j < mesh.variants.size(); ++j)
|
|
{
|
|
size_t var = follow(parents, mesh.variants[j].material - data->materials);
|
|
|
|
parents[var] = root;
|
|
}
|
|
|
|
indices[i] = root;
|
|
}
|
|
|
|
// compute canonical material bounds based on meshes that use them
|
|
std::vector<Bounds> bounds(result.size());
|
|
|
|
for (size_t i = 0; i < meshes.size(); ++i)
|
|
{
|
|
const Mesh& mesh = meshes[i];
|
|
|
|
if (!mesh.material && mesh.variants.empty())
|
|
continue;
|
|
|
|
indices[i] = follow(parents, indices[i]);
|
|
|
|
Bounds mb = computeBounds(mesh, cgltf_attribute_type_texcoord);
|
|
bounds[indices[i]].merge(mb);
|
|
}
|
|
|
|
// detect potential precision issues and warn about them
|
|
if (settings.quantize && !settings.tex_float)
|
|
{
|
|
float max_rel_error = 0;
|
|
|
|
for (size_t i = 0; i < meshes.size(); ++i)
|
|
{
|
|
const Mesh& mesh = meshes[i];
|
|
|
|
if (!mesh.material && mesh.variants.empty())
|
|
continue;
|
|
|
|
const Bounds& b = bounds[indices[i]];
|
|
if (!b.isValid())
|
|
continue;
|
|
|
|
float scale = std::max(b.max.f[0] - b.min.f[0], b.max.f[1] - b.min.f[1]);
|
|
float error = scale * 0.5f / (1 << (settings.tex_bits - 1));
|
|
|
|
if (error < 1e-3f)
|
|
continue;
|
|
|
|
float uvarea = computeUvArea(mesh);
|
|
float rel_error = uvarea > 0 ? error / sqrtf(uvarea) : 0.f;
|
|
|
|
max_rel_error = std::max(max_rel_error, rel_error);
|
|
}
|
|
|
|
if (max_rel_error > 1e-1f)
|
|
fprintf(stderr, "Warning: texture coordinate data has significant error (%.0f%%); consider using floating-point quantization (-vtf) or more bits (-vt N)\n", max_rel_error * 100);
|
|
}
|
|
|
|
// update all material data using canonical bounds
|
|
for (size_t i = 0; i < result.size(); ++i)
|
|
{
|
|
QuantizationTexture& qt = result[i];
|
|
|
|
qt.bits = settings.tex_bits;
|
|
qt.normalized = true;
|
|
|
|
const Bounds& b = bounds[follow(parents, i)];
|
|
|
|
if (b.isValid())
|
|
{
|
|
qt.offset[0] = b.min.f[0];
|
|
qt.offset[1] = b.min.f[1];
|
|
qt.scale[0] = b.max.f[0] - b.min.f[0];
|
|
qt.scale[1] = b.max.f[1] - b.min.f[1];
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
min[0] = min[1] = min[2] = FLT_MAX;
|
|
max[0] = max[1] = max[2] = -FLT_MAX;
|
|
|
|
for (size_t i = 0; i < stream.data.size(); ++i)
|
|
{
|
|
const Attr& a = stream.data[i];
|
|
|
|
for (int k = 0; k < 3; ++k)
|
|
{
|
|
min[k] = std::min(min[k], a.f[k]);
|
|
max[k] = std::max(max[k], a.f[k]);
|
|
}
|
|
}
|
|
|
|
if (settings.quantize)
|
|
{
|
|
if (settings.pos_float)
|
|
{
|
|
for (int k = 0; k < 3; ++k)
|
|
{
|
|
min[k] = meshopt_quantizeFloat(min[k], qp.bits);
|
|
max[k] = meshopt_quantizeFloat(max[k], qp.bits);
|
|
}
|
|
}
|
|
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)
|
|
{
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void renormalizeWeights(uint8_t (&w)[4])
|
|
{
|
|
int sum = w[0] + w[1] + w[2] + w[3];
|
|
|
|
if (sum == 255)
|
|
return;
|
|
|
|
// we assume that the total error is limited to 0.5/component = 2
|
|
// this means that it's acceptable to adjust the max. component to compensate for the error
|
|
int max = 0;
|
|
|
|
for (int k = 1; k < 4; ++k)
|
|
if (w[k] > w[max])
|
|
max = k;
|
|
|
|
w[max] += uint8_t(255 - sum);
|
|
}
|
|
|
|
static void encodeSnorm(void* destination, size_t count, size_t stride, int bits, const float* data)
|
|
{
|
|
assert(stride == 4 || stride == 8);
|
|
assert(bits >= 1 && bits <= 16);
|
|
|
|
signed char* d8 = static_cast<signed char*>(destination);
|
|
short* d16 = static_cast<short*>(destination);
|
|
|
|
for (size_t i = 0; i < count; ++i)
|
|
{
|
|
const float* v = &data[i * 4];
|
|
|
|
int fx = meshopt_quantizeSnorm(v[0], bits);
|
|
int fy = meshopt_quantizeSnorm(v[1], bits);
|
|
int fz = meshopt_quantizeSnorm(v[2], bits);
|
|
int fw = meshopt_quantizeSnorm(v[3], bits);
|
|
|
|
if (stride == 4)
|
|
{
|
|
d8[i * 4 + 0] = (signed char)(fx);
|
|
d8[i * 4 + 1] = (signed char)(fy);
|
|
d8[i * 4 + 2] = (signed char)(fz);
|
|
d8[i * 4 + 3] = (signed char)(fw);
|
|
}
|
|
else
|
|
{
|
|
d16[i * 4 + 0] = short(fx);
|
|
d16[i * 4 + 1] = short(fy);
|
|
d16[i * 4 + 2] = short(fz);
|
|
d16[i * 4 + 3] = short(fw);
|
|
}
|
|
}
|
|
}
|
|
|
|
static int quantizeColor(float v, int bytebits, int bits)
|
|
{
|
|
int result = meshopt_quantizeUnorm(v, bytebits);
|
|
|
|
// replicate the top bit into the low significant bits
|
|
const int mask = (1 << (bytebits - bits)) - 1;
|
|
|
|
return (result & ~mask) | (mask & -(result >> (bytebits - 1)));
|
|
}
|
|
|
|
static void encodeColor(void* destination, size_t count, size_t stride, int bits, const float* data)
|
|
{
|
|
assert(stride == 4 || stride == 8);
|
|
assert(bits >= 2 && bits <= 16);
|
|
|
|
unsigned char* d8 = static_cast<unsigned char*>(destination);
|
|
unsigned short* d16 = static_cast<unsigned short*>(destination);
|
|
|
|
for (size_t i = 0; i < count; ++i)
|
|
{
|
|
const float* c = &data[i * 4];
|
|
|
|
if (stride == 4)
|
|
{
|
|
d8[i * 4 + 0] = uint8_t(quantizeColor(c[0], 8, bits));
|
|
d8[i * 4 + 1] = uint8_t(quantizeColor(c[1], 8, bits));
|
|
d8[i * 4 + 2] = uint8_t(quantizeColor(c[2], 8, bits));
|
|
d8[i * 4 + 3] = uint8_t(quantizeColor(c[3], 8, bits));
|
|
}
|
|
else
|
|
{
|
|
d16[i * 4 + 0] = uint16_t(quantizeColor(c[0], 16, bits));
|
|
d16[i * 4 + 1] = uint16_t(quantizeColor(c[1], 16, bits));
|
|
d16[i * 4 + 2] = uint16_t(quantizeColor(c[2], 16, bits));
|
|
d16[i * 4 + 3] = uint16_t(quantizeColor(c[3], 16, bits));
|
|
}
|
|
}
|
|
}
|
|
|
|
static StreamFormat writeVertexStreamRaw(std::string& bin, const Stream& stream, cgltf_type type, size_t components)
|
|
{
|
|
assert(components >= 1 && components <= 4);
|
|
|
|
for (size_t i = 0; i < stream.data.size(); ++i)
|
|
{
|
|
const Attr& a = stream.data[i];
|
|
|
|
bin.append(reinterpret_cast<const char*>(a.f), sizeof(float) * components);
|
|
}
|
|
|
|
StreamFormat format = {type, cgltf_component_type_r_32f, false, sizeof(float) * components};
|
|
return format;
|
|
}
|
|
|
|
static StreamFormat writeVertexStreamFloat(std::string& bin, const Stream& stream, cgltf_type type, int components, bool expf, int bits, meshopt_EncodeExpMode mode)
|
|
{
|
|
assert(components >= 1 && components <= 4);
|
|
|
|
StreamFormat::Filter filter = expf ? StreamFormat::Filter_Exp : StreamFormat::Filter_None;
|
|
|
|
if (filter == StreamFormat::Filter_Exp)
|
|
{
|
|
size_t offset = bin.size();
|
|
size_t stride = sizeof(float) * components;
|
|
for (size_t i = 0; i < stream.data.size(); ++i)
|
|
bin.append(reinterpret_cast<const char*>(stream.data[i].f), stride);
|
|
|
|
meshopt_encodeFilterExp(&bin[offset], stream.data.size(), stride, bits + 1, reinterpret_cast<const float*>(&bin[offset]), mode);
|
|
}
|
|
else
|
|
{
|
|
for (size_t i = 0; i < stream.data.size(); ++i)
|
|
{
|
|
const Attr& a = stream.data[i];
|
|
|
|
float v[4];
|
|
for (int k = 0; k < components; ++k)
|
|
v[k] = meshopt_quantizeFloat(a.f[k], bits);
|
|
bin.append(reinterpret_cast<const char*>(v), sizeof(float) * components);
|
|
}
|
|
}
|
|
|
|
StreamFormat format = {type, cgltf_component_type_r_32f, false, sizeof(float) * components, filter};
|
|
return format;
|
|
}
|
|
|
|
StreamFormat writeVertexStream(std::string& bin, const Stream& stream, const QuantizationPosition& qp, const QuantizationTexture& qt, const Settings& settings, bool filters)
|
|
{
|
|
if (stream.type == cgltf_attribute_type_position)
|
|
{
|
|
if (!settings.quantize)
|
|
return writeVertexStreamRaw(bin, stream, cgltf_type_vec3, 3);
|
|
|
|
if (settings.pos_float)
|
|
return writeVertexStreamFloat(bin, stream, cgltf_type_vec3, 3, settings.compress && filters, qp.bits,
|
|
settings.compressmore ? meshopt_EncodeExpSharedComponent : meshopt_EncodeExpSeparate);
|
|
|
|
if (stream.target == 0)
|
|
{
|
|
float pos_rscale = qp.scale == 0.f ? 0.f : 1.f / qp.scale;
|
|
|
|
for (size_t i = 0; i < stream.data.size(); ++i)
|
|
{
|
|
const Attr& a = stream.data[i];
|
|
|
|
uint16_t v[4] = {
|
|
uint16_t(meshopt_quantizeUnorm((a.f[0] - qp.offset[0]) * pos_rscale, qp.bits)),
|
|
uint16_t(meshopt_quantizeUnorm((a.f[1] - qp.offset[1]) * pos_rscale, qp.bits)),
|
|
uint16_t(meshopt_quantizeUnorm((a.f[2] - qp.offset[2]) * pos_rscale, qp.bits)),
|
|
0};
|
|
bin.append(reinterpret_cast<const char*>(v), sizeof(v));
|
|
}
|
|
|
|
StreamFormat format = {cgltf_type_vec3, cgltf_component_type_r_16u, qp.normalized, 8};
|
|
return format;
|
|
}
|
|
else
|
|
{
|
|
float pos_rscale = qp.scale == 0.f ? 0.f : 1.f / qp.scale * (qp.normalized ? 32767.f / 65535.f : 1.f);
|
|
|
|
int maxv = 0;
|
|
|
|
for (size_t i = 0; i < stream.data.size(); ++i)
|
|
{
|
|
const Attr& a = stream.data[i];
|
|
|
|
maxv = std::max(maxv, meshopt_quantizeUnorm(fabsf(a.f[0]) * pos_rscale, qp.bits));
|
|
maxv = std::max(maxv, meshopt_quantizeUnorm(fabsf(a.f[1]) * pos_rscale, qp.bits));
|
|
maxv = std::max(maxv, meshopt_quantizeUnorm(fabsf(a.f[2]) * pos_rscale, qp.bits));
|
|
}
|
|
|
|
if (maxv <= 127 && !qp.normalized)
|
|
{
|
|
for (size_t i = 0; i < stream.data.size(); ++i)
|
|
{
|
|
const Attr& a = stream.data[i];
|
|
|
|
int8_t v[4] = {
|
|
int8_t((a.f[0] >= 0.f ? 1 : -1) * meshopt_quantizeUnorm(fabsf(a.f[0]) * pos_rscale, qp.bits)),
|
|
int8_t((a.f[1] >= 0.f ? 1 : -1) * meshopt_quantizeUnorm(fabsf(a.f[1]) * pos_rscale, qp.bits)),
|
|
int8_t((a.f[2] >= 0.f ? 1 : -1) * meshopt_quantizeUnorm(fabsf(a.f[2]) * pos_rscale, qp.bits)),
|
|
0};
|
|
bin.append(reinterpret_cast<const char*>(v), sizeof(v));
|
|
}
|
|
|
|
StreamFormat format = {cgltf_type_vec3, cgltf_component_type_r_8, false, 4};
|
|
return format;
|
|
}
|
|
else
|
|
{
|
|
for (size_t i = 0; i < stream.data.size(); ++i)
|
|
{
|
|
const Attr& a = stream.data[i];
|
|
|
|
int16_t v[4] = {
|
|
int16_t((a.f[0] >= 0.f ? 1 : -1) * meshopt_quantizeUnorm(fabsf(a.f[0]) * pos_rscale, qp.bits)),
|
|
int16_t((a.f[1] >= 0.f ? 1 : -1) * meshopt_quantizeUnorm(fabsf(a.f[1]) * pos_rscale, qp.bits)),
|
|
int16_t((a.f[2] >= 0.f ? 1 : -1) * meshopt_quantizeUnorm(fabsf(a.f[2]) * pos_rscale, qp.bits)),
|
|
0};
|
|
bin.append(reinterpret_cast<const char*>(v), sizeof(v));
|
|
}
|
|
|
|
StreamFormat format = {cgltf_type_vec3, cgltf_component_type_r_16, qp.normalized, 8};
|
|
return format;
|
|
}
|
|
}
|
|
}
|
|
else if (stream.type == cgltf_attribute_type_texcoord)
|
|
{
|
|
if (!settings.quantize)
|
|
return writeVertexStreamRaw(bin, stream, cgltf_type_vec2, 2);
|
|
|
|
// expand the encoded range to ensure it covers [0..1) interval
|
|
// this can slightly reduce precision but we should not need more precision inside 0..1, and this significantly improves compressed size when using encodeExpOne
|
|
if (settings.tex_float)
|
|
return writeVertexStreamFloat(bin, stream, cgltf_type_vec2, 2, settings.compress && filters, qt.bits, meshopt_EncodeExpClamped);
|
|
|
|
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)
|
|
{
|
|
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)
|
|
{
|
|
if (!settings.quantize)
|
|
return writeVertexStreamRaw(bin, stream, cgltf_type_vec3, 3);
|
|
|
|
// expand the encoded range to ensure it covers [0..1) interval
|
|
// for unit length vectors, SharedComponent results in a slightly smaller encoding vs Clamped
|
|
// for morph deltas, we could use SharedComponent but it may preserve noisy small inputs redundantly
|
|
if (settings.nrm_float)
|
|
return writeVertexStreamFloat(bin, stream, cgltf_type_vec3, 3, settings.compress && filters, settings.nrm_bits,
|
|
settings.compressmore && stream.target == 0 ? meshopt_EncodeExpSharedComponent : meshopt_EncodeExpClamped);
|
|
|
|
bool oct = filters && settings.compressmore && stream.target == 0;
|
|
int bits = settings.nrm_bits;
|
|
|
|
StreamFormat::Filter filter = oct ? StreamFormat::Filter_Oct : StreamFormat::Filter_None;
|
|
|
|
size_t offset = bin.size();
|
|
size_t stride = bits > 8 ? 8 : 4;
|
|
bin.resize(bin.size() + stream.data.size() * stride);
|
|
|
|
if (oct)
|
|
meshopt_encodeFilterOct(&bin[offset], stream.data.size(), stride, bits, stream.data[0].f);
|
|
else
|
|
encodeSnorm(&bin[offset], stream.data.size(), stride, bits, stream.data[0].f);
|
|
|
|
cgltf_component_type component_type = bits > 8 ? cgltf_component_type_r_16 : cgltf_component_type_r_8;
|
|
StreamFormat format = {cgltf_type_vec3, component_type, true, stride, filter};
|
|
return format;
|
|
}
|
|
else if (stream.type == cgltf_attribute_type_tangent)
|
|
{
|
|
if (!settings.quantize)
|
|
return writeVertexStreamRaw(bin, stream, cgltf_type_vec4, 4);
|
|
|
|
bool oct = filters && settings.compressmore && stream.target == 0;
|
|
int bits = (settings.nrm_bits > 8) ? 8 : settings.nrm_bits;
|
|
|
|
StreamFormat::Filter filter = oct ? StreamFormat::Filter_Oct : StreamFormat::Filter_None;
|
|
|
|
size_t offset = bin.size();
|
|
size_t stride = 4;
|
|
bin.resize(bin.size() + stream.data.size() * stride);
|
|
|
|
if (oct)
|
|
meshopt_encodeFilterOct(&bin[offset], stream.data.size(), stride, bits, stream.data[0].f);
|
|
else
|
|
encodeSnorm(&bin[offset], stream.data.size(), stride, bits, stream.data[0].f);
|
|
|
|
cgltf_type type = (stream.target == 0) ? cgltf_type_vec4 : cgltf_type_vec3;
|
|
StreamFormat format = {type, cgltf_component_type_r_8, true, 4, filter};
|
|
return format;
|
|
}
|
|
else if (stream.type == cgltf_attribute_type_color)
|
|
{
|
|
bool col = filters && settings.compresskhr && settings.compressmore;
|
|
int bits = settings.col_bits;
|
|
|
|
StreamFormat::Filter filter = col ? StreamFormat::Filter_Color : StreamFormat::Filter_None;
|
|
|
|
size_t offset = bin.size();
|
|
size_t stride = bits > 8 ? 8 : 4;
|
|
bin.resize(bin.size() + stream.data.size() * stride);
|
|
|
|
if (col)
|
|
meshopt_encodeFilterColor(&bin[offset], stream.data.size(), stride, bits, stream.data[0].f);
|
|
else
|
|
encodeColor(&bin[offset], stream.data.size(), stride, bits, stream.data[0].f);
|
|
|
|
if (bits > 8)
|
|
{
|
|
StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_16u, true, 8, filter};
|
|
return format;
|
|
}
|
|
else
|
|
{
|
|
StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_8u, true, 4, filter};
|
|
return format;
|
|
}
|
|
}
|
|
else if (stream.type == cgltf_attribute_type_weights)
|
|
{
|
|
for (size_t i = 0; i < stream.data.size(); ++i)
|
|
{
|
|
const Attr& a = stream.data[i];
|
|
|
|
float ws = a.f[0] + a.f[1] + a.f[2] + a.f[3];
|
|
float wsi = (ws == 0.f) ? 0.f : 1.f / ws;
|
|
|
|
uint8_t v[4] = {
|
|
uint8_t(meshopt_quantizeUnorm(a.f[0] * wsi, 8)),
|
|
uint8_t(meshopt_quantizeUnorm(a.f[1] * wsi, 8)),
|
|
uint8_t(meshopt_quantizeUnorm(a.f[2] * wsi, 8)),
|
|
uint8_t(meshopt_quantizeUnorm(a.f[3] * wsi, 8))};
|
|
|
|
if (wsi != 0.f)
|
|
renormalizeWeights(v);
|
|
|
|
bin.append(reinterpret_cast<const char*>(v), sizeof(v));
|
|
}
|
|
|
|
StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_8u, true, 4};
|
|
return format;
|
|
}
|
|
else if (stream.type == cgltf_attribute_type_joints)
|
|
{
|
|
unsigned int maxj = 0;
|
|
|
|
for (size_t i = 0; i < stream.data.size(); ++i)
|
|
for (int k = 0; k < 4; ++k)
|
|
maxj = std::max(maxj, unsigned(int(stream.data[i].f[k])));
|
|
|
|
assert(maxj <= 65535);
|
|
|
|
if (maxj <= 255)
|
|
{
|
|
for (size_t i = 0; i < stream.data.size(); ++i)
|
|
{
|
|
const Attr& a = stream.data[i];
|
|
|
|
uint8_t v[4] = {
|
|
uint8_t(a.f[0]),
|
|
uint8_t(a.f[1]),
|
|
uint8_t(a.f[2]),
|
|
uint8_t(a.f[3])};
|
|
bin.append(reinterpret_cast<const char*>(v), sizeof(v));
|
|
}
|
|
|
|
StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_8u, false, 4};
|
|
return format;
|
|
}
|
|
else
|
|
{
|
|
for (size_t i = 0; i < stream.data.size(); ++i)
|
|
{
|
|
const Attr& a = stream.data[i];
|
|
|
|
uint16_t v[4] = {
|
|
uint16_t(a.f[0]),
|
|
uint16_t(a.f[1]),
|
|
uint16_t(a.f[2]),
|
|
uint16_t(a.f[3])};
|
|
bin.append(reinterpret_cast<const char*>(v), sizeof(v));
|
|
}
|
|
|
|
StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_16u, false, 8};
|
|
return format;
|
|
}
|
|
}
|
|
else if (stream.type == cgltf_attribute_type_custom)
|
|
{
|
|
// note: _custom is equivalent to _ID, as such the data contains scalar integers
|
|
if (!settings.compressmore || !filters)
|
|
return writeVertexStreamRaw(bin, stream, cgltf_type_scalar, 1);
|
|
|
|
unsigned int maxv = 0;
|
|
|
|
for (size_t i = 0; i < stream.data.size(); ++i)
|
|
maxv = std::max(maxv, unsigned(int(stream.data[i].f[0])));
|
|
|
|
// exp encoding uses a signed mantissa with only 23 significant bits; input glTF encoding may encode indices losslessly up to 2^24
|
|
if (maxv >= (1 << 23))
|
|
return writeVertexStreamRaw(bin, stream, cgltf_type_scalar, 1);
|
|
|
|
for (size_t i = 0; i < stream.data.size(); ++i)
|
|
{
|
|
const Attr& a = stream.data[i];
|
|
|
|
uint32_t id = uint32_t(a.f[0]);
|
|
uint32_t v = id; // exp encoding of integers in [0..2^23-1] range is equivalent to the integer itself
|
|
|
|
bin.append(reinterpret_cast<const char*>(&v), sizeof(v));
|
|
}
|
|
|
|
StreamFormat format = {cgltf_type_scalar, cgltf_component_type_r_32f, false, 4, StreamFormat::Filter_Exp};
|
|
return format;
|
|
}
|
|
else
|
|
{
|
|
return writeVertexStreamRaw(bin, stream, cgltf_type_vec4, 4);
|
|
}
|
|
}
|
|
|
|
StreamFormat writeIndexStream(std::string& bin, const std::vector<unsigned int>& stream)
|
|
{
|
|
unsigned int maxi = 0;
|
|
for (size_t i = 0; i < stream.size(); ++i)
|
|
maxi = std::max(maxi, stream[i]);
|
|
|
|
// save 16-bit indices if we can; note that we can't use restart index (65535)
|
|
if (maxi < 65535)
|
|
{
|
|
for (size_t i = 0; i < stream.size(); ++i)
|
|
{
|
|
uint16_t v[1] = {uint16_t(stream[i])};
|
|
bin.append(reinterpret_cast<const char*>(v), sizeof(v));
|
|
}
|
|
|
|
StreamFormat format = {cgltf_type_scalar, cgltf_component_type_r_16u, false, 2};
|
|
return format;
|
|
}
|
|
else
|
|
{
|
|
for (size_t i = 0; i < stream.size(); ++i)
|
|
{
|
|
uint32_t v[1] = {stream[i]};
|
|
bin.append(reinterpret_cast<const char*>(v), sizeof(v));
|
|
}
|
|
|
|
StreamFormat format = {cgltf_type_scalar, cgltf_component_type_r_32u, false, 4};
|
|
return format;
|
|
}
|
|
}
|
|
|
|
StreamFormat writeTimeStream(std::string& bin, const std::vector<float>& data)
|
|
{
|
|
for (size_t i = 0; i < data.size(); ++i)
|
|
{
|
|
float v[1] = {data[i]};
|
|
bin.append(reinterpret_cast<const char*>(v), sizeof(v));
|
|
}
|
|
|
|
StreamFormat format = {cgltf_type_scalar, cgltf_component_type_r_32f, false, 4};
|
|
return format;
|
|
}
|
|
|
|
StreamFormat writeKeyframeStream(std::string& bin, cgltf_animation_path_type type, const std::vector<Attr>& data, const Settings& settings, bool has_tangents)
|
|
{
|
|
if (type == cgltf_animation_path_type_rotation)
|
|
{
|
|
StreamFormat::Filter filter = settings.compressmore && !has_tangents ? StreamFormat::Filter_Quat : StreamFormat::Filter_None;
|
|
|
|
size_t offset = bin.size();
|
|
size_t stride = 8;
|
|
bin.resize(bin.size() + data.size() * stride);
|
|
|
|
if (filter == StreamFormat::Filter_Quat)
|
|
meshopt_encodeFilterQuat(&bin[offset], data.size(), stride, settings.rot_bits, data[0].f);
|
|
else
|
|
encodeSnorm(&bin[offset], data.size(), stride, 16, data[0].f);
|
|
|
|
StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_16, true, 8, filter};
|
|
return format;
|
|
}
|
|
else if (type == cgltf_animation_path_type_weights)
|
|
{
|
|
for (size_t i = 0; i < data.size(); ++i)
|
|
{
|
|
const Attr& a = data[i];
|
|
|
|
uint8_t v[1] = {uint8_t(meshopt_quantizeUnorm(a.f[0], 8))};
|
|
bin.append(reinterpret_cast<const char*>(v), sizeof(v));
|
|
}
|
|
|
|
StreamFormat format = {cgltf_type_scalar, cgltf_component_type_r_8u, true, 1};
|
|
return format;
|
|
}
|
|
else if (type == cgltf_animation_path_type_translation || type == cgltf_animation_path_type_scale)
|
|
{
|
|
StreamFormat::Filter filter = settings.compressmore ? StreamFormat::Filter_Exp : StreamFormat::Filter_None;
|
|
int bits = (type == cgltf_animation_path_type_translation) ? settings.trn_bits : settings.scl_bits;
|
|
|
|
size_t offset = bin.size();
|
|
|
|
for (size_t i = 0; i < data.size(); ++i)
|
|
{
|
|
const Attr& a = data[i];
|
|
|
|
float v[3] = {a.f[0], a.f[1], a.f[2]};
|
|
bin.append(reinterpret_cast<const char*>(v), sizeof(v));
|
|
}
|
|
|
|
if (filter == StreamFormat::Filter_Exp)
|
|
meshopt_encodeFilterExp(&bin[offset], data.size(), 12, bits, reinterpret_cast<const float*>(&bin[offset]), meshopt_EncodeExpSharedVector);
|
|
|
|
StreamFormat format = {cgltf_type_vec3, cgltf_component_type_r_32f, false, 12, filter};
|
|
return format;
|
|
}
|
|
else
|
|
{
|
|
for (size_t i = 0; i < data.size(); ++i)
|
|
{
|
|
const Attr& a = data[i];
|
|
|
|
float v[4] = {a.f[0], a.f[1], a.f[2], a.f[3]};
|
|
bin.append(reinterpret_cast<const char*>(v), sizeof(v));
|
|
}
|
|
|
|
StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_32f, false, 16};
|
|
return format;
|
|
}
|
|
}
|
|
|
|
void compressVertexStream(std::string& bin, const std::string& data, size_t count, size_t stride, int level)
|
|
{
|
|
assert(data.size() == count * stride);
|
|
|
|
std::vector<unsigned char> compressed(meshopt_encodeVertexBufferBound(count, stride));
|
|
size_t size = meshopt_encodeVertexBufferLevel(&compressed[0], compressed.size(), data.c_str(), count, stride, level, level > 0);
|
|
|
|
bin.append(reinterpret_cast<const char*>(&compressed[0]), size);
|
|
}
|
|
|
|
void compressIndexStream(std::string& bin, const std::string& data, size_t count, size_t stride)
|
|
{
|
|
assert(stride == 2 || stride == 4);
|
|
assert(data.size() == count * stride);
|
|
assert(count % 3 == 0);
|
|
|
|
std::vector<unsigned char> compressed(meshopt_encodeIndexBufferBound(count, count));
|
|
size_t size = 0;
|
|
|
|
if (stride == 2)
|
|
size = meshopt_encodeIndexBuffer(&compressed[0], compressed.size(), reinterpret_cast<const uint16_t*>(data.c_str()), count);
|
|
else
|
|
size = meshopt_encodeIndexBuffer(&compressed[0], compressed.size(), reinterpret_cast<const uint32_t*>(data.c_str()), count);
|
|
|
|
bin.append(reinterpret_cast<const char*>(&compressed[0]), size);
|
|
}
|
|
|
|
void compressIndexSequence(std::string& bin, const std::string& data, size_t count, size_t stride)
|
|
{
|
|
assert(stride == 2 || stride == 4);
|
|
assert(data.size() == count * stride);
|
|
|
|
std::vector<unsigned char> compressed(meshopt_encodeIndexSequenceBound(count, count));
|
|
size_t size = 0;
|
|
|
|
if (stride == 2)
|
|
size = meshopt_encodeIndexSequence(&compressed[0], compressed.size(), reinterpret_cast<const uint16_t*>(data.c_str()), count);
|
|
else
|
|
size = meshopt_encodeIndexSequence(&compressed[0], compressed.size(), reinterpret_cast<const uint32_t*>(data.c_str()), count);
|
|
|
|
bin.append(reinterpret_cast<const char*>(&compressed[0]), size);
|
|
}
|