feat(preview): add low poly vehicle library
@@ -259,6 +259,17 @@ tilt_y = TILT_JITTER * math.cos(index * 0.927295)
|
||||
| 靠调 `FOLIAGE_EMISSION` 提亮植被 | 用错了旋钮,该调 albedo gain |
|
||||
| 在 `MATERIALS` 中间插入条目 | GLB 材质索引整体平移 |
|
||||
|
||||
## 第三方资产导入的源文件边界
|
||||
|
||||
第三方 Blend 只属于一次性入库工具的输入,不能成为 `build:area` 或 Cesium 预览的运行时依赖。
|
||||
完成拆分后,仓库必须包含可直接消费的 glTF/bin、共享贴图、manifest 和人工预览;原始下载文件可
|
||||
以删除。导入工具应将源文件路径作为显式 `--source` 参数,manifest 最多保留源文件名作溯源,
|
||||
不能写死用户 Downloads 目录。
|
||||
|
||||
删除原始文件前必须确认入库资产已通过结构验证和人工预览;删除后若需重新导入,必须重新取得
|
||||
同一 Blend 与其外部贴图。`blender/tools/split_lowpoly_cars.py` 是这一约定的实例,不参与区域
|
||||
构建阶段。
|
||||
|
||||
---
|
||||
|
||||
## 相关
|
||||
|
||||
1
.trellis/tasks/08-05-lowpoly-cars-assets/check.jsonl
Normal file
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
54
.trellis/tasks/08-05-lowpoly-cars-assets/design.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# 设计:LowPoly Cars 资产库
|
||||
|
||||
## 边界与目录
|
||||
|
||||
本任务独立于区域构建和 Cesium 预览。新增目录
|
||||
`assets/models/custom/lowpoly_cars/`,其内部结构与 Shapespark 植物包一致:
|
||||
|
||||
```text
|
||||
lowpoly_cars/
|
||||
manifest.json
|
||||
index.html
|
||||
textures/
|
||||
<vehicle-id>/model.gltf
|
||||
<vehicle-id>/model.bin
|
||||
<vehicle-id>/preview.png
|
||||
```
|
||||
|
||||
源 `.blend` 保留在用户 Downloads,不复制入仓库;`manifest.json` 保存源文件名与根对象名,
|
||||
以便可追溯而不引入二进制源文件。
|
||||
|
||||
## 拆分与规范化
|
||||
|
||||
新 Blender 工具以 Empty 根节点和其全部后代 mesh 作为一辆车。根节点名是稳定初始 ID:
|
||||
`car_a01_002`、`truck_a01_001` 等。名称重复的车辆保留全部条目,不依据几何统计猜测合并。
|
||||
|
||||
导出前把每辆车的根节点变换 bake 到其后代网格,再把局部原点置于车辆的水平中心和最低接地点。
|
||||
规范化的预览坐标为 X 前进、Y 向上、Z 横向;若源根节点的朝向不符合它,工具按对象族显式
|
||||
变换并由渲染缩略图复核。不能依赖源文件中用于陈列的全局坐标、`0.001` 缩放或 90°/180°旋转。
|
||||
|
||||
保留同一辆车的 mesh 分件与材质槽,避免为图库的便利合并导致窗、轮胎等材质丢失。图像依赖
|
||||
集中复制到 `textures/`,glTF 以相对路径引用,重复车款不能复制同一张贴图。
|
||||
|
||||
## 产物与预览
|
||||
|
||||
工具为每辆车导出 glTF/bin,使用隔离的三点布光、地面阴影与固定透视相机输出
|
||||
`preview.png`。`index.html` 只消费 manifest,响应式显示缩略图、ID、类别、三角面数、尺寸和
|
||||
材质;它不依赖 Cesium、Node 打包或区域输出,通过 HTTP 直接查看。
|
||||
|
||||
`manifest.json` 的每项至少包含 `id`、`name`、`category`、`sourceRoot`、`model`、`bin`、
|
||||
`preview`,以及 `metrics.vertices`、`metrics.triangles`、`metrics.dimensions`、
|
||||
`metrics.materials`。`category` 取 `car`、`truck` 或 `tractor`。
|
||||
|
||||
## 风险与回退
|
||||
|
||||
Blender 的沙箱内加载会崩溃,因此导出、渲染和检查仅通过受批准的沙箱外 Blender 调用。若
|
||||
某一根节点导出失败,工具记录错误并继续其余资产;manifest 只收录有效产物,失败根节点写入
|
||||
报告。删除 `lowpoly_cars/` 不影响任何当前构建阶段或 Cesium 巡航。
|
||||
|
||||
## Cesium 候选集
|
||||
|
||||
候选集固定为 `car_a01_002`、`car_a02_002`、`car_a03_001`,以及按稳定 ID 排序的前三辆
|
||||
卡车 `truck_a01_001`、`truck_a02_001`、`truck_a03_001`。预览构建把这六个 glTF/bin 与一张
|
||||
共享 512px 贴图复制到区域输出目录并改写其 image URI;浏览器页面加载时从文件名列表随机选
|
||||
一项,所有巡航实体在本页使用相同模型。这样构建产物保持确定,随机性只属于用户可见的会话。
|
||||
1
.trellis/tasks/08-05-lowpoly-cars-assets/implement.jsonl
Normal file
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
27
.trellis/tasks/08-05-lowpoly-cars-assets/implement.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# 实施计划:LowPoly Cars 资产库
|
||||
|
||||
1. 在 `blender/tools/` 增加可重复执行的拆分/导出工具,读取指定 `.blend`,枚举命名 Empty
|
||||
根节点,bake 变换、规范化坐标并分别导出车辆。
|
||||
2. 让工具生成共享贴图目录、逐车 glTF/bin、缩略图、manifest 与静态图库 HTML;输出排序必须
|
||||
按稳定 ID,避免重复运行产生无关差异。
|
||||
3. 为导出结果提供轻量结构检查:验证 manifest 路径存在、ID 唯一、几何指标为正、模型和缩略图
|
||||
可读取;对源根节点与导出条目数量做对账。
|
||||
4. 在沙箱外运行导出与缩略图渲染,目检图库中的普通车、卡车和拖拉机,核对朝向、落地与比例。
|
||||
5. 运行 Python/Node 语法检查和现有 preview 测试,确认不触及 `vehicle-model.js`、路线 JSON 或
|
||||
Cesium 默认预览;记录验证结果。
|
||||
6. 验收后提交资产库、工具、来源说明与必要的代码规范;不改当前默认巡航模型。
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
blender --factory-startup --background --python blender/tools/<tool>.py -- --source <blend> --output assets/models/custom/lowpoly_cars
|
||||
python3 blender/tools/<tool>.py --verify assets/models/custom/lowpoly_cars
|
||||
node --check scripts/lib/vehicle-model.js
|
||||
npm run test:preview-assets
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## 回滚
|
||||
|
||||
本任务只新增独立资产库和工具。若某项导出或方向不合格,删除该新增目录和工具即可;当前区域
|
||||
构建和 Cesium 预览没有依赖它们。
|
||||
53
.trellis/tasks/08-05-lowpoly-cars-assets/prd.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# 拆分 LowPoly Cars 车辆资产
|
||||
|
||||
## Goal
|
||||
|
||||
将用户下载的 `011.+LowPoly_Cars_01_blend.blend` 拆分为可独立检查、可被后续
|
||||
Cesium 车辆预览复用的低模车辆资产库;沿用 Shapespark 植物包的资产目录、清单与
|
||||
浏览器预览体验。
|
||||
|
||||
## Confirmed Facts
|
||||
|
||||
- 源文件位于 `/Users/que01/Downloads/011.+LowPoly_Cars_01_blend.blend`,为 Blender
|
||||
3.0、64 位小端格式,约 25 MB;不应直接作为仓库产物引用。
|
||||
- 当前 Blender 4.5.12 LTS 在沙箱中加载会崩溃;在沙箱外可稳定读取,故拆分和渲染应通过
|
||||
沙箱外 Blender 调用完成。
|
||||
- 源文件有 12 个可导出 Empty 根节点:5 个普通车条目(其中 `car_a01`、`car_a02` 各有两份
|
||||
同名变体)、4 个卡车、3 个拖拉机;每项由 11 到 19 个 mesh 组成。
|
||||
- 根节点保存了展示排布用的 `0.001` 缩放与 90°/180°旋转。拆分必须应用这些层级变换,统一
|
||||
比例、地面基线和车辆前进轴,不能将源文件世界坐标原样导出。
|
||||
- 主体使用共享 `carmaterial`(含 base-color 图和 normal map),拆分后应共用贴图,不复制到
|
||||
每一个车辆目录。
|
||||
- 植物包位于 `assets/models/custom/shapespark_plants/`:每个资产拥有
|
||||
`model.gltf`、`model.bin`、`preview.png`,共享贴图放 `textures/`,并以
|
||||
`manifest.json` 和 `index.html` 提供机器与人工检查入口。
|
||||
- 已完成的车辆连续路线系统读取预览车辆 glTF;新资产库在验收前不得破坏该默认车辆或
|
||||
现有路线播放。
|
||||
|
||||
## Requirements
|
||||
|
||||
1. 稳定读取源 `.blend`,记录其集合、对象、材质、几何指标、坐标朝向和许可/来源信息。
|
||||
2. 按可独立使用的车辆变体拆分为 glTF 资产,保留正确比例、轮胎落地基线、前进朝向与
|
||||
原始材质语义;共享贴图不得为每辆车重复复制。
|
||||
3. 在 `assets/models/custom/` 下创建与植物包一致的清晰目录、JSON manifest 与简洁的
|
||||
HTML 图库预览;每辆车显示缩略图、三角面数、尺寸与材质信息。
|
||||
4. 预览页必须可通过本地 HTTP 查看,不依赖主场景或 QGIS;缩略图应能帮助比较车款、颜色
|
||||
和比例。
|
||||
5. 新库经过 Blender/glTF 结构检查;从普通车中固定选 3 个、从卡车中选前 3 个组成候选集,
|
||||
Cesium 每次打开预览时从该 6 个模型随机选一辆作为巡航车辆。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] 能稳定枚举源文件内的可导出车辆,并把每个产物与源对象/集合对应起来。
|
||||
- [ ] 每个可用车辆有独立 `model.gltf`/二进制数据、预览图与 manifest 条目;预览图和 HTML
|
||||
能正确加载。
|
||||
- [ ] manifest 为每项记录稳定 ID、名称、来源、几何指标、尺寸、材质和相对路径。
|
||||
- [ ] 人工查看图库可比较全部导出的车辆;没有重复贴图、明显错误的坐标轴或悬空/埋地车轮。
|
||||
- [ ] 现有目标区域的 Cesium 巡航预览仍可生成和播放。
|
||||
- [ ] 目标区域预览目录包含选定的 6 个车辆模型与一张共享贴图;页面刷新会从候选集随机选车。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 不在本任务中制作车辆动画、转向轮、交通行为、LOD 或性能压缩策略。
|
||||
- 不修改 QGIS 工程、道路生成或上一轮已经确认的车辆连续路线逻辑。
|
||||
- 不引入车辆动画、逐车随机分配、性能压缩或车辆选择控件;本轮只在页面加载时随机确定一种车。
|
||||
26
.trellis/tasks/08-05-lowpoly-cars-assets/task.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "lowpoly-cars-assets",
|
||||
"name": "lowpoly-cars-assets",
|
||||
"title": "拆分 LowPoly Cars 车辆资产",
|
||||
"description": "",
|
||||
"status": "in_progress",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-05",
|
||||
"completedAt": null,
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -19,3 +19,21 @@
|
||||
旧的 SpeedTree apple、Lyrog fattree、Poly Haven `shrub_02`、原始高模
|
||||
`custom/bush` 和 rejected `custom/bush_low` 已删除。相关实验结论保留在
|
||||
`docs/changelog.md` 和归档 Trellis 任务中。
|
||||
|
||||
## LowPoly Cars
|
||||
|
||||
`assets/models/custom/lowpoly_cars/` 是从用户本地下载的
|
||||
`011.+LowPoly_Cars_01_blend.blend` 拆分出的车辆图库。目录结构与植物包一致:每辆车
|
||||
拥有 `model.gltf`、`model.bin` 与 `preview.png`;全部资产共用
|
||||
`textures/color_512x512.jpg`,以保留颜色图集中的各车型车漆区域。
|
||||
|
||||
`manifest.json` 记录源 Empty 根节点、分类、尺寸、顶点/三角面数和相对文件路径,
|
||||
`index.html` 用于人工选择车款。源文件许可证尚未确认;在重新分发资产前必须核对原始下载
|
||||
页面的许可。
|
||||
|
||||
拆分由 `blender/tools/split_lowpoly_cars.py` 执行。该工具只读取源 Blend 与指定贴图,
|
||||
不会接入或替换当前 Cesium 巡航车辆。
|
||||
|
||||
区域构建**不依赖** Downloads 中的原始 `.blend`:它只读取本目录已提交的 glTF/bin/贴图。
|
||||
因此原始 Blend 可以在验收后删除;代价是日后不能从零重跑该导入脚本,除非重新取得同一源文件
|
||||
与颜色贴图。
|
||||
|
||||
BIN
assets/models/custom/lowpoly_cars/car_a01_002/model.bin
Normal file
1072
assets/models/custom/lowpoly_cars/car_a01_002/model.gltf
Normal file
BIN
assets/models/custom/lowpoly_cars/car_a01_002/preview.png
Normal file
|
After Width: | Height: | Size: 367 KiB |
BIN
assets/models/custom/lowpoly_cars/car_a01_003/model.bin
Normal file
1072
assets/models/custom/lowpoly_cars/car_a01_003/model.gltf
Normal file
BIN
assets/models/custom/lowpoly_cars/car_a01_003/preview.png
Normal file
|
After Width: | Height: | Size: 336 KiB |
BIN
assets/models/custom/lowpoly_cars/car_a02_002/model.bin
Normal file
1149
assets/models/custom/lowpoly_cars/car_a02_002/model.gltf
Normal file
BIN
assets/models/custom/lowpoly_cars/car_a02_002/preview.png
Normal file
|
After Width: | Height: | Size: 351 KiB |
BIN
assets/models/custom/lowpoly_cars/car_a02_003/model.bin
Normal file
1149
assets/models/custom/lowpoly_cars/car_a02_003/model.gltf
Normal file
BIN
assets/models/custom/lowpoly_cars/car_a02_003/preview.png
Normal file
|
After Width: | Height: | Size: 360 KiB |
BIN
assets/models/custom/lowpoly_cars/car_a03_001/model.bin
Normal file
1445
assets/models/custom/lowpoly_cars/car_a03_001/model.gltf
Normal file
BIN
assets/models/custom/lowpoly_cars/car_a03_001/preview.png
Normal file
|
After Width: | Height: | Size: 338 KiB |
13
assets/models/custom/lowpoly_cars/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html><meta charset="utf-8"><title>LowPoly Cars</title>
|
||||
<style>body{margin:24px;font:14px system-ui,sans-serif;background:#edf1f3;color:#16212a}main{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:14px}article{background:#fff;border:1px solid #cbd5db;border-radius:6px;padding:10px}img{width:100%;aspect-ratio:1;object-fit:contain;background:#20282e}h2{font-size:15px;margin:8px 0 4px}p{margin:3px 0;color:#4a5862}</style><main><article><img src="car_a01_002/preview.png" alt="Car A01 002"><h2>Car A01 002</h2><p>car · 4,574 tris</p><p>5.3661 x 2.2731 x 2.1082 m</p><p>carmaterial</p></article>
|
||||
<article><img src="car_a01_003/preview.png" alt="Car A01 003"><h2>Car A01 003</h2><p>car · 4,574 tris</p><p>5.3661 x 2.2731 x 2.1082 m</p><p>carmaterial</p></article>
|
||||
<article><img src="car_a02_002/preview.png" alt="Car A02 002"><h2>Car A02 002</h2><p>car · 5,898 tris</p><p>5.3727 x 2.3141 x 2.1082 m</p><p>carmaterial</p></article>
|
||||
<article><img src="car_a02_003/preview.png" alt="Car A02 003"><h2>Car A02 003</h2><p>car · 5,898 tris</p><p>5.3727 x 2.3141 x 2.1082 m</p><p>carmaterial</p></article>
|
||||
<article><img src="car_a03_001/preview.png" alt="Car A03 001"><h2>Car A03 001</h2><p>car · 14,028 tris</p><p>9.6993 x 2.7325 x 2.1082 m</p><p>carmaterial</p></article>
|
||||
<article><img src="tractor_a01_001/preview.png" alt="Tractor A01 001"><h2>Tractor A01 001</h2><p>tractor · 3,899 tris</p><p>4.3862 x 2.5279 x 3.04 m</p><p>carmaterial</p></article>
|
||||
<article><img src="tractor_a02_001/preview.png" alt="Tractor A02 001"><h2>Tractor A02 001</h2><p>tractor · 4,295 tris</p><p>5.894 x 2.8004 x 3.0523 m</p><p>carmaterial</p></article>
|
||||
<article><img src="tractor_a03_001/preview.png" alt="Tractor A03 001"><h2>Tractor A03 001</h2><p>tractor · 12,767 tris</p><p>10.4554 x 2.7708 x 3.04 m</p><p>carmaterial</p></article>
|
||||
<article><img src="truck_a01_001/preview.png" alt="Truck A01 001"><h2>Truck A01 001</h2><p>truck · 8,724 tris</p><p>14.7667 x 3.288 x 4.5493 m</p><p>carmaterial</p></article>
|
||||
<article><img src="truck_a02_001/preview.png" alt="Truck A02 001"><h2>Truck A02 001</h2><p>truck · 8,444 tris</p><p>12.1239 x 3.2881 x 4.5493 m</p><p>carmaterial</p></article>
|
||||
<article><img src="truck_a03_001/preview.png" alt="Truck A03 001"><h2>Truck A03 001</h2><p>truck · 12,172 tris</p><p>12.1625 x 3.4661 x 4.0701 m</p><p>carmaterial</p></article>
|
||||
<article><img src="truck_a04_001/preview.png" alt="Truck A04 001"><h2>Truck A04 001</h2><p>truck · 13,127 tris</p><p>12.1068 x 3.4692 x 4.0701 m</p><p>carmaterial</p></article></main>
|
||||
260
assets/models/custom/lowpoly_cars/manifest.json
Normal file
@@ -0,0 +1,260 @@
|
||||
{
|
||||
"source": "011.+LowPoly_Cars_01_blend.blend",
|
||||
"license": "Unknown; verify the original download license before redistribution.",
|
||||
"texture": "textures/color_512x512.jpg",
|
||||
"layout": "Each asset has model.gltf/model.bin/preview.png; the 512px color texture is shared in textures/.",
|
||||
"assets": [
|
||||
{
|
||||
"id": "car_a01_002",
|
||||
"name": "Car A01 002",
|
||||
"category": "car",
|
||||
"sourceRoot": "car_a01_002",
|
||||
"model": "car_a01_002/model.gltf",
|
||||
"bin": "car_a01_002/model.bin",
|
||||
"preview": "car_a01_002/preview.png",
|
||||
"metrics": {
|
||||
"vertices": 2526,
|
||||
"triangles": 4574,
|
||||
"dimensions": [
|
||||
5.3661,
|
||||
2.2731,
|
||||
2.1082
|
||||
],
|
||||
"materials": [
|
||||
"carmaterial"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "car_a01_003",
|
||||
"name": "Car A01 003",
|
||||
"category": "car",
|
||||
"sourceRoot": "car_a01_003",
|
||||
"model": "car_a01_003/model.gltf",
|
||||
"bin": "car_a01_003/model.bin",
|
||||
"preview": "car_a01_003/preview.png",
|
||||
"metrics": {
|
||||
"vertices": 2526,
|
||||
"triangles": 4574,
|
||||
"dimensions": [
|
||||
5.3661,
|
||||
2.2731,
|
||||
2.1082
|
||||
],
|
||||
"materials": [
|
||||
"carmaterial"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "car_a02_002",
|
||||
"name": "Car A02 002",
|
||||
"category": "car",
|
||||
"sourceRoot": "car_a02_002",
|
||||
"model": "car_a02_002/model.gltf",
|
||||
"bin": "car_a02_002/model.bin",
|
||||
"preview": "car_a02_002/preview.png",
|
||||
"metrics": {
|
||||
"vertices": 3426,
|
||||
"triangles": 5898,
|
||||
"dimensions": [
|
||||
5.3727,
|
||||
2.3141,
|
||||
2.1082
|
||||
],
|
||||
"materials": [
|
||||
"carmaterial"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "car_a02_003",
|
||||
"name": "Car A02 003",
|
||||
"category": "car",
|
||||
"sourceRoot": "car_a02_003",
|
||||
"model": "car_a02_003/model.gltf",
|
||||
"bin": "car_a02_003/model.bin",
|
||||
"preview": "car_a02_003/preview.png",
|
||||
"metrics": {
|
||||
"vertices": 3426,
|
||||
"triangles": 5898,
|
||||
"dimensions": [
|
||||
5.3727,
|
||||
2.3141,
|
||||
2.1082
|
||||
],
|
||||
"materials": [
|
||||
"carmaterial"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "car_a03_001",
|
||||
"name": "Car A03 001",
|
||||
"category": "car",
|
||||
"sourceRoot": "car_a03_001",
|
||||
"model": "car_a03_001/model.gltf",
|
||||
"bin": "car_a03_001/model.bin",
|
||||
"preview": "car_a03_001/preview.png",
|
||||
"metrics": {
|
||||
"vertices": 9401,
|
||||
"triangles": 14028,
|
||||
"dimensions": [
|
||||
9.6993,
|
||||
2.7325,
|
||||
2.1082
|
||||
],
|
||||
"materials": [
|
||||
"carmaterial"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "tractor_a01_001",
|
||||
"name": "Tractor A01 001",
|
||||
"category": "tractor",
|
||||
"sourceRoot": "tractor_a01_001",
|
||||
"model": "tractor_a01_001/model.gltf",
|
||||
"bin": "tractor_a01_001/model.bin",
|
||||
"preview": "tractor_a01_001/preview.png",
|
||||
"metrics": {
|
||||
"vertices": 2301,
|
||||
"triangles": 3899,
|
||||
"dimensions": [
|
||||
4.3862,
|
||||
2.5279,
|
||||
3.04
|
||||
],
|
||||
"materials": [
|
||||
"carmaterial"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "tractor_a02_001",
|
||||
"name": "Tractor A02 001",
|
||||
"category": "tractor",
|
||||
"sourceRoot": "tractor_a02_001",
|
||||
"model": "tractor_a02_001/model.gltf",
|
||||
"bin": "tractor_a02_001/model.bin",
|
||||
"preview": "tractor_a02_001/preview.png",
|
||||
"metrics": {
|
||||
"vertices": 2517,
|
||||
"triangles": 4295,
|
||||
"dimensions": [
|
||||
5.894,
|
||||
2.8004,
|
||||
3.0523
|
||||
],
|
||||
"materials": [
|
||||
"carmaterial"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "tractor_a03_001",
|
||||
"name": "Tractor A03 001",
|
||||
"category": "tractor",
|
||||
"sourceRoot": "tractor_a03_001",
|
||||
"model": "tractor_a03_001/model.gltf",
|
||||
"bin": "tractor_a03_001/model.bin",
|
||||
"preview": "tractor_a03_001/preview.png",
|
||||
"metrics": {
|
||||
"vertices": 8853,
|
||||
"triangles": 12767,
|
||||
"dimensions": [
|
||||
10.4554,
|
||||
2.7708,
|
||||
3.04
|
||||
],
|
||||
"materials": [
|
||||
"carmaterial"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "truck_a01_001",
|
||||
"name": "Truck A01 001",
|
||||
"category": "truck",
|
||||
"sourceRoot": "truck_a01_001",
|
||||
"model": "truck_a01_001/model.gltf",
|
||||
"bin": "truck_a01_001/model.bin",
|
||||
"preview": "truck_a01_001/preview.png",
|
||||
"metrics": {
|
||||
"vertices": 4751,
|
||||
"triangles": 8724,
|
||||
"dimensions": [
|
||||
14.7667,
|
||||
3.288,
|
||||
4.5493
|
||||
],
|
||||
"materials": [
|
||||
"carmaterial"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "truck_a02_001",
|
||||
"name": "Truck A02 001",
|
||||
"category": "truck",
|
||||
"sourceRoot": "truck_a02_001",
|
||||
"model": "truck_a02_001/model.gltf",
|
||||
"bin": "truck_a02_001/model.bin",
|
||||
"preview": "truck_a02_001/preview.png",
|
||||
"metrics": {
|
||||
"vertices": 4583,
|
||||
"triangles": 8444,
|
||||
"dimensions": [
|
||||
12.1239,
|
||||
3.2881,
|
||||
4.5493
|
||||
],
|
||||
"materials": [
|
||||
"carmaterial"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "truck_a03_001",
|
||||
"name": "Truck A03 001",
|
||||
"category": "truck",
|
||||
"sourceRoot": "truck_a03_001",
|
||||
"model": "truck_a03_001/model.gltf",
|
||||
"bin": "truck_a03_001/model.bin",
|
||||
"preview": "truck_a03_001/preview.png",
|
||||
"metrics": {
|
||||
"vertices": 6695,
|
||||
"triangles": 12172,
|
||||
"dimensions": [
|
||||
12.1625,
|
||||
3.4661,
|
||||
4.0701
|
||||
],
|
||||
"materials": [
|
||||
"carmaterial"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "truck_a04_001",
|
||||
"name": "Truck A04 001",
|
||||
"category": "truck",
|
||||
"sourceRoot": "truck_a04_001",
|
||||
"model": "truck_a04_001/model.gltf",
|
||||
"bin": "truck_a04_001/model.bin",
|
||||
"preview": "truck_a04_001/preview.png",
|
||||
"metrics": {
|
||||
"vertices": 7397,
|
||||
"triangles": 13127,
|
||||
"dimensions": [
|
||||
12.1068,
|
||||
3.4692,
|
||||
4.0701
|
||||
],
|
||||
"materials": [
|
||||
"carmaterial"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
assets/models/custom/lowpoly_cars/textures/color_512x512.jpg
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
assets/models/custom/lowpoly_cars/tractor_a01_001/model.bin
Normal file
918
assets/models/custom/lowpoly_cars/tractor_a01_001/model.gltf
Normal file
@@ -0,0 +1,918 @@
|
||||
{
|
||||
"asset": {
|
||||
"generator": "Khronos glTF Blender I/O v4.5.51",
|
||||
"version": "2.0"
|
||||
},
|
||||
"extensionsUsed": [
|
||||
"KHR_materials_specular",
|
||||
"KHR_materials_ior"
|
||||
],
|
||||
"scene": 0,
|
||||
"scenes": [
|
||||
{
|
||||
"name": "Scene",
|
||||
"nodes": [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10
|
||||
]
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"mesh": 0,
|
||||
"name": "Rectangle020.001"
|
||||
},
|
||||
{
|
||||
"mesh": 1,
|
||||
"name": "Rectangle019.001"
|
||||
},
|
||||
{
|
||||
"mesh": 2,
|
||||
"name": "Object178.001"
|
||||
},
|
||||
{
|
||||
"mesh": 3,
|
||||
"name": "Object175.001"
|
||||
},
|
||||
{
|
||||
"mesh": 4,
|
||||
"name": "Line250.001"
|
||||
},
|
||||
{
|
||||
"mesh": 5,
|
||||
"name": "Cylinder135.001"
|
||||
},
|
||||
{
|
||||
"mesh": 6,
|
||||
"name": "Cylinder133.001"
|
||||
},
|
||||
{
|
||||
"mesh": 7,
|
||||
"name": "Box1449.001"
|
||||
},
|
||||
{
|
||||
"mesh": 8,
|
||||
"name": "Box1448.001"
|
||||
},
|
||||
{
|
||||
"mesh": 9,
|
||||
"name": "Box1447.001"
|
||||
},
|
||||
{
|
||||
"mesh": 10,
|
||||
"name": "Box1444.001"
|
||||
}
|
||||
],
|
||||
"materials": [
|
||||
{
|
||||
"doubleSided": true,
|
||||
"extensions": {
|
||||
"KHR_materials_specular": {
|
||||
"specularFactor": 0
|
||||
},
|
||||
"KHR_materials_ior": {
|
||||
"ior": 1.4500000476837158
|
||||
}
|
||||
},
|
||||
"name": "carmaterial",
|
||||
"pbrMetallicRoughness": {
|
||||
"baseColorTexture": {
|
||||
"index": 0
|
||||
},
|
||||
"roughnessFactor": 0.858578622341156
|
||||
}
|
||||
}
|
||||
],
|
||||
"meshes": [
|
||||
{
|
||||
"name": "Mesh.212",
|
||||
"primitives": [
|
||||
{
|
||||
"attributes": {
|
||||
"POSITION": 0,
|
||||
"NORMAL": 1,
|
||||
"TEXCOORD_0": 2
|
||||
},
|
||||
"indices": 3,
|
||||
"material": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Mesh.213",
|
||||
"primitives": [
|
||||
{
|
||||
"attributes": {
|
||||
"POSITION": 4,
|
||||
"NORMAL": 5,
|
||||
"TEXCOORD_0": 6
|
||||
},
|
||||
"indices": 7,
|
||||
"material": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Mesh.214",
|
||||
"primitives": [
|
||||
{
|
||||
"attributes": {
|
||||
"POSITION": 8,
|
||||
"NORMAL": 9,
|
||||
"TEXCOORD_0": 10
|
||||
},
|
||||
"indices": 11,
|
||||
"material": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Mesh.215",
|
||||
"primitives": [
|
||||
{
|
||||
"attributes": {
|
||||
"POSITION": 12,
|
||||
"NORMAL": 13,
|
||||
"TEXCOORD_0": 14
|
||||
},
|
||||
"indices": 15,
|
||||
"material": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Mesh.216",
|
||||
"primitives": [
|
||||
{
|
||||
"attributes": {
|
||||
"POSITION": 16,
|
||||
"NORMAL": 17,
|
||||
"TEXCOORD_0": 18
|
||||
},
|
||||
"indices": 19,
|
||||
"material": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Mesh.217",
|
||||
"primitives": [
|
||||
{
|
||||
"attributes": {
|
||||
"POSITION": 20,
|
||||
"NORMAL": 21,
|
||||
"TEXCOORD_0": 22
|
||||
},
|
||||
"indices": 23,
|
||||
"material": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Mesh.218",
|
||||
"primitives": [
|
||||
{
|
||||
"attributes": {
|
||||
"POSITION": 24,
|
||||
"NORMAL": 25,
|
||||
"TEXCOORD_0": 26
|
||||
},
|
||||
"indices": 27,
|
||||
"material": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Mesh.219",
|
||||
"primitives": [
|
||||
{
|
||||
"attributes": {
|
||||
"POSITION": 28,
|
||||
"NORMAL": 29,
|
||||
"TEXCOORD_0": 30
|
||||
},
|
||||
"indices": 31,
|
||||
"material": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Mesh.220",
|
||||
"primitives": [
|
||||
{
|
||||
"attributes": {
|
||||
"POSITION": 32,
|
||||
"NORMAL": 33,
|
||||
"TEXCOORD_0": 34
|
||||
},
|
||||
"indices": 35,
|
||||
"material": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Mesh.221",
|
||||
"primitives": [
|
||||
{
|
||||
"attributes": {
|
||||
"POSITION": 36,
|
||||
"NORMAL": 37,
|
||||
"TEXCOORD_0": 38
|
||||
},
|
||||
"indices": 39,
|
||||
"material": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Mesh.222",
|
||||
"primitives": [
|
||||
{
|
||||
"attributes": {
|
||||
"POSITION": 40,
|
||||
"NORMAL": 41,
|
||||
"TEXCOORD_0": 42
|
||||
},
|
||||
"indices": 43,
|
||||
"material": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"textures": [
|
||||
{
|
||||
"sampler": 0,
|
||||
"source": 0
|
||||
}
|
||||
],
|
||||
"images": [
|
||||
{
|
||||
"mimeType": "image/jpeg",
|
||||
"name": "LowPolyCarsCesiumTexture",
|
||||
"uri": "../textures/color_512x512.jpg"
|
||||
}
|
||||
],
|
||||
"accessors": [
|
||||
{
|
||||
"bufferView": 0,
|
||||
"componentType": 5126,
|
||||
"count": 1469,
|
||||
"max": [
|
||||
1.6975711584091187,
|
||||
2.9161386489868164,
|
||||
1.1722427606582642
|
||||
],
|
||||
"min": [
|
||||
-2.203437328338623,
|
||||
0.7860412001609802,
|
||||
-1.204569697380066
|
||||
],
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 1,
|
||||
"componentType": 5126,
|
||||
"count": 1469,
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 2,
|
||||
"componentType": 5126,
|
||||
"count": 1469,
|
||||
"type": "VEC2"
|
||||
},
|
||||
{
|
||||
"bufferView": 3,
|
||||
"componentType": 5123,
|
||||
"count": 2427,
|
||||
"type": "SCALAR"
|
||||
},
|
||||
{
|
||||
"bufferView": 4,
|
||||
"componentType": 5126,
|
||||
"count": 96,
|
||||
"max": [
|
||||
1.930496096611023,
|
||||
1.744305968284607,
|
||||
0.6042758226394653
|
||||
],
|
||||
"min": [
|
||||
0.1960076093673706,
|
||||
0.93204265832901,
|
||||
-0.635987401008606
|
||||
],
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 5,
|
||||
"componentType": 5126,
|
||||
"count": 96,
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 6,
|
||||
"componentType": 5126,
|
||||
"count": 96,
|
||||
"type": "VEC2"
|
||||
},
|
||||
{
|
||||
"bufferView": 7,
|
||||
"componentType": 5123,
|
||||
"count": 168,
|
||||
"type": "SCALAR"
|
||||
},
|
||||
{
|
||||
"bufferView": 8,
|
||||
"componentType": 5126,
|
||||
"count": 32,
|
||||
"max": [
|
||||
1.334498405456543,
|
||||
2.7617526054382324,
|
||||
0.8541421890258789
|
||||
],
|
||||
"min": [
|
||||
-0.2517751455307007,
|
||||
1.9346791505813599,
|
||||
-0.7565109729766846
|
||||
],
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 9,
|
||||
"componentType": 5126,
|
||||
"count": 32,
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 10,
|
||||
"componentType": 5126,
|
||||
"count": 32,
|
||||
"type": "VEC2"
|
||||
},
|
||||
{
|
||||
"bufferView": 11,
|
||||
"componentType": 5123,
|
||||
"count": 54,
|
||||
"type": "SCALAR"
|
||||
},
|
||||
{
|
||||
"bufferView": 12,
|
||||
"componentType": 5126,
|
||||
"count": 32,
|
||||
"max": [
|
||||
-1.6952612400054932,
|
||||
1.5370264053344727,
|
||||
0.7388277649879456
|
||||
],
|
||||
"min": [
|
||||
-1.8932229280471802,
|
||||
0.9074157476425171,
|
||||
-0.7088407874107361
|
||||
],
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 13,
|
||||
"componentType": 5126,
|
||||
"count": 32,
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 14,
|
||||
"componentType": 5126,
|
||||
"count": 32,
|
||||
"type": "VEC2"
|
||||
},
|
||||
{
|
||||
"bufferView": 15,
|
||||
"componentType": 5123,
|
||||
"count": 48,
|
||||
"type": "SCALAR"
|
||||
},
|
||||
{
|
||||
"bufferView": 16,
|
||||
"componentType": 5126,
|
||||
"count": 310,
|
||||
"max": [
|
||||
-0.3921399712562561,
|
||||
3.040038824081421,
|
||||
0.8353042006492615
|
||||
],
|
||||
"min": [
|
||||
-0.5216806530952454,
|
||||
1.6912429332733154,
|
||||
0.6449068188667297
|
||||
],
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 17,
|
||||
"componentType": 5126,
|
||||
"count": 310,
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 18,
|
||||
"componentType": 5126,
|
||||
"count": 310,
|
||||
"type": "VEC2"
|
||||
},
|
||||
{
|
||||
"bufferView": 19,
|
||||
"componentType": 5123,
|
||||
"count": 468,
|
||||
"type": "SCALAR"
|
||||
},
|
||||
{
|
||||
"bufferView": 20,
|
||||
"componentType": 5126,
|
||||
"count": 2140,
|
||||
"max": [
|
||||
2.1827592849731445,
|
||||
1.1958849430084229,
|
||||
1.163870096206665
|
||||
],
|
||||
"min": [
|
||||
-1.683742880821228,
|
||||
0.37404870986938477,
|
||||
-1.1929504871368408
|
||||
],
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 21,
|
||||
"componentType": 5126,
|
||||
"count": 2140,
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 22,
|
||||
"componentType": 5126,
|
||||
"count": 2140,
|
||||
"type": "VEC2"
|
||||
},
|
||||
{
|
||||
"bufferView": 23,
|
||||
"componentType": 5123,
|
||||
"count": 4008,
|
||||
"type": "SCALAR"
|
||||
},
|
||||
{
|
||||
"bufferView": 24,
|
||||
"componentType": 5126,
|
||||
"count": 2208,
|
||||
"max": [
|
||||
1.9612305164337158,
|
||||
1.6576728820800781,
|
||||
1.2428208589553833
|
||||
],
|
||||
"min": [
|
||||
-2.057445526123047,
|
||||
0,
|
||||
-1.2850725650787354
|
||||
],
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 25,
|
||||
"componentType": 5126,
|
||||
"count": 2208,
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 26,
|
||||
"componentType": 5126,
|
||||
"count": 2208,
|
||||
"type": "VEC2"
|
||||
},
|
||||
{
|
||||
"bufferView": 27,
|
||||
"componentType": 5123,
|
||||
"count": 3720,
|
||||
"type": "SCALAR"
|
||||
},
|
||||
{
|
||||
"bufferView": 28,
|
||||
"componentType": 5126,
|
||||
"count": 56,
|
||||
"max": [
|
||||
0.3900825083255768,
|
||||
1.8057881593704224,
|
||||
0.758997917175293
|
||||
],
|
||||
"min": [
|
||||
0.17267626523971558,
|
||||
1.7638591527938843,
|
||||
0.7036498785018921
|
||||
],
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 29,
|
||||
"componentType": 5126,
|
||||
"count": 56,
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 30,
|
||||
"componentType": 5126,
|
||||
"count": 56,
|
||||
"type": "VEC2"
|
||||
},
|
||||
{
|
||||
"bufferView": 31,
|
||||
"componentType": 5123,
|
||||
"count": 84,
|
||||
"type": "SCALAR"
|
||||
},
|
||||
{
|
||||
"bufferView": 32,
|
||||
"componentType": 5126,
|
||||
"count": 40,
|
||||
"max": [
|
||||
0.10584698617458344,
|
||||
0.8977700471878052,
|
||||
0.9958042502403259
|
||||
],
|
||||
"min": [
|
||||
-0.517703652381897,
|
||||
0.8380477428436279,
|
||||
0.7023323178291321
|
||||
],
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 33,
|
||||
"componentType": 5126,
|
||||
"count": 40,
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 34,
|
||||
"componentType": 5126,
|
||||
"count": 40,
|
||||
"type": "VEC2"
|
||||
},
|
||||
{
|
||||
"bufferView": 35,
|
||||
"componentType": 5123,
|
||||
"count": 60,
|
||||
"type": "SCALAR"
|
||||
},
|
||||
{
|
||||
"bufferView": 36,
|
||||
"componentType": 5126,
|
||||
"count": 280,
|
||||
"max": [
|
||||
-1.3012173175811768,
|
||||
1.658207654953003,
|
||||
0.7557153701782227
|
||||
],
|
||||
"min": [
|
||||
-1.922289252281189,
|
||||
0.9572750329971313,
|
||||
-0.7371543645858765
|
||||
],
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 37,
|
||||
"componentType": 5126,
|
||||
"count": 280,
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 38,
|
||||
"componentType": 5126,
|
||||
"count": 280,
|
||||
"type": "VEC2"
|
||||
},
|
||||
{
|
||||
"bufferView": 39,
|
||||
"componentType": 5123,
|
||||
"count": 396,
|
||||
"type": "SCALAR"
|
||||
},
|
||||
{
|
||||
"bufferView": 40,
|
||||
"componentType": 5126,
|
||||
"count": 192,
|
||||
"max": [
|
||||
-1.859789252281189,
|
||||
1.3449180126190186,
|
||||
0.6165856122970581
|
||||
],
|
||||
"min": [
|
||||
-1.9417186975479126,
|
||||
1.19881010055542,
|
||||
-0.5781869888305664
|
||||
],
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 41,
|
||||
"componentType": 5126,
|
||||
"count": 192,
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 42,
|
||||
"componentType": 5126,
|
||||
"count": 192,
|
||||
"type": "VEC2"
|
||||
},
|
||||
{
|
||||
"bufferView": 43,
|
||||
"componentType": 5123,
|
||||
"count": 264,
|
||||
"type": "SCALAR"
|
||||
}
|
||||
],
|
||||
"bufferViews": [
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 17628,
|
||||
"byteOffset": 0,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 17628,
|
||||
"byteOffset": 17628,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 11752,
|
||||
"byteOffset": 35256,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 4854,
|
||||
"byteOffset": 47008,
|
||||
"target": 34963
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 1152,
|
||||
"byteOffset": 51864,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 1152,
|
||||
"byteOffset": 53016,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 768,
|
||||
"byteOffset": 54168,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 336,
|
||||
"byteOffset": 54936,
|
||||
"target": 34963
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 384,
|
||||
"byteOffset": 55272,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 384,
|
||||
"byteOffset": 55656,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 256,
|
||||
"byteOffset": 56040,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 108,
|
||||
"byteOffset": 56296,
|
||||
"target": 34963
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 384,
|
||||
"byteOffset": 56404,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 384,
|
||||
"byteOffset": 56788,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 256,
|
||||
"byteOffset": 57172,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 96,
|
||||
"byteOffset": 57428,
|
||||
"target": 34963
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 3720,
|
||||
"byteOffset": 57524,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 3720,
|
||||
"byteOffset": 61244,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 2480,
|
||||
"byteOffset": 64964,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 936,
|
||||
"byteOffset": 67444,
|
||||
"target": 34963
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 25680,
|
||||
"byteOffset": 68380,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 25680,
|
||||
"byteOffset": 94060,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 17120,
|
||||
"byteOffset": 119740,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 8016,
|
||||
"byteOffset": 136860,
|
||||
"target": 34963
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 26496,
|
||||
"byteOffset": 144876,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 26496,
|
||||
"byteOffset": 171372,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 17664,
|
||||
"byteOffset": 197868,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 7440,
|
||||
"byteOffset": 215532,
|
||||
"target": 34963
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 672,
|
||||
"byteOffset": 222972,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 672,
|
||||
"byteOffset": 223644,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 448,
|
||||
"byteOffset": 224316,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 168,
|
||||
"byteOffset": 224764,
|
||||
"target": 34963
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 480,
|
||||
"byteOffset": 224932,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 480,
|
||||
"byteOffset": 225412,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 320,
|
||||
"byteOffset": 225892,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 120,
|
||||
"byteOffset": 226212,
|
||||
"target": 34963
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 3360,
|
||||
"byteOffset": 226332,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 3360,
|
||||
"byteOffset": 229692,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 2240,
|
||||
"byteOffset": 233052,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 792,
|
||||
"byteOffset": 235292,
|
||||
"target": 34963
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 2304,
|
||||
"byteOffset": 236084,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 2304,
|
||||
"byteOffset": 238388,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 1536,
|
||||
"byteOffset": 240692,
|
||||
"target": 34962
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteLength": 528,
|
||||
"byteOffset": 242228,
|
||||
"target": 34963
|
||||
}
|
||||
],
|
||||
"samplers": [
|
||||
{
|
||||
"magFilter": 9729,
|
||||
"minFilter": 9987
|
||||
}
|
||||
],
|
||||
"buffers": [
|
||||
{
|
||||
"byteLength": 242756,
|
||||
"uri": "model.bin"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
assets/models/custom/lowpoly_cars/tractor_a01_001/preview.png
Normal file
|
After Width: | Height: | Size: 352 KiB |
BIN
assets/models/custom/lowpoly_cars/tractor_a02_001/model.bin
Normal file
1072
assets/models/custom/lowpoly_cars/tractor_a02_001/model.gltf
Normal file
BIN
assets/models/custom/lowpoly_cars/tractor_a02_001/preview.png
Normal file
|
After Width: | Height: | Size: 356 KiB |
BIN
assets/models/custom/lowpoly_cars/tractor_a03_001/model.bin
Normal file
1534
assets/models/custom/lowpoly_cars/tractor_a03_001/model.gltf
Normal file
BIN
assets/models/custom/lowpoly_cars/tractor_a03_001/preview.png
Normal file
|
After Width: | Height: | Size: 338 KiB |
BIN
assets/models/custom/lowpoly_cars/truck_a01_001/model.bin
Normal file
1226
assets/models/custom/lowpoly_cars/truck_a01_001/model.gltf
Normal file
BIN
assets/models/custom/lowpoly_cars/truck_a01_001/preview.png
Normal file
|
After Width: | Height: | Size: 347 KiB |
BIN
assets/models/custom/lowpoly_cars/truck_a02_001/model.bin
Normal file
1226
assets/models/custom/lowpoly_cars/truck_a02_001/model.gltf
Normal file
BIN
assets/models/custom/lowpoly_cars/truck_a02_001/preview.png
Normal file
|
After Width: | Height: | Size: 346 KiB |
BIN
assets/models/custom/lowpoly_cars/truck_a03_001/model.bin
Normal file
1534
assets/models/custom/lowpoly_cars/truck_a03_001/model.gltf
Normal file
BIN
assets/models/custom/lowpoly_cars/truck_a03_001/preview.png
Normal file
|
After Width: | Height: | Size: 331 KiB |
BIN
assets/models/custom/lowpoly_cars/truck_a04_001/model.bin
Normal file
1534
assets/models/custom/lowpoly_cars/truck_a04_001/model.gltf
Normal file
BIN
assets/models/custom/lowpoly_cars/truck_a04_001/preview.png
Normal file
|
After Width: | Height: | Size: 336 KiB |
334
blender/tools/split_lowpoly_cars.py
Normal file
@@ -0,0 +1,334 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Split the downloaded LowPoly Cars Blend into a browsable, reusable asset kit.
|
||||
|
||||
Run through Blender because the source Blend is not safe to open in the sandbox:
|
||||
blender --factory-startup --background --python blender/tools/split_lowpoly_cars.py -- \
|
||||
--source /Users/que01/Downloads/011.+LowPoly_Cars_01_blend.blend \
|
||||
--textures /Users/que01/Downloads/textures111 \
|
||||
--output assets/models/custom/lowpoly_cars
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
|
||||
TEXTURE_NAME = "color_512x512.jpg"
|
||||
PREVIEW_SIZE = 640
|
||||
CESIUM_ALBEDO_GAIN = 2.6
|
||||
|
||||
|
||||
def cli_args():
|
||||
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||
values = {}
|
||||
while argv:
|
||||
key = argv.pop(0)
|
||||
if not key.startswith("--"):
|
||||
raise RuntimeError("Expected a --key argument, got " + key)
|
||||
if not argv:
|
||||
raise RuntimeError("Missing value for " + key)
|
||||
values[key[2:]] = argv.pop(0)
|
||||
return values
|
||||
|
||||
|
||||
def require_file(path, label):
|
||||
if not path or not os.path.isfile(path):
|
||||
raise RuntimeError(label + " does not exist: " + str(path))
|
||||
|
||||
|
||||
def category_for(name):
|
||||
return name.split("_", 1)[0]
|
||||
|
||||
|
||||
def roots():
|
||||
return sorted(
|
||||
(obj for obj in bpy.data.objects if obj.type == "EMPTY" and obj.children),
|
||||
key=lambda obj: obj.name,
|
||||
)
|
||||
|
||||
|
||||
def descendants(root):
|
||||
found = []
|
||||
pending = list(root.children)
|
||||
while pending:
|
||||
obj = pending.pop()
|
||||
found.append(obj)
|
||||
pending.extend(obj.children)
|
||||
return [obj for obj in found if obj.type == "MESH"]
|
||||
|
||||
|
||||
def world_vertices(meshes):
|
||||
return [obj.matrix_world @ vertex.co for obj in meshes for vertex in obj.data.vertices]
|
||||
|
||||
|
||||
def longitudinal_angle(points):
|
||||
"""Align the longest ground-plane PCA axis with +X for consistent previews."""
|
||||
mean_x = sum(point.x for point in points) / len(points)
|
||||
mean_y = sum(point.y for point in points) / len(points)
|
||||
xx = sum((point.x - mean_x) ** 2 for point in points)
|
||||
yy = sum((point.y - mean_y) ** 2 for point in points)
|
||||
xy = sum((point.x - mean_x) * (point.y - mean_y) for point in points)
|
||||
return 0.5 * math.atan2(2.0 * xy, xx - yy)
|
||||
|
||||
|
||||
def bounds(objects):
|
||||
points = [obj.matrix_world @ Vector(corner) for obj in objects for corner in obj.bound_box]
|
||||
lower = Vector(tuple(min(point[axis] for point in points) for axis in range(3)))
|
||||
upper = Vector(tuple(max(point[axis] for point in points) for axis in range(3)))
|
||||
return lower, upper
|
||||
|
||||
|
||||
def reset_temp_collection(collection):
|
||||
for obj in list(collection.objects):
|
||||
mesh = obj.data if obj.type == "MESH" else None
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
if mesh and mesh.users == 0:
|
||||
bpy.data.meshes.remove(mesh)
|
||||
|
||||
|
||||
def make_normalized_objects(root, collection):
|
||||
source_meshes = descendants(root)
|
||||
points = world_vertices(source_meshes)
|
||||
if not points:
|
||||
raise RuntimeError(root.name + " has no mesh descendants")
|
||||
lower = Vector(tuple(min(point[axis] for point in points) for axis in range(3)))
|
||||
upper = Vector(tuple(max(point[axis] for point in points) for axis in range(3)))
|
||||
center = Vector(((lower.x + upper.x) / 2.0, (lower.y + upper.y) / 2.0, lower.z))
|
||||
rotation = Matrix.Rotation(-longitudinal_angle(points), 4, "Z")
|
||||
transform = rotation @ Matrix.Translation(-center)
|
||||
result = []
|
||||
for source in source_meshes:
|
||||
mesh = source.data.copy()
|
||||
mesh.transform(transform @ source.matrix_world)
|
||||
obj = bpy.data.objects.new(source.name, mesh)
|
||||
collection.objects.link(obj)
|
||||
for material in source.data.materials:
|
||||
mesh.materials.append(material)
|
||||
result.append(obj)
|
||||
return result
|
||||
|
||||
|
||||
def select_only(objects):
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
for obj in objects:
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = objects[0]
|
||||
|
||||
|
||||
def bake_cesium_texture(texture_path, output_path):
|
||||
"""Bake the Cesium albedo gain into the shared JPEG pixels."""
|
||||
source = bpy.data.images.load(texture_path, check_existing=False)
|
||||
source.name = "LowPolyCarsSourceTexture"
|
||||
result = bpy.data.images.new("LowPolyCarsCesiumTexture", source.size[0], source.size[1], alpha=False)
|
||||
pixels = list(source.pixels)
|
||||
for index in range(0, len(pixels), 4):
|
||||
pixels[index] = min(1.0, pixels[index] * CESIUM_ALBEDO_GAIN)
|
||||
pixels[index + 1] = min(1.0, pixels[index + 1] * CESIUM_ALBEDO_GAIN)
|
||||
pixels[index + 2] = min(1.0, pixels[index + 2] * CESIUM_ALBEDO_GAIN)
|
||||
pixels[index + 3] = 1.0
|
||||
result.pixels = pixels
|
||||
result.filepath_raw = output_path
|
||||
result.file_format = "JPEG"
|
||||
result.save()
|
||||
return result
|
||||
|
||||
|
||||
def setup_material_texture(image):
|
||||
# The source Blend retains a broken relative image datablock. Loading a
|
||||
# fresh datablock, rather than mutating that stale reference, is required
|
||||
# for both Eevee thumbnails and glTF image export.
|
||||
for material in bpy.data.materials:
|
||||
if not material.use_nodes or not material.node_tree:
|
||||
continue
|
||||
for node in material.node_tree.nodes:
|
||||
if node.type == "TEX_IMAGE":
|
||||
node.image = image
|
||||
|
||||
|
||||
def export_gltf(objects, asset_dir):
|
||||
gltf_path = os.path.join(asset_dir, "model.gltf")
|
||||
select_only(objects)
|
||||
bpy.ops.export_scene.gltf(
|
||||
filepath=gltf_path,
|
||||
export_format="GLTF_SEPARATE",
|
||||
use_selection=True,
|
||||
export_keep_originals=True,
|
||||
export_image_format="AUTO",
|
||||
)
|
||||
with open(gltf_path, "r", encoding="utf-8") as handle:
|
||||
gltf = json.load(handle)
|
||||
for image in gltf.get("images", []):
|
||||
uri = image.get("uri")
|
||||
if not uri:
|
||||
continue
|
||||
# Do not delete the exporter URI. Blender can write it as an absolute
|
||||
# path or a relative traversal back to the user's downloaded source.
|
||||
# The final manifest owns one copied texture under textures/ instead.
|
||||
image["uri"] = "../textures/" + TEXTURE_NAME
|
||||
with open(gltf_path, "w", encoding="utf-8") as handle:
|
||||
json.dump(gltf, handle, indent=2)
|
||||
handle.write("\n")
|
||||
|
||||
|
||||
def render_preview(objects, path):
|
||||
scene = bpy.context.scene
|
||||
lower, upper = bounds(objects)
|
||||
span = upper - lower
|
||||
target = (lower + upper) / 2.0
|
||||
target.z = lower.z + span.z * 0.42
|
||||
camera_data = bpy.data.cameras.new("AssetPreviewCamera")
|
||||
camera = bpy.data.objects.new("AssetPreviewCamera", camera_data)
|
||||
scene.collection.objects.link(camera)
|
||||
camera.location = Vector((span.x * 1.18, -max(span.x, span.y) * 1.42, span.z * 0.92))
|
||||
direction = target - camera.location
|
||||
camera.rotation_euler = direction.to_track_quat("-Z", "Y").to_euler()
|
||||
camera.data.lens = 52
|
||||
scene.camera = camera
|
||||
floor_mesh = bpy.data.meshes.new("AssetPreviewFloor")
|
||||
floor_mesh.from_pydata([(-20, -20, 0), (20, -20, 0), (20, 20, 0), (-20, 20, 0)], [], [(0, 1, 2, 3)])
|
||||
floor = bpy.data.objects.new("AssetPreviewFloor", floor_mesh)
|
||||
scene.collection.objects.link(floor)
|
||||
floor_material = bpy.data.materials.new("AssetPreviewFloor")
|
||||
floor_material.diffuse_color = (0.13, 0.16, 0.18, 1.0)
|
||||
floor.data.materials.append(floor_material)
|
||||
for name, location, energy, size in (
|
||||
("AssetPreviewKey", (span.x, -span.y, span.z * 2.2), 1100, 5.0),
|
||||
("AssetPreviewFill", (-span.x, -span.y * 0.5, span.z * 1.2), 650, 4.0),
|
||||
("AssetPreviewRim", (0, span.y, span.z * 2.4), 900, 3.0),
|
||||
):
|
||||
data = bpy.data.lights.new(name, "AREA")
|
||||
data.energy = energy
|
||||
data.shape = "DISK"
|
||||
data.size = size
|
||||
light = bpy.data.objects.new(name, data)
|
||||
scene.collection.objects.link(light)
|
||||
light.location = location
|
||||
light.rotation_euler = (0, 0, 0)
|
||||
light.rotation_euler = (target - light.location).to_track_quat("-Z", "Y").to_euler()
|
||||
scene.render.engine = "BLENDER_EEVEE_NEXT"
|
||||
scene.render.resolution_x = PREVIEW_SIZE
|
||||
scene.render.resolution_y = PREVIEW_SIZE
|
||||
scene.render.resolution_percentage = 100
|
||||
scene.render.image_settings.file_format = "PNG"
|
||||
scene.render.filepath = path
|
||||
scene.world.color = (0.035, 0.045, 0.055)
|
||||
bpy.ops.render.render(write_still=True)
|
||||
for obj in [camera, floor] + [obj for obj in scene.objects if obj.name.startswith("AssetPreview") and obj not in {camera, floor}]:
|
||||
if obj.name in bpy.data.objects:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
|
||||
def metrics(objects):
|
||||
lower, upper = bounds(objects)
|
||||
return {
|
||||
"vertices": sum(len(obj.data.vertices) for obj in objects),
|
||||
"triangles": sum(len(loop.vertices) - 2 for obj in objects for loop in obj.data.polygons),
|
||||
"dimensions": [round(value, 4) for value in upper - lower],
|
||||
"materials": sorted({material.name for obj in objects for material in obj.data.materials if material}),
|
||||
}
|
||||
|
||||
|
||||
def gallery_html(assets):
|
||||
cards = "\n".join(
|
||||
"<article><img src=\"{preview}\" alt=\"{name}\"><h2>{name}</h2>"
|
||||
"<p>{category} · {triangles:,} tris</p><p>{dims} m</p><p>{materials}</p></article>".format(
|
||||
preview=asset["preview"], name=asset["name"], category=asset["category"],
|
||||
triangles=asset["metrics"]["triangles"],
|
||||
dims=" x ".join(str(value) for value in asset["metrics"]["dimensions"]),
|
||||
materials=", ".join(asset["metrics"]["materials"]),
|
||||
) for asset in assets
|
||||
)
|
||||
return """<!doctype html><meta charset=\"utf-8\"><title>LowPoly Cars</title>
|
||||
<style>body{margin:24px;font:14px system-ui,sans-serif;background:#edf1f3;color:#16212a}main{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:14px}article{background:#fff;border:1px solid #cbd5db;border-radius:6px;padding:10px}img{width:100%;aspect-ratio:1;object-fit:contain;background:#20282e}h2{font-size:15px;margin:8px 0 4px}p{margin:3px 0;color:#4a5862}</style><main>""" + cards + "</main>\n"
|
||||
|
||||
|
||||
def verify(output):
|
||||
manifest_path = os.path.join(output, "manifest.json")
|
||||
require_file(manifest_path, "manifest")
|
||||
with open(manifest_path, "r", encoding="utf-8") as handle:
|
||||
manifest = json.load(handle)
|
||||
if not manifest.get("license"):
|
||||
raise RuntimeError("Manifest must state the source license status")
|
||||
ids = [asset["id"] for asset in manifest.get("assets", [])]
|
||||
if len(ids) != len(set(ids)) or not ids:
|
||||
raise RuntimeError("Manifest has missing or duplicate asset IDs")
|
||||
for asset in manifest["assets"]:
|
||||
for field in ("model", "bin", "preview"):
|
||||
require_file(os.path.join(output, asset[field]), asset["id"] + " " + field)
|
||||
if asset["metrics"]["vertices"] <= 0 or asset["metrics"]["triangles"] <= 0:
|
||||
raise RuntimeError(asset["id"] + " has empty geometry")
|
||||
with open(os.path.join(output, asset["model"]), "r", encoding="utf-8") as handle:
|
||||
gltf = json.load(handle)
|
||||
uris = [image.get("uri") for image in gltf.get("images", [])]
|
||||
if uris != ["../textures/" + TEXTURE_NAME]:
|
||||
raise RuntimeError(asset["id"] + " does not reference the shared texture")
|
||||
require_file(os.path.join(output, "textures", TEXTURE_NAME), "shared texture")
|
||||
print("LOWPOLY_CARS_VERIFY", json.dumps({"assets": len(ids), "ids": ids}))
|
||||
|
||||
|
||||
def main():
|
||||
args = cli_args()
|
||||
if "verify" in args:
|
||||
verify(os.path.abspath(args["verify"]))
|
||||
return
|
||||
source = os.path.abspath(args.get("source", ""))
|
||||
texture_dir = os.path.abspath(args.get("textures", ""))
|
||||
output = os.path.abspath(args.get("output", ""))
|
||||
require_file(source, "source Blend")
|
||||
texture_path = os.path.join(texture_dir, TEXTURE_NAME)
|
||||
require_file(texture_path, "required shared texture")
|
||||
if os.path.exists(output):
|
||||
raise RuntimeError("Output already exists; remove it only after reviewing its contents: " + output)
|
||||
bpy.ops.wm.open_mainfile(filepath=source)
|
||||
# The source Blend is a display lineup. Hide it before thumbnail rendering;
|
||||
# the normalized temporary mesh objects remain renderable.
|
||||
for obj in bpy.context.scene.objects:
|
||||
obj.hide_render = True
|
||||
os.makedirs(output)
|
||||
shared_textures = os.path.join(output, "textures")
|
||||
os.makedirs(shared_textures)
|
||||
# Match the Cesium tree profile by baking the gain into the output pixels.
|
||||
setup_material_texture(bake_cesium_texture(texture_path, os.path.join(shared_textures, TEXTURE_NAME)))
|
||||
temp = bpy.data.collections.new("LowPolyCarsExport")
|
||||
bpy.context.scene.collection.children.link(temp)
|
||||
assets = []
|
||||
for root in roots():
|
||||
asset_dir = os.path.join(output, root.name)
|
||||
os.makedirs(asset_dir)
|
||||
objects = make_normalized_objects(root, temp)
|
||||
export_gltf(objects, asset_dir)
|
||||
render_preview(objects, os.path.join(asset_dir, "preview.png"))
|
||||
assets.append({
|
||||
"id": root.name,
|
||||
"name": root.name.replace("_", " ").title(),
|
||||
"category": category_for(root.name),
|
||||
"sourceRoot": root.name,
|
||||
"model": root.name + "/model.gltf",
|
||||
"bin": root.name + "/model.bin",
|
||||
"preview": root.name + "/preview.png",
|
||||
"metrics": metrics(objects),
|
||||
})
|
||||
reset_temp_collection(temp)
|
||||
manifest = {
|
||||
"source": os.path.basename(source),
|
||||
"license": "Unknown; verify the original download license before redistribution.",
|
||||
"texture": "textures/" + TEXTURE_NAME,
|
||||
"layout": "Each asset has model.gltf/model.bin/preview.png; the 512px color texture is shared in textures/.",
|
||||
"assets": assets,
|
||||
}
|
||||
with open(os.path.join(output, "manifest.json"), "w", encoding="utf-8") as handle:
|
||||
json.dump(manifest, handle, indent=2)
|
||||
handle.write("\n")
|
||||
with open(os.path.join(output, "index.html"), "w", encoding="utf-8") as handle:
|
||||
handle.write(gallery_html(assets))
|
||||
verify(output)
|
||||
print("LOWPOLY_CARS_DONE", json.dumps({"assets": len(assets), "output": output}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -12,7 +12,7 @@ const {
|
||||
} = require("./lib/scene-layers");
|
||||
const { digest: glbDigest } = require("./glb-digest");
|
||||
const { buildVehicleRoute: buildPreviewVehicleRoute } = require("./lib/vehicle-route");
|
||||
const { makeVehicleGltf: makePreviewVehicleGltf } = require("./lib/vehicle-model");
|
||||
const { writePreviewVehicleLibrary } = require("./lib/vehicle-library");
|
||||
const {
|
||||
cesiumPreviewHtml,
|
||||
previewSummary,
|
||||
@@ -468,13 +468,13 @@ function writeCesiumPreview(area) {
|
||||
const startedAt = new Date(started).toISOString();
|
||||
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
|
||||
writeVehicleRoute(area);
|
||||
writeVehicleModel(area);
|
||||
const vehicleModelNames = writeVehicleModel(area);
|
||||
writeCesiumPreviewSupportFiles(path.dirname(htmlPath));
|
||||
const glbName = path.basename(area.outputs.glb);
|
||||
const metadataName = path.basename(area.outputs.metadata);
|
||||
const routeName = path.basename(area.outputs.vehicleRoute);
|
||||
const vehicleModelName = path.basename(area.outputs.vehicleModel);
|
||||
fs.writeFileSync(htmlPath, cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, area.id));
|
||||
fs.writeFileSync(htmlPath, cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, area.id, vehicleModelNames));
|
||||
console.log(`Cesium preview: ${htmlPath}`);
|
||||
const finished = Date.now();
|
||||
writeStageManifest(area, {
|
||||
@@ -510,10 +510,14 @@ function writeVehicleRoute(area) {
|
||||
}
|
||||
|
||||
function writeVehicleModel(area) {
|
||||
const gltf = makePreviewVehicleGltf();
|
||||
fs.mkdirSync(path.dirname(area.outputs.vehicleModel), { recursive: true });
|
||||
fs.writeFileSync(area.outputs.vehicleModel, `${JSON.stringify(gltf, null, 2)}\n`);
|
||||
console.log(`Vehicle model: ${area.outputs.vehicleModel}`);
|
||||
const outDir = path.dirname(area.outputs.vehicleModel);
|
||||
const fileStem = path.basename(area.outputs.vehicleModel, ".gltf").replace(/-vehicle-car$/, "");
|
||||
const models = writePreviewVehicleLibrary(outDir, fileStem);
|
||||
// Preserve the existing output/manifest contract for old preview HTML.
|
||||
fs.copyFileSync(path.join(outDir, models[0]), area.outputs.vehicleModel);
|
||||
console.log(`Vehicle models: ${models.length} candidates in ${path.dirname(area.outputs.vehicleModel)}`);
|
||||
return models;
|
||||
}
|
||||
|
||||
function sceneGeojsonRecords(area) {
|
||||
|
||||
@@ -13,13 +13,14 @@ function writeCesiumPreviewSupportFiles(outDir) {
|
||||
}
|
||||
}
|
||||
|
||||
function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, areaId) {
|
||||
function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, areaId, vehicleModelNames = []) {
|
||||
const previewConfig = {
|
||||
areaId,
|
||||
glbName,
|
||||
metadataName,
|
||||
routeName,
|
||||
vehicleModelName,
|
||||
vehicleModelNames,
|
||||
};
|
||||
return `<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
const viewer = createViewer();
|
||||
setLoadingMessage("Loading model", config.glbName || "");
|
||||
const assets = await loadSceneAssets(viewer, metadata, placement);
|
||||
const cruise = addVehicleCruises(viewer, routeData, config.vehicleModelName);
|
||||
const cruise = addVehicleCruises(viewer, routeData, config.vehicleModelNames, config.vehicleModelName);
|
||||
const cameras = createCameraPresets(viewer, metadata, placement, cruise);
|
||||
|
||||
buildAssetToggles(assets);
|
||||
@@ -422,7 +422,7 @@
|
||||
return stopFollow;
|
||||
}
|
||||
|
||||
function addVehicleCruises(viewer, routeData, vehicleModelName) {
|
||||
function addVehicleCruises(viewer, routeData, vehicleModelNames, fallbackVehicleModelName) {
|
||||
const segments = ((routeData && (routeData.routes || routeData.segments)) || [])
|
||||
.filter((segment) => segment.coordinates && segment.coordinates.length >= 2)
|
||||
.slice(0, 5);
|
||||
@@ -436,7 +436,8 @@
|
||||
viewer.clock.shouldAnimate = segments.length > 0;
|
||||
|
||||
const vehicles = segments.map((segment, index) => {
|
||||
const vehicle = addCruiseVehicle(viewer, segment, index, start, speed, vehicleModelName);
|
||||
const vehicle = addCruiseVehicle(viewer, segment, index, start, speed,
|
||||
selectedVehicleModelName(vehicleModelNames, fallbackVehicleModelName));
|
||||
const option = document.createElement("option");
|
||||
option.value = String(index);
|
||||
option.textContent = routeLabel(segment, index);
|
||||
@@ -450,6 +451,14 @@
|
||||
};
|
||||
}
|
||||
|
||||
function selectedVehicleModelName(modelNames, fallbackModelName) {
|
||||
const choices = Array.isArray(modelNames) && modelNames.length
|
||||
? modelNames
|
||||
: [fallbackModelName];
|
||||
const usable = choices.filter(Boolean);
|
||||
return usable[Math.floor(Math.random() * usable.length)] || "";
|
||||
}
|
||||
|
||||
function addCruiseVehicle(viewer, segment, index, start, speed, vehicleModelName) {
|
||||
const route = prepareRoute(segment);
|
||||
const positions = new Cesium.CallbackProperty((time, result) => {
|
||||
@@ -482,9 +491,10 @@
|
||||
orientation: routeOrientation(route, start, speed, 0.0),
|
||||
model: {
|
||||
uri: vehicleModelName,
|
||||
scale: 1.0,
|
||||
minimumPixelSize: 24,
|
||||
maximumScale: 80
|
||||
// Keep the library's world scale visible. A minimum screen size made
|
||||
// long trucks balloon in overview and at intersections.
|
||||
scale: 0.72,
|
||||
minimumPixelSize: 0
|
||||
}
|
||||
});
|
||||
return {
|
||||
|
||||
49
scripts/lib/vehicle-library.js
Normal file
@@ -0,0 +1,49 @@
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const VEHICLE_IDS = [
|
||||
"car_a01_002",
|
||||
"car_a02_002",
|
||||
"car_a03_001",
|
||||
"truck_a01_001",
|
||||
"truck_a02_001",
|
||||
"truck_a03_001",
|
||||
];
|
||||
const LIBRARY_ROOT = path.resolve(__dirname, "..", "..", "assets", "models", "custom", "lowpoly_cars");
|
||||
const TEXTURE_NAME = "color_512x512.jpg";
|
||||
const REVERSED_MODEL_IDS = new Set(["truck_a03_001"]);
|
||||
|
||||
function writePreviewVehicleLibrary(outDir, fileStem) {
|
||||
const textureOutput = `${fileStem}-vehicle-texture.jpg`;
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
fs.copyFileSync(path.join(LIBRARY_ROOT, "textures", TEXTURE_NAME), path.join(outDir, textureOutput));
|
||||
return VEHICLE_IDS.map((id) => {
|
||||
const modelName = `${fileStem}-vehicle-${id}.gltf`;
|
||||
const binName = `${fileStem}-vehicle-${id}.bin`;
|
||||
const sourceDir = path.join(LIBRARY_ROOT, id);
|
||||
const gltf = JSON.parse(fs.readFileSync(path.join(sourceDir, "model.gltf"), "utf8"));
|
||||
for (const image of gltf.images || []) image.uri = textureOutput;
|
||||
for (const buffer of gltf.buffers || []) buffer.uri = binName;
|
||||
if (REVERSED_MODEL_IDS.has(id)) {
|
||||
// The source's longitudinal PCA axis points rearward for this truck.
|
||||
for (const node of gltf.nodes || []) node.rotation = [0, 1, 0, 0];
|
||||
}
|
||||
for (const material of gltf.materials || []) {
|
||||
const pbr = material.pbrMetallicRoughness || (material.pbrMetallicRoughness = {});
|
||||
// Match the Cesium foliage profile: lift albedo first, then use the
|
||||
// texture itself as a modest shadow-side floor.
|
||||
pbr.baseColorFactor = [2.1, 2.1, 2.1, 1];
|
||||
if (pbr.baseColorTexture) {
|
||||
material.emissiveTexture = { ...pbr.baseColorTexture };
|
||||
material.emissiveFactor = [0.25, 0.25, 0.25];
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(path.join(outDir, modelName), `${JSON.stringify(gltf, null, 2)}\n`);
|
||||
fs.copyFileSync(path.join(sourceDir, "model.bin"), path.join(outDir, binName));
|
||||
return modelName;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { VEHICLE_IDS, REVERSED_MODEL_IDS, writePreviewVehicleLibrary };
|
||||
@@ -7,6 +7,7 @@ const os = require("os");
|
||||
const path = require("path");
|
||||
const { cesiumPreviewHtml } = require("./lib/area-preview");
|
||||
const { makeVehicleGltf } = require("./lib/vehicle-model");
|
||||
const { VEHICLE_IDS, REVERSED_MODEL_IDS, writePreviewVehicleLibrary } = require("./lib/vehicle-library");
|
||||
const { allowedTurns, buildVehicleRoute, classifyConnection } = require("./lib/vehicle-route");
|
||||
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "preview-assets-"));
|
||||
@@ -85,17 +86,39 @@ assert.equal(
|
||||
vehicle.buffers[0].byteLength,
|
||||
);
|
||||
|
||||
const vehicleLibraryRoot = path.join(__dirname, "..", "assets", "models", "custom", "lowpoly_cars");
|
||||
if (fs.existsSync(vehicleLibraryRoot)) {
|
||||
const libraryDir = path.join(tempDir, "vehicle-library");
|
||||
const models = writePreviewVehicleLibrary(libraryDir, "fixture");
|
||||
assert.deepEqual(models, VEHICLE_IDS.map((id) => `fixture-vehicle-${id}.gltf`));
|
||||
assert.equal(models.length, 6);
|
||||
assert.ok(fs.existsSync(path.join(libraryDir, "fixture-vehicle-texture.jpg")));
|
||||
for (const model of models) {
|
||||
const gltf = JSON.parse(fs.readFileSync(path.join(libraryDir, model), "utf8"));
|
||||
assert.deepEqual(gltf.images.map((image) => image.uri), ["fixture-vehicle-texture.jpg"]);
|
||||
assert.equal(gltf.buffers[0].uri, model.replace(/\.gltf$/, ".bin"));
|
||||
assert.deepEqual(gltf.materials[0].pbrMetallicRoughness.baseColorFactor, [2.1, 2.1, 2.1, 1]);
|
||||
assert.deepEqual(gltf.materials[0].emissiveFactor, [0.25, 0.25, 0.25]);
|
||||
assert.equal(gltf.materials[0].emissiveTexture.index, gltf.materials[0].pbrMetallicRoughness.baseColorTexture.index);
|
||||
const reversed = model.includes("truck_a03_001");
|
||||
assert.equal(reversed, REVERSED_MODEL_IDS.has(model.match(/vehicle-(.+)\.gltf$/)[1]));
|
||||
assert.equal(gltf.nodes.every((node) => JSON.stringify(node.rotation || []) === JSON.stringify([0, 1, 0, 0])), reversed);
|
||||
}
|
||||
}
|
||||
|
||||
const html = cesiumPreviewHtml(
|
||||
"scene<&>.glb",
|
||||
"scene.json",
|
||||
"route.json",
|
||||
"vehicle.gltf",
|
||||
"north<&>\u2028valley",
|
||||
["car-a.gltf", "truck-a.gltf"],
|
||||
);
|
||||
assert.match(html, /<title>north<&>\u2028valley Cesium Preview<\/title>/);
|
||||
assert.match(html, /Loading scene<&>\.glb/);
|
||||
assert.match(html, /"areaId":"north\\u003c\\u0026\\u003e\\u2028valley"/);
|
||||
assert.match(html, /"glbName":"scene\\u003c\\u0026\\u003e\.glb"/);
|
||||
assert.match(html, /"vehicleModelNames":\["car-a\.gltf","truck-a\.gltf"\]/);
|
||||
assert.match(html, /id="viewMode"/);
|
||||
assert.match(html, /data-view-mode="inspect"/);
|
||||
assert.match(html, /id="semanticToggles" class="control-subgroup hidden"/);
|
||||
|
||||