把带贴图的 OBJ / glTF 转成 3D Tiles:我写了个 mesh-to-tiles
做倾斜摄影或单体模型上 Cesium 时,经常卡在同一件事:手上是一份带贴图的 OBJ 或 glTF,场景里要的是 3D Tiles。现成工具能转,但 LOD、图集和纹理压缩往往要再串一圈。我把这条链路收成了一个 Node 命令行工具 mesh-to-tiles,输入网格,输出 3D Tiles 1.1 隐式八叉树。
仓库:github.com/Tao-tao520/...,协议 MIT。
它解决什么问题
目标很具体:
- 输入:带贴图的
.obj(含.mtl和map_Kd),或带底色贴图的.glb/.gltf。GLB 里可以已经是 Draco。 - 输出:
tileset.json+ 隐式八叉树瓦片。每个瓦片是一个 Draco 网格,外加一张重新烘焙的图集。 - 加载端:Cesium 一类支持 3D Tiles 1.1 的引擎,材质按 unlit、双面来。
层级按模型包围盒自动切。refine 用 REPLACE:粗层被细层整块替换,避免两层叠在一起发虚。
图集最细一层是 4096,从原图采样;往上每一级边长减半,最低 128。默认编码成 KTX2(ETC1S / Basis Universal)。Cesium 加载时再转成当前显卡的块压缩格式,传输和显存都比一堆大 PNG 轻。
一条命令里做了什么
流水线可以记成四步:
- 读网格。OBJ 或 glTF / GLB 读进来,带上底色贴图。
- 按包围盒做隐式八叉树 。最细格子默认边长 40 米,层数上限默认 6,最细层
geometricError默认 1 米。这些都能用参数改。 - 每个瓦片重烘焙一张图集。UV 岛按原贴图重排,不是简单把原图缩小塞进去。只有 4096 那一层按原图分辨率采样;更粗的层级先缩到 2048 再采样,避免每层都啃全分辨率。
- 压缩后写出 。几何用
draco3dgltf,符合KHR_draco_mesh_compression。图集默认 KTX2 ETC1S(质量 128,不带 mipmap),符合KHR_texture_basisu,编码走worker_threads。加上--no-ktx2就保持 PNG。
坐标有两套,写错了模型会躺倒,这里单独说清楚:
- 瓦片局部坐标是 Z 轴向上、单位米。
- 瓦片里的 glTF 按规范写成 Y 轴向上。Cesium 加载时会转回 Z 轴向上。
--up只说明输入模型 哪根轴朝上,不改输出方向。OBJ 默认z,glTF / GLB 默认y。- ODM 导出的 OBJ 和 GLB,顶点往往都是 Z 轴向上。OBJ 可以不写
--up;GLB 要写--up z。 --longitude、--latitude、--height是局部原点(0,0,0)的大地坐标,不会从网格里读 。--height是 WGS84 椭球高,不是离地高。顶点原来的 Z 会保留。经纬度要么一起传,要么都不传。
环境与本地运行
需要 Node.js 18 或更高。
bash
npm install
npm run build
node dist/cli.js --input model.obj --output ./tiles
带地理位置的例子:
bash
mesh-to-tiles --input model.glb --output ./tiles --longitude 109.40388330 --latitude 34.03154500 --height 0
也可以 npm link 之后直接用全局命令 mesh-to-tiles。
不想在目标机器上装 Node
工具链依赖 sharp 和 Basis 的 WASM,拷一个 node_modules 过去很容易缺文件。仓库里按平台各打一份,把运行时一起带上:
bash
npm run pack:win
npm run pack:linux
产物在 release/mesh-to-tiles-win-x64 和 release/mesh-to-tiles-linux-x64。目录里的 node、sharp 和 WASM 要一起拷贝,不要只拷脚本。Linux 包面向 glibc 系统,例如 Ubuntu。
Windows:
text
mesh-to-tiles.cmd --input model.obj --output ./tiles --longitude 109.40 --latitude 34.03 --height 0
Linux:
bash
chmod +x mesh-to-tiles
./mesh-to-tiles --input model.obj --output ./tiles --up z --longitude 109.40 --latitude 34.03 --height 0
参数一览
| 参数 | 说明 | 默认 |
|---|---|---|
--input |
.obj、.glb 或 .gltf |
必填 |
--output |
输出目录,每次运行会先清空 | 必填 |
| `--up z | y` | 输入模型的向上轴 |
--leaf-size |
最细格子边长,米 | 40 |
--leaf-error |
最细层级的 geometricError,米 |
1 |
--max-levels |
层级数上限 | 6 |
--longitude |
局部原点经度,度 | 不写 |
--latitude |
局部原点纬度,度 | 不写 |
--height |
局部原点椭球高,米 | 0 |
--no-ktx2 |
图集保持 PNG | 默认编码 KTX2 |
输出目录每次都会先清空,路径别指到还有别的文件的文件夹。
产物长什么样
text
tiles/
tileset.json
tiles/{level}/{x}/{y}/{z}.glb
subtrees/0/0/0/0.subtree
tileset.json 是 3D Tiles 1.1,subdivisionScheme 为 OCTREE,refine 为 REPLACE。一个 subtree 覆盖全部层级。glTF 为 Y 轴向上、unlit、Draco。底色贴图默认 image/ktx2,并声明 KHR_texture_basisu。
层级 L 的图集边长是:
text
max(128, 4096 / 2^(maxLevel - L))
模型不大、层级少的时候,最细层不一定真到 4096,按这个公式算即可。
使用时注意的几点
- GLB 的向上轴别照搬 glTF 默认值。 规范里 glTF 是 Y-up,但不少生产管线(比如 ODM)导出的顶点是 Z-up。模型进 Cesium 后横躺,先查
--up。 - 高度是椭球高。 和地形的离地高不是同一个数。贴地效果不对时,先核对原点高程,再查地形。
- KTX2 编码在 CPU 上。 大模型、高层级时,这一步会占时间。调试阶段可以先加
--no-ktx2,确认几何和 LOD,再打开 KTX2 出正式包。 - 图集有上限。 单瓦片一张图集,UV 岛会重排。原模型贴图又多又碎时,4096 也不一定装得下所有细节,必要时把
--leaf-size调小,让单个瓦片盖的范围变小。 - 发布包要整目录拷贝。 缺了同目录的
node、sharp或 WASM,命令行能启动也会在读图或压纹理时报错。
技术栈(方便对照版本)
| 部分 | 使用 |
|---|---|
| 语言与运行时 | TypeScript,Node.js 18+ |
| glTF 与 Draco | @gltf-transform/core、extensions、functions,draco3dgltf |
| 贴图缩放与采样 | sharp |
| KTX2 | @loaders.gl/textures 里的 Basis Universal WASM,worker_threads 并行 |
| 瓦片格式 | 3D Tiles 1.1 implicit octree,KHR_draco_mesh_compression,KHR_materials_unlit,可选 KHR_texture_basisu |
小结
mesh-to-tiles 把「带贴图网格 → 隐式八叉树 3D Tiles」收成一条命令:LOD 按包围盒生成,每层一张重烘焙图集,几何 Draco,纹理默认 KTX2,并用经纬度和椭球高把局部原点放到地球上。
代码和用法见 GitHub:Tao-tao520/mesh-to-tiles。参数和输出格式以仓库 README 为准。
关键词: 3D Tiles,Cesium,glTF,OBJ,Draco,KTX2,隐式八叉树,LOD