- 图形学
- 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 开源仓库中的比特流规范文档 docs/spec/prediction.normal.transform.md 为核心,完整拆解法线属性预测解码所使用的「规范化八面体变换」(Prediction Scheme Normal Octahedron Canonicalized Transform):从ModMax、InvertDiamond、GetRotationCount、RotatePoint、IsInBottomLeft五个辅助函数,到ComputeOriginalValue的完整解码主流程,并结合 normal_compression_utils.h 中的OctahedronToolBox、编码端变换实现与单元测试,说明该变换为何能提升熵编码效率,以及它在 Draco 法线解码流水线中的具体位置。读完本文,你将能够从源码与规范两个层面理解 Draco 法线属性的量化、预测、校正与反量化全过程,并可据此自行实现或移植兼容的解码器。
一、背景:为什么要对法线做规范化八面体变换
Draco 在压缩网格法线(normal attribute)时,并不会直接存储三个浮点分量,而是先把单位法线向量转换为八面体坐标(octahedral coordinates),再进行量化。这一做法的原理在 normal_compression_utils.h 的头部注释中有详细说明:
将一个八面体内切于球体,法线向量的方向可由八面体表面上的一个点表示。右半球(x > 0)的点被投影到 x = 0 平面,形成菱形;左半球的点同样投影,但将菱形外侧的三角形向外翻折,最后平移得到一个全为正值(
<0, max_value>范围)的方形。
其结果是:三维单位向量被压缩成两个整数坐标 (s, t)。相关关键量定义如下:
- q:量化位数(quantization bits),合法范围为 2~30;
- max_quantized_value:q 位能表示的最大值
(1 << q) - 1,必须为奇数(因为瓦片不是周期性的、外边缘不可识别,每轴需要奇数个值,见 prediction_scheme_normal_octahedron_canonicalized_encoding_transform.h); - max_value:菱形的最大取值
max_quantized_value - 1,为偶数; - center_value:平移后菱形的中心
max_value / 2。
以上四个量的换算在OctahedronToolBox::SetQuantizationBits()(normal_compression_utils.h)与规范的ComputeOriginalValue伪代码中完全一致:
quantization_bits_ = MostSignificantBit(encoded_max_quantized_value) + 1 max_quantized_value_ = (1 << quantization_bits_) - 1 max_value_ = max_quantized_value_ - 1 center_value_ = max_value_ / 2以 prediction_scheme_normal_octahedron_canonicalized_transform_test.cc 的Interface测试为例:Transform(15)对应的max_quantized_value == 15、center_value == 7、quantization_bits == 4,与上述公式完全吻合。
二、变换的整体设计思想(编码端视角)
规范化八面体变换的动机,可以从编码端变换头文件 prediction_scheme_normal_octahedron_canonicalized_encoding_transform.h 的注释中读到:
- 八面体坐标方形被划分为中心菱形(4 个内三角形,对应八面体上半部分)与四个外三角形(对应下半部分);
- 给定预测值 P 与待编码的真实值 Q,变换首先检查 P 是否位于菱形之外;若是,则把外三角形向内翻折(
InvertDiamond),使得校正向量更短; - 随后检查 P 是否位于左下象限;若不是,则对 P 和 Q 一起做旋转(
RotatePoint),使所有长校正值都变为正值,从而缩小校正值的取值范围、增大正向大校正值的出现频率,最终提升熵编码(Draco 使用 RANS)的压缩率; - 这一切之所以可行,是因为解码端同样知道预测值 P,因此可以完全镜像地执行翻转与旋转,并在解码完成后逆向还原。
简言之,该变换的目标是让送入熵编码器的校正值分布更集中、范围更小、更偏向正值,这是法线属性压缩率优于直接存储差值的关键。
三、五个辅助函数逐一解析(规范原文 + 源码印证)
本节逐字继承规范文档 docs/spec/prediction.normal.transform.md 中的五个辅助函数,并给出对应的 C++ 实现位置。
3.1 ModMax():模折叠,把校正值折叠回合法区间
规范中的伪代码:
int32_t ModMax(x, center_value_, max_quantized_value_) { if (x > center_value_) return x - max_quantized_value_; if (x < -center_value_) return x + max_quantized_value_; return x; }其 C++ 实现位于OctahedronToolBox::ModMax()(normal_compression_utils.h)。该函数作用于解码端的校正值:由于八面体坐标空间具有周期性(例如(0,0) == (max_value, max_value),见 normal_compression_utils.h),预测值与真实值相加后可能越出[-center_value_, center_value_]的合法区间,ModMax通过加减max_quantized_value_将其折回,等价于取模运算。
3.2 InvertDiamond():菱形内外翻折
规范中的伪代码:
void InvertDiamond(s, t, center_value_) { sign_s = 0; sign_t = 0; if (s >= 0 && t >= 0) { sign_s = 1; sign_t = 1; } else if (s <= 0 && t <= 0) { sign_s = -1; sign_t = -1; } else { sign_s = (s > 0) ? 1 : -1; sign_t = (t > 0) ? 1 : -1; } corner_point_s = sign_s * center_value_; corner_point_t = sign_t * center_value_; s = 2 * s - corner_point_s; t = 2 * t - corner_point_t; if (sign_s * sign_t >= 0) { temp = s; s = -t; t = -temp; } else { temp = s; s = t; t = temp; } s = (s + corner_point_s) / 2; t = (t + corner_point_t) / 2; }其 C++ 实现位于OctahedronToolBox::InvertDiamond()(normal_compression_utils.h)。它把中心菱形外部的点翻折进菱形内部(编码端),或把菱形内部的点翻折到外部(解码端还原)。源码实现还专门用无符号整数运算避免坏数据引起的有符号整数溢出(见注释 "Perform the addition and subtraction using unsigned integers to avoid signed integer overflows for bad data"),这是规范伪代码未体现的工程细节。
3.3 GetRotationCount():按象限确定旋转次数
规范中的伪代码(完整的 9 分支判断):
void GetRotationCount(pred, count) { sign_x = pred[0]; sign_y = pred[1]; rotation_count = 0; if (sign_x == 0) { if (sign_y == 0) rotation_count = 0; else if (sign_y > 0) rotation_count = 3; else rotation_count = 1; } else if (sign_x > 0) { if (sign_y >= 0) rotation_count = 2; else rotation_count = 1; } else { if (sign_y <= 0) rotation_count = 0; else rotation_count = 3; } count = rotation_count; }其 C++ 实现位于PredictionSchemeNormalOctahedronCanonicalizedTransformBase::GetRotationCount()(prediction_scheme_normal_octahedron_canonicalized_transform_base.h)。旋转次数对应四个象限:右上 → 2、左上 → 3、右下 → 1、左下 → 0(左下不旋转,作为"规范"基准)。测试文件 的GetRotationCount用例逐一验证了这 9 种输入组合。
3.4 RotatePoint():对点执行 0~3 次顺时针旋转
规范中的伪代码:
void RotatePoint(p, rotation_count, out_p) { switch (rotation_count) { case 1: out_p.push_back(p[1]); out_p.push_back(-p[0]); return; case 2: out_p.push_back(-p[0]); out_p.push_back(-p[1]); return; case 3: out_p.push_back(-p[1]); out_p.push_back(p[0]); return; default: out_p.push_back(p[0]); out_p.push_back(p[1]); return; } }其 C++ 实现位于PredictionSchemeNormalOctahedronCanonicalizedTransformBase::RotatePoint()(prediction_scheme_normal_octahedron_canonicalized_transform_base.h)。旋转次数 1/2/3 分别对应顺时针旋转 90°/180°/270°。
3.5 IsInBottomLeft():判断是否位于左下象限
规范中的伪代码:
bool IsInBottomLeft(p) { if (p[0] == 0 && p[1] == 0) return true; return (p[0] < 0 && p[1] <= 0); }其 C++ 实现位于PredictionSchemeNormalOctahedronCanonicalizedTransformBase::IsInBottomLeft()(prediction_scheme_normal_octahedron_canonicalized_transform_base.h)。注意原点(0,0)被特殊处理为"在左下象限",这意味着当预测值位于原点时不做旋转,这也是 测试用例 中IsInBottomLeft(Point2(0,0))返回true的原因。
四、核心主流程:ComputeOriginalValue 的解码步骤拆解
规范文档给出了两个层次的入口函数,二者是同一逻辑的外层包装与内层核心。
4.1 ComputeOriginalValue():参数重建与入口
void ComputeOriginalValue(pred_vals, corr_vals, out_orig_vals) { encoded_max_quantized_value = pred_trasnform_normal_max_q_val[curr_att_dec][curr_att]; quantization_bits_ = MostSignificantBit(encoded_max_quantized_value) + 1; max_quantized_value_ = (1 << quantization_bits_) - 1; max_value_ = max_quantized_value_ - 1; center_value_ = max_value_ / 2; pred.push_back(pred_vals[0]); pred.push_back(pred_vals[1]); corr.push_back(corr_vals[0]); corr.push_back(corr_vals[1]); ComputeOriginalValue2(pred, corr, &orig, center_value_, max_quantized_value_); out_orig_vals[0] = orig[0]; out_orig_vals[1] = orig[1]; }这一层先从比特流解析出的pred_trasnform_normal_max_q_val恢复量化参数(q、max_quantized_value、max_value、center_value),再调用内层核心函数。其 C++ 实现对应 prediction_scheme_normal_octahedron_canonicalized_decoding_transform.h 的ComputeOriginalValue(pred_vals, corr_vals, out_orig_vals)公有接口。
4.2 ComputeOriginalValue2():逐步还原真实量化坐标
void ComputeOriginalValue2(pred_in, corr, out, center_value_, max_quantized_value_) { t.assign(2, center_value_); SubtractVectors(pred_in, t, &pred); // ① 平移:以菱形中心为原点 pred_is_in_diamond = Abs(pred[0]) + Abs(pred[1]) <= center_value_; if (!pred_is_in_diamond) { // ② 预测点不在菱形内 → 翻折 InvertDiamond(&pred[0], &pred[1], center_value_); } pred_is_in_bottom_left = IsInBottomLeft(pred); GetRotationCount(pred, &rotation_count); if (!pred_is_in_bottom_left) { // ③ 不在左下象限 → 旋转预测点 RotatePoint(pred, rotation_count, &temp_rot); for (i = 0; i < temp_rot.size(); ++i) pred[i] = temp_rot[i]; } AddVectors(pred, corr, &orig); // ④ 预测 + 校正 = 旋转坐标系下的真实值 orig[0] = ModMax(orig[0], center_value_, max_quantized_value_); orig[1] = ModMax(orig[1], center_value_, max_quantized_value_); if (!pred_is_in_bottom_left) { // ⑤ 逆向旋转 reverse_rotation_count = (4 - rotation_count) % 4; RotatePoint(orig, reverse_rotation_count, &temp_rot); for (i = 0; i < temp_rot.size(); ++i) orig[i] = temp_rot[i]; } if (!pred_is_in_diamond) { // ⑥ 若当初翻折过,再翻折还原 InvertDiamond(&orig[0], &orig[1], center_value_); } AddVectors(orig, t, out); // ⑦ 平移回正坐标系 }对应 C++ 私有实现见 prediction_scheme_normal_octahedron_canonicalized_decoding_transform.h 中的ComputeOriginalValue(Point2 pred, Point2 corr)。两处流程完全一致,需要注意的工程差异是:
- 解码端使用
AddAsUnsigned(pred[0], corr[0])(prediction_scheme_normal_octahedron_canonicalized_decoding_transform.h)进行无符号加法,避免坏数据导致的有符号溢出; - 该函数的开头还有一组
DRACO_DCHECK_LE断言(prediction_scheme_normal_octahedron_canonicalized_decoding_transform.h),在调试构建中校验预测值与校正值都落在[0, 2*center_value]区间内。
4.3 与编码端 ComputeCorrection 的对称性
编码端ComputeCorrection(prediction_scheme_normal_octahedron_canonicalized_encoding_transform.h)与解码端完全对称:
orig = orig - t; pred = pred - t; if (!IsInDiamond(pred)) { InvertDiamond(&orig); InvertDiamond(&pred); } // 内外翻折 if (!IsInBottomLeft(pred)) { rotation_count = GetRotationCount(pred); orig = RotatePoint(orig, rotation_count); pred = RotatePoint(pred, rotation_count); } // 旋转 corr = orig - pred; corr = MakePositive(corr); // 转为正校正值其中MakePositive(normal_compression_utils.h)将负校正值加max_quantized_value转为正值;解码端则通过ModMax+AddAsUnsigned完成逆运算。编码端EncodeTransformData(prediction_scheme_normal_octahedron_canonicalized_encoding_transform.h)会把max_quantized_value与center_value写入比特流,供解码端重建参数。
五、变换数据在比特流中的解析(DecodeTransformData)
规范化八面体变换的变换参数在解码流水线中的读取位置,见 docs/spec/prediction.decoder.md 的DecodeTransformData()与ParseNormalOctahedronCanonicalizedTransformData():
void ParseNormalOctahedronCanonicalizedTransformData() { pred_trasnform_normal_max_q_val[curr_att_dec][curr_att] I32 unused_center_value I32 }即比特流中顺序写入两个 32 位有符号整数:
| 字段 | 类型 | 含义 |
|---|---|---|
pred_trasnform_normal_max_q_val | I32 | 最大量化值(1<<q)-1,解码端据此重建全部参数 |
unused_center_value | I32 | 中心值(解码端可忽略,因为可由 max 值推导) |
变量含义可参考 docs/spec/variable.descriptions.md(pred_trasnform_normal_max_q_val、pred_transform_normal_flip_normal_bits、seq_int_att_dec_symbols_to_signed_ints等条目)。C++ 侧对应PredictionSchemeNormalOctahedronCanonicalizedDecodingTransform::DecodeTransformData()(prediction_scheme_normal_octahedron_canonicalized_decoding_transform.h):读取max_quantized_value与center_value后校验quantization_bits必须在 [2, 30] 区间(对模糊测试产生的坏数据进行防御)。此外,set_max_quantized_value(prediction_scheme_normal_octahedron_transform_base.h)还会校验max_quantized_value必须为奇数,否则拒绝。
六、在法线解码流水线中的完整位置
变换不是孤立存在的,它位于几何法线预测解码器(Mesh Prediction Scheme Geometric Normal Decoder)的末端。完整链路见 docs/spec/prediction.normal.decoder.md 的MeshPredictionSchemeGeometricNormalDecoder_ComputeOriginalValues():
- 取校正符号:从
seq_int_att_dec_symbols_to_signed_ints取回已解码符号并转为带符号整数; - 重建量化参数:从
pred_trasnform_normal_max_q_val推出 q、max_quantized_value、max_value、center_value; - 几何法线预测:
MeshPredictionSchemeGeometricNormalPredictorArea_ComputePredictedValue()以当前 corner 为中心,沿左/右 Swing 遍历相邻三角形,累加叉积得到面积加权法线预测(normal),并在绝对值和超过1 << 29时做标量除法降幅; - 向量规范化:
CanonicalizeIntegerVector()(即OctahedronToolBox::CanonicalizeIntegerVector,见 normal_compression_utils.h)把预测 3D 法线缩放为绝对值之和恰等于center_value_的整数向量; - 可选翻转:若
flip_normal_bits[data_id]为真(该标志由 docs/spec/prediction.decoder.md 的DecodePredictionData_GeometricNormal()经 RANS 解码得到),对预测法线取反,以处理双面法线方向歧义; - 向量转八面体坐标:
IntegerVectorToQuantizedOctahedralCoords()把 3D 整数向量映射为量化 (s, t)(见 normal_compression_utils.h); - 调用本文的变换:
PredictionSchemeNormalOctahedronCanonicalizedDecodingTransform_ComputeOriginalValue()把 (s, t) 预测值与校正值还原为原始量化坐标; - 反量化输出:解码出的原始量化坐标最终由 docs/spec/sequential.normal.attribute.decoder.md 的
QuantizedOctaherdalCoordsToUnitVector()/TransformAttributesToOriginalFormat_Normal()反量化为单位法线浮点向量(对应 C++ 实现 sequential_normal_attribute_decoder.cc 的StoreValues→InverseTransformAttribute)。
C++ 侧的调度关系与之对应:SequentialNormalAttributeDecoder(sequential_normal_attribute_decoder.cc)仅接受3 分量、DT_FLOAT32的法线属性,法线属性解码器类型为SEQUENTIAL_ATTRIBUTE_ENCODER_NORMALS(见 docs/spec/variable.descriptions.md),预测方法为MESH_PREDICTION_GEOMETRIC_NORMAL(值为 6),变换类型为PREDICTION_TRANSFORM_NORMAL_OCTAHEDRON_CANONICALIZED(值为 3,见 docs/spec/variable.descriptions.md)。
七、正确性验证:单元测试与数值示例
仓库为本文所有关键行为提供了可运行的单元测试,见 prediction_scheme_normal_octahedron_canonicalized_transform_test.cc:
Init:Transform(15)构造后AreCorrectionsPositive()为真——校正值恒为正,印证了"旋转使长校正值全部为正"的设计目标;IsInBottomLeft:覆盖 9 组象限/原点输入;GetRotationCount:覆盖 9 种符号组合与预期旋转次数;RotateRepresentation:验证编码端RotatePoint对原点值与其预测值施加相同旋转的等价性;ComputeCorrection:用(o, p) → (cx, cy)的数十组用例,验证菱形内外、四象限各组合的校正值计算(断言corr == (c + 15) % 15,即校正值以 15 为模取正);Interface:验证max_quantized_value == 15、center_value == 7、quantization_bits == 4。
以TestComputeCorrection(transform, 3, 4, 1, 2, -2, -2)为例:真实值 (3,4)、预测值 (1,2) 均在菱形内(|1|+|2|=3 ≤ 7),预测点位于右上象限,旋转次数为 2(180°),旋转后校正值为 (-2,-2),MakePositive后仍为正值。解码端执行ModMax与反向旋转后即还原 (3,4),编码—解码闭环一致。这类用例从数值上证明了 预测.normal.transform.md 中伪代码与 C++ 实现的行为完全等价。
八、实现兼容解码器时的关键注意点
若需要依据本文与规范自行实现兼容的 Draco 法线解码器,请特别留意以下几点:
- max_quantized_value 必须为奇数(形如
2^b - 1),center_value = (max_quantized_value - 1) / 2;解码端只需从流中读取该值即可推导其余参数; - 校正值一律为正(编码端经
MakePositive处理),解码端使用ModMax折叠并配合无符号加法还原,切勿直接做有符号相减; - InvertDiamond 与旋转的先后顺序不可调换:必须先菱形翻折、后象限旋转(解码还原时反向执行),这是编解码对称性的前提;
- 原点 (0,0) 属于左下象限,其旋转次数为 0,这是
IsInBottomLeft的特殊分支,测试用例已显式覆盖; - 量化位数合法性范围 [2, 30],解码端必须校验,防止模糊输入导致移位溢出;
- 该变换仅用于3 分量、浮点法线属性,且需要配合
MESH_PREDICTION_GEOMETRIC_NORMAL预测与flip_normal_bits标志位使用,脱离该流水线单独调用没有意义。
九、小结
规范化八面体法线预测变换是 Draco 法线压缩质量的关键一环:它在八面体量化的基础上,通过「菱形内外翻折 + 象限旋转」两个几何操作,把校正值分布整形为更集中、更偏正值的形态,从而显著提升 RANS 熵编码的效率。本文从规范文档 docs/spec/prediction.normal.transform.md 的七个函数出发,逐一对齐了仓库中的 C++ 实现(prediction_scheme_normal_octahedron_canonicalized_transform_base.h、prediction_scheme_normal_octahedron_canonicalized_decoding_transform.h、normal_compression_utils.h),并贯通了从比特流解析(docs/spec/prediction.decoder.md)、几何法线预测(docs/spec/prediction.normal.decoder.md)到浮点反量化(docs/spec/sequential.normal.attribute.decoder.md)的完整解码链路。如需深入编码端行为或参与调试,可进一步研读编码变换头文件与其配套单元测试。
- 图形学
- 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 几何法线预测解码器深入解析:MeshPredictionSchemeGeometricNormalDecoder 的面积加权法线与八面体坐标还原算法
Draco 几何法线预测解码器深入解析:MeshPredictionSchemeGeometricNormalDecoder 的面积加权法线与八面体坐标还原算法
图形学3D渲染Draco 顺序法线属性解码器(SequentialNormalAttributeDecoder)解析:八面体量化法线从比特流到单位向量的完整还原
Draco 顺序法线属性解码器(SequentialNormalAttributeDecoder)解析:八面体量化法线从比特流到单位向量的完整还原 导读 本文基
图形学3D渲染深入解析BlurHash算法原理与DCT变换
深入解析BlurHash算法原理与DCT变换 本文深入探讨了BlurHash算法中离散余弦变换 DCT 的核心作用及其实现原理。DCT通过将图像从空间域转换到频
图像处理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考