← 返回文章
#3DGS#坐标系#PlayCanvas#管线

3DGS 坐标系与 180°Z 约定:从 PLY 到引擎空间

同一份高斯点云在重建工具、离线管线与浏览器引擎之间坐标定义不同,本文整理我们在管线中落实的不变量与踩过的坑。

本文目录

为什么坐标会成为问题

3DGS 训练工具通常输出 COLMAP 风格的数据:Z 轴向上(Z-up),相机位姿以 world-to-camera 形式给出。浏览器中的渲染引擎则按 Y 轴向上(Y-up)组织场景。两者之间没有统一的“正确”方向,只要某一步少做了一次变换,场景就会表现为倒立、侧躺或镜像,而且很难从画面上一眼看出是哪一步出错。

我们遇到的问题大多属于这一类:同一份数据在 scene-studio 里看起来正常,经过离线管线后翻转,或者在查看器里碰撞体与模型对不上。

PLY 文件里隐含的约定

PLY 本身不记录坐标系。在 @playcanvas/splat-transform v3 中,PLY 数据被标记为绕 Z 轴旋转 180° 的变换空间(文档中称为 Transform.PLY)。也就是说,读入的高斯数据已经包含一层隐含的 180°Z 变换,写出时必须按相同约定还原,否则会整体镜像。

这个约定是库内部的惯例,并不写在文件头里,因此管线中每一处读写 PLY 的地方都需要明确处理它。

我们落实的不变量

scene-pipeline 的 AGENTS.md 中把坐标规则写成了明确的不变量,核心有三条:

  1. document TRS 是引擎空间:场景文档中记录的平移、旋转与缩放,已经处于引擎坐标系。
  2. 源数据的 meta.transform 含 PLY 的 180°Z 惯例:读入时需要把这层惯例与文档变换合并。
  3. 先左乘再烘焙:TRS 以左乘方式作用于 pending 变换,之后整体烘到 IDENTITY,再交给碰撞与 LOD 消费。

烘焙之后,下游模块不再关心原始坐标约定。这样碰撞、LOD 与打包三个分支看到的是同一份数据,不会出现某个分支多翻一次的情况。

此外还有一条纪律:不要在任何地方手工翻转坐标。每次看到“这里看起来反了,加个负号”的修改,都应当回到变换链路里找到漏掉的那一步。

四元数的数组顺序

管线内部统一使用 [x, y, z, w] 的四元数顺序,这与 PlayCanvas 的 Quat 原生顺序一致。很多第三方工具使用 [w, x, y, z],如果混用,旋转会表现为随机的轴向错误,而且不会报错。我们的做法是在边界处显式转换,并在测试中覆盖一个非对称的三维尖塔场景,用朝向回归来守住这一点。

Z-up 到 Y-up 的轴向校正

对于 COLMAP 产出的 PLY,常见做法是绕 X 轴旋转 +90°,把 Z-up 变为 Y-up。轴向校正 CLI 把这个角度暴露为 --base-rot-x(默认 90)与 --flip-updown-x 两个参数,实际旋转为两者之和。

在 v3 的实现中,库内部会对 PLY 数据做一次坐标空间的共轭补偿,因此用户看到的角度参数与旧版保持同一语义。如果你在日志中看到 Axis rotation (deg),那是最终生效的角度,遇到方向不对时以它为准调整。

同时,v3 的输出列顺序是 canonical 的(位置、旋转、缩放、不透明度、球谐系数),与输入列顺序可能不同。下游应当按属性名读取列,不要硬编码列下标。

怎么验证

我们的做法是用一个非对称的三维模型(比如带尖顶的塔)做回归:

  • 它的前后左右与上下都能一眼分辨,任何轴向错误都会被画面暴露。
  • 每次修改变换链路后,先跑管线生成产物,再用查看器的 ?scene= 方式加载,比较尖顶的朝向与地面的位置。
  • 碰撞体与可视模型需要分别检查,因为两者经过的变换分支不同。

小结

3DGS 的坐标问题本质上是“约定没有写在数据里”。解决办法是把约定写成显式的不变量,放在管线的固定位置处理,并用非对称场景做回归。不要依赖肉眼调整角度来掩盖缺失的变换步骤。

OPEN TO WORK

有类似的工程问题?

3DGS 数据处理、流式加载与查看器定制,欢迎交流或合作。