磁刻技术实录 · 第 23 篇 · 工程实践 · 技术基线 2026-10-31
我们正在做的「磁刻(Cike)」,是一款鸿蒙原生的生活记录与个性化创作应用。用户可以把照片做成数字冰箱贴,在洞洞板上自由摆放、组合自己的生活片段,也可以把喜欢的内容做成桌面卡片,用来回看、互动和分享。
在空间记录上,我们引入了 3D Gaussian Splatting(3DGS,三维高斯泼溅),把拍摄的视频重建为可以转动、靠近观察的三维场景。为此,我们重构了视频重建链路,实现自己的 C++ 训练核心,并结合鸿蒙原生的媒体处理、模型查看和分享能力,希望让用户保存一段经历时,也能留下当时的空间关系。
训练完成后,用户转动模型时仍然期待连续、及时的视觉反馈。我们分享实例化绘制、深度排序与透明混合,是为了解释磁刻查看器每一帧在做什么,以及减少提交与上传成本时哪些渲染关系必须保持正确。
查看器需要把高斯参数快速转换成屏幕画面。磁刻使用 GLES 实例化绘制,让每个高斯通过一组实例属性生成屏幕几何,而不是为每个点单独发起绘制调用。实例化降低了调用组织成本,但透明排序和片元覆盖仍需要认真处理。
以下代码按当前实现整理,保留关键运算与控制顺序;省略导入、日志和完整类型,部分辅助函数采用说明性命名,片段需结合上下文阅读。
这里讨论的是交互查看器。它的全局点排序与训练器的 Tile 列表具有不同组织方式,不能因为都使用深度顺序就认为两条管线完全等价。验证训练与显示一致性时,应固定模型和相机,逐项分析差异。
图 1:只在必要变化时重新整理数据
实例属性描述一个分布
一个高斯需要位置、颜色、不透明度和协方差信息。查看器把这些数据放入实例缓冲,使用属性步进让每个实例读取自己的记录。顶点阶段根据相机与形状生成覆盖椭圆的几何,片元阶段计算高斯权重和最终颜色。
字段布局必须与 CPU 结构一致。当前实现对记录大小和部分字段偏移进行静态检查,避免新增属性后,着色器仍按旧布局读取。此类错误可能表现为颜色和位置同时异常,比编译错误更难诊断。
透明度要求正确顺序
透明高斯的合成依赖前后关系。当前查看器根据相机方向排序,并使用与预乘颜色相匹配的混合方式。若颜色已经乘以 Alpha,却再次使用普通未预乘混合规则,就会额外衰减;反过来也会产生边缘异常。
高斯中心深度排序是一种工程近似。很大或强烈交叠的椭球可能存在更复杂的逐像素关系,因此看到局部透明层次误差时,需要同时检查排序近似和形状范围,而不能只增加混合精度。
并非所有相机变化都需要重排
在当前查看器的变换关系下,某些平移、靠近远离或扫描动画参数不会改变点之间的相对深度顺序。代码因此关注影响排序的旋转和模型变化,避免每帧无条件复制、排序与上传全部数据。
这个优化依赖具体相机模型。若未来引入其他变换、多个独立模型或不同排序策略,应重新验证哪些参数会改变顺序,不能把当前判断当作普遍图形规律。
排序只在相对深度关系可能改变时重做
if (model_changed || rotation_changed) {
sorted_points = points;
std::sort(sorted_points.begin(), sorted_points.end(),
[&](const Splat& a, const Splat& b) {
return EyeDepth(a) > EyeDepth(b); // 正深度下远到近
});
UploadInstanceBuffer(sorted_points);
}
UpdateViewUniforms();
DrawInstances();
示例将深度定义为正向距离,因此远到近使用大值在前;若直接使用 GLES 的负 z 值,比较方向要对应调整。不能只复制比较符号而不明确坐标含义。当前查看器通过旋转变化与模型变化决定排序,平移与沿视线移动通常不会改变点之间的相对深度顺序。
排序和上传一起减少,才能避免 CPU 排完序后每帧仍然搬运同一份数据。实例化绘制减少重复提交公共几何,但并没有消除 CPU 排序,也没有消除透明区域的片元开销;性能分析应分别衡量这些成本。
图 2:点数相同的模型也可能有不同绘制负担
上传与绘制要拆开测量
一次明显卡顿可能发生在 CPU 排序、缓冲上传或 GPU 片元处理。只统计整帧平均耗时,无法判断下一步应该减少排序、改变数据布局,还是控制大范围覆盖。分析应记录模型数量、实例记录大小、排序触发频率和屏幕覆盖。
高斯点数并不能完全描述渲染负担。大量小高斯与少量覆盖全屏的大高斯,可能有不同片元成本。透明重叠越多,过度绘制越明显;画面分辨率和视角也会改变结果。
验证视觉与性能同时成立
可以固定一份模型,分别执行旋转、平移和缩放,检查排序是否在应触发时发生、静止时是否仍重复上传。再比较不同视角的颜色与遮挡,确保优化没有让旧顺序被错误复用。
设备性能报告应保留机型、分辨率、模型点数、视角路径和持续时间。短时间最高帧率与长时间稳定交互不是同一个指标。当前代码结构可以支持这样的分析,但实际性能结论仍要来自对应设备记录。
预乘颜色需要匹配的混合因子
float q = dot(localCoord, localCoord);
float alpha = baseOpacity * exp(-0.5 * q);
outColor = vec4(baseColor * alpha, alpha);
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, instance_count);
片元输出已经把 RGB 乘过 Alpha,所以源颜色因子必须是 GL_ONE。若继续使用 GL_SRC_ALPHA,颜色会再乘一次 Alpha,半透明边缘容易发暗。反过来,若输出未预乘颜色却使用当前混合方式,也会得到错误亮度。
这里的局部坐标应由投影椭圆转换得到,不能把整个三维高斯简单替换为固定屏幕圆点。绘制使用一组公共四边形顶点,每个实例提供自己的位置、形状和颜色。混合是有顺序的,因此实例化并不意味着可以随意取消排序。
实例化绘制让高斯查看器具备清晰的执行结构。进一步优化需要围绕排序、传输和片元三个阶段展开,每次改变都应同时保留视觉正确性与可比较的测量条件。