磁刻技术实录 · 第 19 篇 · 工程实践 · 技术基线 2026-10-31
我们正在做的「磁刻(Cike)」,是一款鸿蒙原生的生活记录与个性化创作应用。用户可以把照片做成数字冰箱贴,在洞洞板上自由摆放、组合自己的生活片段,也可以把喜欢的内容做成桌面卡片,用来回看、互动和分享。
在空间记录上,我们引入了 3D Gaussian Splatting(3DGS,三维高斯泼溅),把拍摄的视频重建为可以转动、靠近观察的三维场景。为此,我们重构了视频重建链路,实现自己的 C++ 训练核心,并结合鸿蒙原生的媒体处理、模型查看和分享能力,希望让用户保存一段经历时,也能留下当时的空间关系。
用户离开重建页面后,三维记忆还需要被重新打开、上传或分享,因此模型文件必须保存足够清楚的含义。我们分享 PLY 的字段与坐标约定,是想说明怎样让训练器生成的结果在后续环节中保持同一种解释。
PLY 是容器格式,能够描述顶点属性,但“文件后缀相同”不代表两个模型使用相同语义。高斯模型可能把尺度存成对数,把透明度存成 logit,把颜色存成球谐系数。坐标轴和四元数顺序也可能不同。磁刻因此把 PLY 当作明确协议设计,而不是简单把参数数组写到磁盘。
以下代码按当前实现整理,保留关键运算与控制顺序;省略导入、日志和完整类型,部分辅助函数采用说明性命名,片段需结合上下文阅读。
当前产物保留查看器兼容的 17 个浮点字段:位置、法线占位、SH0 颜色、不透明度、三个尺度和四元数。法线写零,属于格式兼容字段;它们不表示模型已经具有可用于网格光照的真实表面法线。
图 1:几何、外观和参考相机需要一起转换和验证
坐标转换必须作用于整个分布
如果训练世界与显示世界采用不同轴约定,改变中心位置还不够。高斯的方向与协方差也必须跟随变换。对于正交变换 B,中心变成 B×mean,协方差变成 B×Σ×Bᵀ。只翻转位置而不改变旋转,会让椭球方向与结构不一致。
当前视频导出使用明确的轴变换,将训练参数转换到查看器的世界约定,并为参考相机生成对应外参。转换后的相机投影必须与转换前一致,这可以通过重投影回归直接验证,而不必依赖肉眼观察。
相机平移与相机中心不同
外参形式 Xc=R×Xw+t 中,t 是世界到相机变换的平移,不是相机在世界中的位置。相机中心为 -Rᵀt。两者混用经常导致模型初始视角奇怪,尤其在存在旋转时。元数据用 translationEncoding 明确解释,避免字段名称引发误读。
四元数同样必须声明排列与方向。世界到相机与相机到世界互为逆变换,不能只改字段顺序。序列化时保存足够精度,可以减少回读后旋转正交性和重投影误差。
来源标记让文件独立成立
自研模型写入 cike_gs_train_v1 与 cike_gs_camera_v1 注释,携带 RGB SH0、坐标约定和参考相机。模型被复制或分享后,不必依赖原目录中的临时相机清单,仍然能够识别正确颜色和初始观察信息。
这种设计还需要长度上限和版本检查。文件头不是可以无条件信任的任意字符串,读取器要限制扫描字节、校验数值范围,并对未知版本采取明确路径。来源标记帮助选择兼容策略,但不能替代所有字段验证。
写出字段前先确定每个数的含义
// 逻辑字段排列;实际输出需明确小端字节序,不能依赖结构体填充。
float values[17] = {
g.mean.x, g.mean.y, g.mean.z,
0, 0, 0, // 法线占位
g.sh_dc.x, g.sh_dc.y, g.sh_dc.z,
g.opacity_logit,
g.log_scale.x, g.log_scale.y, g.log_scale.z,
q.w, q.x, q.y, q.z
};
WriteFloat32LittleEndian(values, 17);
相同的 float 类型可以表达完全不同的空间。这里的 opacity 是 logit,scale 是对数,颜色是 SH0 系数,四元数按 w、x、y、z 排列。加载器如果把它们当作已经解码的数值,画面会严重失真,但文件长度和读取过程仍然可能完全正常。
PLY 头部的属性顺序必须与二进制数据一致。示例没有直接写 C++ 结构体,是因为结构体可能有对齐填充,也不能替代明确的字节序协议。自研文件中还携带来源与相机标记,使查看器可以选择匹配的解释方式。
图 2:字段存在不代表解释方式一致
二进制记录必须按声明解释
读取器应根据属性类型和顺序计算记录跨度与字段偏移,而不是假定任何 PLY 都具有相同内存布局。文件长度、顶点数量与记录大小需要相互核对,避免截断文件被当作完整模型。
往返测试可以构造旋转各向异性高斯,保存再读取,检查中心、尺度、颜色和透明度参数。进一步用固定相机渲染或重投影,验证语义是否保持。只比较字节数量无法发现通道交换、旋转方向或对数尺度解释错误。
协议演进需要保留兼容边界
将来加入高阶球谐、压缩字段或多参考相机时,应增加明确版本或能力标记,避免旧查看器误读。未知附加字段可以按格式规则跳过,但未知核心编码不能靠猜测。产物协议稳定后,训练器和查看器才能独立演进。
坐标转换需要同时作用于位置和形状
Mat3 basis = Diagonal(Vec3(1, -1, -1));
Vec3 output_mean = basis * input_mean;
Mat3 output_covariance = basis * input_covariance * Transpose(basis);
// 时间戳作为十进制文本写入元数据。
std::string timestamp_text = std::to_string(timestamp_ns);
WriteCameraMetadata(timestamp_text, camera_parameters);
这里用协方差形式说明基变换。实际序列化仍写尺度和旋转等参数,需要构造与变换后形状等价的表示。只翻转中心坐标、忘记椭球朝向,会让位置看似正确而形状方向错误,尤其在非球形高斯上明显。
纳秒时间戳可能超过 JavaScript 安全整数范围,所以用十进制字符串跨语言传递。字符串需要验证长度、字符和范围,再用于排序;不能先转成普通 Number 再转回字符串,那时低位精度可能已经丢失。元数据不是附属装饰,而是还原观察关系的一部分。
对于空间内容应用,模型文件本身就是产品交付物。让它携带足够语义、能够验证并可独立打开,才能支撑重新加载、跨目录复制和系统分享这些真实用户行为。