- 图形学
- 3D渲染
【免费下载链接】draco
Draco is a library for compressing and decompressing 3D geometric meshes and point clouds. It is intended to improve the storage and transmission of 3D graphics.
导读
本文围绕 Draco 开源 3D 几何网格与点云压缩库的官方格式规范文档 docs/spec/metadata.decoder.md,系统讲解.drc比特流中元数据(Metadata)的解码过程与二进制布局。元数据是 Draco 流中用于携带"几何体之外的说明性信息"(如顶点属性名称、单位、自定义键值对、嵌套子元数据)的可选部分,理解其解码器实现,是开发自定义解码器、调试 .drc 文件、或为 Draco 扩展元数据能力的基础。读完本文,你将掌握元数据解码的完整调用流程、每一字段的位级编码方式(varint / UI8 / I8)、键值对与嵌套结构的还原规则,以及仓库中对应的源码与测试证据。
一、元数据在 Draco 解码流程中的位置
Draco 的解码总入口Decode()按固定顺序处理比特流:解析文件头(ParseHeader)→ 若标志位含METADATA_FLAG_MASK则调用DecodeMetadata()→ 解码连通性数据 → 解码属性数据(见 docs/spec/draco.decoder.md):
void Decode() { ParseHeader(); if (flags & METADATA_FLAG_MASK) DecodeMetadata(); DecodeConnectivityData(); DecodeAttributeData(); }这说明元数据解码是可选阶段:只有当头部 flags 设置了元数据标志位时,比特流中才存在元数据块。元数据不参与几何体本身的压缩,而是作为一种自包含的旁路信息块存在,解码失败通常只会导致Status::DRACO_ERROR,而不会破坏后续几何数据解析的字节对齐(因为编码与解码顺序严格对称)。
二、解码器整体架构与伪代码骨架
2.1 顶层过程 DecodeMetadata()
规范文档给出的顶层伪代码如下:
void DecodeMetadata() { ParseMetadataCount(); for (i = 0; i < num_att_metadata; ++i) { ParseAttributeMetadataId(i); DecodeMetadataElement(att_metadata[i]); } DecodeMetadataElement(file_metadata); }即元数据块由两部分串联构成:
- 属性元数据区:先读取属性元数据个数
num_att_metadata,再逐个读取每个属性元数据的唯一 ID,并递归解码其内容; - 文件级元数据区:紧接着解码一个属于整个几何体(point cloud / mesh)的"文件元数据"。
这与仓库中MetadataDecoder::DecodeGeometryMetadata()的实现完全对应(src/draco/metadata/metadata_decoder.cc):
bool MetadataDecoder::DecodeGeometryMetadata(DecoderBuffer *in_buffer, GeometryMetadata *metadata) { buffer_ = in_buffer; uint32_t num_att_metadata = 0; if (!DecodeVarint(&num_att_metadata, buffer_)) return false; for (uint32_t i = 0; i < num_att_metadata; ++i) { uint32_t att_unique_id; if (!DecodeVarint(&att_unique_id, buffer_)) return false; std::unique_ptr<AttributeMetadata> att_metadata(new AttributeMetadata()); att_metadata->set_att_unique_id(att_unique_id); if (!DecodeMetadata(static_cast<Metadata *>(att_metadata.get()))) return false; metadata->AddAttributeMetadata(std::move(att_metadata)); } return DecodeMetadata(static_cast<Metadata *>(metadata)); }注意伪代码中的ParseMetadataCount()与ParseAttributeMetadataId()在真实实现中合并进了同一个函数:属性元数据个数与每个属性元数据的唯一 ID 均以 varUI32(varint 编码的 uint32)读取。AttributeMetadata的att_unique_id必须与点云中对应属性的唯一 ID 一致(src/draco/metadata/geometry_metadata.h),解码后的属性元数据通过AddAttributeMetadata()挂到GeometryMetadata下。
2.2 顶层调用的真实入口
在点云解码器中,PointCloudDecoder::DecodeMetadata()会创建GeometryMetadata,调用MetadataDecoder::DecodeGeometryMetadata(),成功后通过point_cloud_->AddMetadata()挂载到解码结果上(src/draco/compression/point_cloud/point_cloud_decoder.cc)。整个调用链为:
PointCloudDecoder::Decode() └─> PointCloudDecoder::DecodeMetadata() └─> MetadataDecoder::DecodeGeometryMetadata() ├─> MetadataDecoder::DecodeMetadata() // 每个属性元数据 └─> MetadataDecoder::DecodeMetadata() // 文件级元数据 └─> DecodeEntry() / DecodeName()三、字段级位级布局:每种类型如何编码
规范文档中多次出现三类类型标注,其含义如下:
| 标注 | 含义 |
|---|---|
varUI32 | varint 编码的无符号 32 位整数(长度可变,小数值占用更少字节) |
UI8 | 无符号 8 位整数(固定 1 字节) |
I8[sz] | sz个有符号 8 位整数(即sz字节的原始数据) |
3.1 varint 编码细节
varUI32的编解码实现在 src/draco/core/varint_encoding.h 与 src/draco/core/varint_decoding.h 中。规则为:每个字节低 7 位存放数据,最高位(bit 7)为 1 表示"还有后续字节",为 0 表示"本字节是最后一个";解码时按从最高有效字节到最低有效字节的顺序递归读取并左移 7 位拼装。因此:
- 值
0 ~ 127:只占 1 字节; - 值
128 ~ 16383:占 2 字节; - 依此类推,
uint32最大占用 5 字节。
这正是"元数据条目数、键长度、值长度等计数都用 varint"的原因——绝大多数元数据条目数量都很小,用 varint 可以显著节省空间。
3.2 键与值的长度前缀规则
规范中键/子元数据键的长度用UI8(1 字节无符号整数)表示,因此单个键名最长 255 字节。这一点在编码器侧有明确约束(src/draco/metadata/metadata_encoder.cc):
bool MetadataEncoder::EncodeString(EncoderBuffer *out_buffer, const std::string &str) { // We only support string of maximum length 255 which is using one byte to // encode the length. if (str.size() > 255) return false; ... }对应解码器DecodeName()(src/draco/metadata/metadata_decoder.cc)读取 1 字节长度后,再按长度读取原始字节。空字符串编码为单字节0x00。
四、核心过程逐个拆解
4.1 ParseMetadataCount()
void ParseMetadataCount() { num_att_metadata varUI32 }读取属性元数据的总个数。伪代码中用独立过程表达,实际实现中是DecodeGeometryMetadata()开头的第一个DecodeVarint(&num_att_metadata, buffer_)调用。
4.2 ParseAttributeMetadataId()
void ParseAttributeMetadataId(index) { att_metadata_id[index] varUI32 }按索引依次读取每个属性元数据对应的属性唯一 ID(varUI32),并据此创建带att_unique_id的AttributeMetadata对象。
4.3 ParseMetadataElement():键值对与子元数据计数
void ParseMetadataElement(metadata) { metadata.num_entries varUI32 for (i = 0; i < metadata.num_entries; ++i) { sz = metadata.key_size[i] UI8 metadata.key[i] I8[sz] sz = metadata.value_size[i] UI8 metadata.value[i] I8[sz] } metadata.num_sub_metadata varUI32 }这一段定义了单个元数据对象的二进制结构,可分为三部分:
- 条目数:
num_entries(varUI32),即本元数据中键值对的个数; - 条目序列:对每个条目,先写键名长度(UI8)+ 键名字节(I8[sz]),再写值长度(UI8)+ 值字节(I8[sz])。注意规范中键和值的长度都用 UI8,但真实实现中值长度使用的是 varint(见下节 4.5),且值被整体视为二进制 blob 存储;
- 子元数据个数:
num_sub_metadata(varUI32),用于后续递归解码嵌套元数据。
在仓库中,这一阶段对应MetadataDecoder::DecodeMetadata()的主体循环(src/draco/metadata/metadata_decoder.cc):
uint32_t num_entries = 0; if (!DecodeVarint(&num_entries, buffer_)) return false; for (uint32_t i = 0; i < num_entries; ++i) { if (!DecodeEntry(metadata)) return false; } uint32_t num_sub_metadata = 0; if (!DecodeVarint(&num_sub_metadata, buffer_)) return false; if (num_sub_metadata > buffer_->remaining_size()) return false; for (uint32_t i = 0; i < num_sub_metadata; ++i) { metadata_stack.push_back({metadata, nullptr, ...}); }值得注意的健壮性细节:num_sub_metadata解码后会与buffer_->remaining_size()比较,若子元数据数量异常偏大(超过剩余字节数)则直接返回 false——这是一种防御性校验,防止恶意或损坏的比特流触发大量无效分配。
4.4 ParseSubMetadataKey():嵌套键
void ParseSubMetadataKey(metadata, index) { sz = metadata.sub_metadata_key_size[index] UI8 metadata.sub_metadata_key[index] I8[sz] }每个子元数据都带有一个键名(1 字节长度 + 字节串)。解码器读取该名称后,通过parent_metadata->AddSubMetadata(sub_metadata_name, ...)把新解析出的子元数据挂到父元数据下。若同名子元数据已存在,AddSubMetadata()返回 false(src/draco/metadata/metadata.cc),解码即失败。
4.5 DecodeMetadataElement():递归解码嵌套结构
void DecodeMetadataElement(metadata) { ParseMetadataElement(metadata); for (i = 0; i < metadata.num_sub_metadata; ++i) { ParseSubMetadataKey(metadata, i); DecodeMetadataElement(metadata.sub_metadata[i]); } }这是元数据解码的递归核心:先解析当前元数据的条目与子元数据计数,然后对每个子元数据,先读其键名再递归解码其内容,形成一棵以文件级元数据为根、可任意嵌套的元数据树。
真实实现采用显式栈 + 迭代而非递归,原因在注释中写得很清楚:Limit metadata nesting depth to avoid stack overflow in destructor,嵌套深度上限为kMaxSubmetadataLevel = 1000(src/draco/metadata/metadata_decoder.cc)。每个栈帧记录(parent_metadata, decoded_metadata, level),子元数据入栈后统一出栈处理,层级超过 1000 即返回失败。这样既保持了与伪代码等价的解码顺序,又避免了深度嵌套导致的栈溢出。
4.6 DecodeEntry():单条键值对的还原
void DecodeEntry(metadata) { DecodeName(&entry_name); // UI8 长度 + 字节 DecodeVarint(&data_size); // 值长度(varint) if (data_size == 0) return false; if (data_size > buffer_->remaining_size()) return false; std::vector<uint8_t> entry_value(data_size); buffer_->Decode(&entry_value[0], data_size); metadata->AddEntryBinary(entry_name, entry_value); }(结构对应 src/draco/metadata/metadata_decoder.cc。)
这里有三个关键实现细节,与规范伪代码略有差异,以仓库源码为准:
- 键名:同样走
DecodeName()(UI8 长度前缀); - 值长度:规范写的是 UI8,实际代码用
DecodeVarint读取值长度(节省空间,也更灵活); - 值语义:规范写
I8[sz],实际代码把值作为原始字节缓冲读取(std::vector<uint8_t>),随后统一通过AddEntryBinary()存入元数据。所有类型(int、double、string、数组、二进制)在序列化时都先被压平为字节缓冲,解码端按EntryValue存储(src/draco/metadata/metadata.h)。
此外还有三重防御校验:data_size == 0返回 false(拒绝空值条目);data_size > buffer_->remaining_size()返回 false(防止越界读取);Decode失败同样返回 false。
五、解码侧数据模型:Metadata / EntryValue / AttributeMetadata
解码结果最终落在三类对象上:
Metadata:通用元数据容器,内部是std::map<std::string, EntryValue> entries_与std::map<std::string, std::unique_ptr<Metadata>> sub_metadatas_(src/draco/metadata/metadata.h)。提供AddEntryInt/IntArray/Double/DoubleArray/String/Binary及对应GetEntry*访问接口;同名条目会被覆盖,同名子元数据则拒绝添加。EntryValue:条目的值类型,内部是std::vector<uint8_t>字节缓冲,支持按 int/double/string/vector 等类型读取(模板GetValue<T>())。AttributeMetadata : Metadata:带att_unique_id的属性元数据,通过唯一 ID 与点云中的属性关联(src/draco/metadata/geometry_metadata.h);GeometryMetadata : Metadata则持有属性元数据列表att_metadatas_,并提供按唯一 ID 查询的接口(GetAttributeMetadataByUniqueId)。
解码完成后的典型使用方式:通过point_cloud->GetMetadata()获取GeometryMetadata,再调用GetAttributeMetadataByUniqueId(id)定位某个属性的元数据,用GetEntryString(name, &value)等接口读取具体键值。
六、编码端对照:保证可逆的关键
元数据解码的字节顺序并非任意约定,而是编码器的严格镜像。MetadataEncoder(src/draco/metadata/metadata_encoder.cc)的写顺序为:
EncodeGeometryMetadata():写属性元数据个数(varint)→ 对每个属性元数据写att_unique_id(varint)+ 其条目与子元数据 → 最后写文件级元数据;EncodeMetadata():写条目数(varint)→ 逐条目写键名(UI8 长度 + 字节)+ 值长度(varint)+ 值字节 → 写子元数据个数(varint)→ 逐子元数据写键名 + 递归内容。
解码端顺序与之完全对称,这也是MetadataEncoderTest能做"编码→解码→逐字段比对"的前提。测试用例覆盖了单条目、多类型条目(int/double/string)、数组条目、二进制条目、嵌套子元数据、带属性元数据的几何元数据等场景(src/draco/metadata/metadata_encoder_test.cc),例如:
TEST_F(MetadataEncoderTest, TestEncodingNestedMetadata) { metadata.AddEntryDouble("double", 1.234); std::unique_ptr<draco::Metadata> sub_metadata(new draco::Metadata()); sub_metadata->AddEntryInt("int", 100); metadata.AddSubMetadata("sub0", std::move(sub_metadata)); TestEncodingMetadata(); }TestEncodingMetadata()内部先EncodeMetadata写入EncoderBuffer,再用DecoderBuffer::Init()装载同一段字节流,调用DecodeMetadata()还原,最后递归比对条目字节与子元数据树(src/draco/metadata/metadata_encoder_test.cc)。
七、格式要点速查
| 数据 | 编码类型 | 说明 |
|---|---|---|
num_att_metadata | varUI32 | 属性元数据个数 |
att_metadata_id[i] | varUI32 | 第 i 个属性元数据对应的属性唯一 ID |
num_entries | varUI32 | 当前元数据键值对个数 |
key_size/key | UI8 + I8[sz] | 键名,长度 ≤ 255 字节 |
value_size/value | varint + 字节 | 值,按二进制 blob 存储 |
num_sub_metadata | varUI32 | 子元数据个数 |
sub_metadata_key_size/ key | UI8 + I8[sz] | 子元数据键名 |
八、局限性与兼容性说明
- 键名长度上限 255 字节是编码器强制约束(超出即编码失败),解码器按 UI8 读取与之匹配;
- 值长度在规范文档中写作 UI8,但当前仓库实现使用 varint,因此以仓库源码为准;
- 元数据嵌套深度上限 1000(
kMaxSubmetadataLevel),超限解码失败; - 只有文件头 flags 携带
METADATA_FLAG_MASK时,比特流中才包含元数据块;无元数据的 .drc 文件不会产生该部分; - 解码结果依赖点云/网格解码器在后续阶段按
att_unique_id正确关联属性,因此属性元数据 ID 必须与属性唯一 ID 一致。
结语
Draco 的元数据解码器在格式上只依赖三类原语(varint、UI8 长度前缀、原始字节串),在结构上是一棵可任意嵌套的键值对树,顶层由"属性元数据列表 + 文件级元数据"组成。规范文档 docs/spec/metadata.decoder.md 给出了完整的伪代码骨架,而仓库实现 src/draco/metadata/metadata_decoder.cc 在保持相同字节序的基础上,补充了迭代式解码、深度上限、剩余字节校验等健壮性措施。理解这一对称的编解码结构,是安全解析 .drc 比特流、为 Draco 增加自定义元数据能力或开发兼容实现的第一步。
- 图形学
- 3D渲染
【免费下载链接】draco
Draco is a library for compressing and decompressing 3D geometric meshes and point clouds. It is intended to improve the storage and transmission of 3D graphics.
相关推荐
Metadata as Code 规范详解:Knowledge Catalog(Dataplex)元数据即代码的工程设计
Metadata as Code 规范详解:Knowledge Catalog(Dataplex)元数据即代码的工程设计 导读 本文基于 knowledge c
数据目录AI Agent人工智能知识管理示例工程Alcatraz包格式详解:JSON规范与元数据结构
Alcatraz包格式详解:JSON规范与元数据结构 引言 作为Xcode的Package Manager(包管理器),Alcatraz的核心功能在于对各类扩展
开发工具RuboCop 0.35.0 升级指南:配置继承新玩法与一批新检测规则
RuboCop 0.35.0 升级指南:配置继承新玩法与一批新检测规则 团队想把统一的 RuboCop 配置打包成 gem 分发,但过去 .rubocop.ym
代码质量Lint格式化静态分析开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考