正常路径之外的**字段边界**同样只在跨端跑起来才暴露。配合 Web v0.7.5 增加:
1. 不可用模型:用 Assimp 把真实 PMX 转成 GLB 后入库(等价"管线只产出 GLB"),
Web 侧断言 usable=false、原因准确(骨架达标时不得说"humanoid 不达标")、
不在 catalog/export、defaultModelId 不变;Windows 侧(包内探针)断言
bundle.models 里没有任何非 VRM 模型、按 id 找不到该模型、defaultModelId 仍指向可用 VRM。
2. 场景 config 只给 {physics:{gravity:{y:-9.8}}}:Web 侧断言服务端补齐默认
(碰撞体 auto、地面 0.6/45°、静风、相机 follow、环境光 1、出生点 1 条);
Windows 侧断言客户端解析出的**同一组默认值**逐项一致。
实测(Web v0.7.5 × Windows v0.2.5 包内 pck,真实 MySQL):
✓ usable=false 且原因为「未产出 VRM(格式为 GLB;humanoid 覆盖 19/22 已达标)…」
✓ 目录里的模型全是 VRM/不可用模型 #2 不在目录里/defaultModelId 仍为 1
✓ 最简配置:重力 -9.8 保留,碰撞体 auto、地面 0.6/45°、静风、相机 follow、
环境光 1.0、出生点 1 条,两端逐项一致
跨端集成验收全部通过。
顺带修掉播种脚本自身的顺序错误(把依赖 catalog 的断言插到了 catalog 取回之前),
以及把断言改成「先取目录再断言」。不影响发布产物(build/ 已排除出 pck)。
427 lines
28 KiB
Markdown
427 lines
28 KiB
Markdown
# opendance-windows
|
||
|
||
OpenDance Studio **阶段二**: Windows 客户端(Godot 4)。
|
||
|
||
接收 Android 辅助 App 通过 WebSocket 推送的 33 点骨骼关键点,驱动 VRM 角色,
|
||
并与 Web 管理服务端下发的参考动作做实时 DTW 评分。
|
||
|
||
> 本客户端只接收关键点坐标,**不接收、不存储任何视频画面**。
|
||
|
||
## 状态
|
||
|
||
**阶段二已完成**: T-WIN-001 -> T-WIN-014 全部落地(含场景选择与整包发布)。
|
||
|
||
| 验收项 | 结果 |
|
||
|--|--|
|
||
| 脚本编译 | OK 全部通过(无 SCRIPT ERROR) |
|
||
| 无头测试 | OK **751 项全部通过**(10 个测试套件) |
|
||
| 应用启动 | OK 180 帧无运行时错误 |
|
||
| Windows 导出 | OK 生成 `OpenDance.exe`(104 MB, PE 头校验通过)+ `OpenDance.pck` |
|
||
| 真实 VRM 验证 | OK 154 骨骼 VRoid 模型,humanoid 覆盖率 **100%** |
|
||
| 骨骼重定向 | OK 关键点方向与骨骼实际指向点积 **1.0000** |
|
||
| 跨实现交叉校验 | OK 关节角与 Web 服务端 TypeScript 实现**逐帧一致** |
|
||
| 场景选择与物理落地 | OK `scenes[]` 可选、重力/风/摩擦/碰撞体落地并可日志核对 |
|
||
| 连接保活与设备去重 | OK welcome/config 声明静默超时与心跳;应用层 ping/pong;按 deviceId 去重 |
|
||
| 发布整包 | OK 单个 `opendance-windows-v0.2.5.zip` + `SHA256SUMS`(不拆分卷) |
|
||
| 版本号展示 | OK 「关于」清单显示 `v0.2.2`;`project.godot` / `project.yaml` / `export_presets.cfg` / exe 的 PE 版本资源 / 产物名 / tag 全部一致 |
|
||
| 场景物理与保活 | OK 新增真实 GLB 端到端用例 + 17 秒保活对照用例(751 项断言全过) |
|
||
|
||
## v0.2.5 修订(config 改为变更驱动 + 契约「可选/兜底」语义定向验收)
|
||
|
||
**缺陷**:`config` 被当成心跳 —— 每 2 秒**无条件**广播一次(契约 §3.3 的语义是
|
||
"运行中**调整**时下发")。一次 30 分钟推流 = 900 条无信息量的消息,而且让
|
||
「收到 config」无法被当成「配置变了」的信号。实测 v0.2.4:17 秒内 9 条 → v0.2.5:**1 条**。
|
||
修复:抽出 `config_payload()` 作为唯一载荷与指纹来源;`hello` 应答固定为
|
||
**先 `welcome` 再 `config`**(避免新对端第一条消息变成 config);之后仅当载荷变化才广播,
|
||
并用 `CONFIG_PUSH_INTERVAL_MS` 去抖。
|
||
|
||
**新增 3 个契约语义场景**(`accept-ws.sh` / `ws-client.mjs`,总 30 → **40 项断言**):
|
||
* **v0.1.0 风格对端**:只发二进制帧(每 500ms 一帧)、**从不 ping**、`hello` 不带 `deviceId`,
|
||
持续 17 秒(> 静默超时)→ 连接必须保持(契约 §1.1 的向后兼容承诺);
|
||
* **两条都不带 `deviceId`** 的连接同时在线 → 不做去重,按连接计数(§5);
|
||
* **完全不发 `hello`**、只发 5 帧的对端 → 不断开、不崩、帧仍计入。
|
||
|
||
顺带修掉验收脚本自身两处**误判**:把「跨连接的 frame_id 回退」当缺陷(`frame_id` 是
|
||
每条连接各自从 0 递增,新连接重新开始是合规的)、把「静默超时次数」当固定值
|
||
(空闲连接本来就会被正常超时);现改为**定向断言**:由 hello 记录建立
|
||
`client_id → device` 映射,只检查 v0.1.0 风格对端没有被误踢。
|
||
测试客户端也改为**按类型取消息**(`nextOfType`)—— 契约没规定消息顺序,客户端必须顺序无关。
|
||
|
||
## v0.2.4 修订(编码取整对齐 + 产物不再夹带测试夹具)
|
||
|
||
**缺陷 1(编码取整与参考实现不一致)**:`KeypointFrame.encode()` 用 GDScript 的
|
||
`roundi`(半数远离零),而契约参考实现(Web 的 `Math.round`)与**生产发送方 Android**
|
||
(Kotlin `Math.round`)都是「半数朝 +∞」—— `-2.5` 分别是 `-3` 与 `-2`。
|
||
实测在 12 个跨实现向量里有 1 例 **33 字节不同**。本端是接收方、`encode()` 只用于自测,
|
||
对产品无影响;但它让「跨实现一致性」只能停留在宽松口径("差异仅限取整边界"),
|
||
而宽松口径会退化成永远没人看的已知差异。**修复**:新增 `_round_like_js(v) = floor(v + 0.5)`
|
||
并用于 4 处取整;`accept-frame-vectors.sh` 的口径**收紧为「编码差异必须为 0」**。
|
||
|
||
**缺陷 2(产物里夹带开发/验收工具)**:`export_filter="all_resources"` +
|
||
`exclude_filter` 没有 `build/*`,于是每次新增构建/验收脚本都会被编译进 pck ——
|
||
包括**故意不合规**的坏服务端夹具 `probe-ws-bad.gd` 与向量文件。
|
||
`OpenDance.pck` 因此从 145,400 B 涨到 **315,464 B(54% 是测试夹具)**。
|
||
**修复**:`exclude_filter` 增加 `build/*`;并新增**产物级防线**
|
||
`build/integration/list-pck.gd`(遍历包内条目、标出开发残留)+ `accept-release.sh`
|
||
断言「无残留」,避免下次加个新工具目录又重演。
|
||
|
||
## v0.2.3 修订(跨端集成验收 + 两处真实缺陷)
|
||
|
||
**缺陷 1(契约字段缺失)**:`CatalogTypes.Motion` 没有 `thumbnail_url` 字段,
|
||
而服务端 `catalog/export` 从 `data-models 0.6.0` 起一直在发 `thumbnailUrl`
|
||
(同一文件的 `Model` / `Scene` 都有)。客户端默默丢掉该字段。
|
||
**修复**:补字段与解析(缺失时为 `""`,兼容旧服务端),并新增
|
||
`tests/test_catalog_types.gd`(4 项)用**服务端真实报文形状**锁住三个资源类型的
|
||
`thumbnailUrl` 覆盖;同时显式记录一处**刻意不消费**的差异
|
||
(`Model.usable`/`unusableReason` —— 目录只下发可用模型,恒为 true)。
|
||
|
||
**缺陷 2(二进制响应白做 UTF-8 解码)**:`ApiClient._request()` 对**所有**响应
|
||
无条件执行 `raw.get_string_from_utf8()`。数据本身没错(二进制路径返回的是原始字节),
|
||
但每下载一个文件都会打印一条
|
||
`Unicode parsing error … Invalid UTF-8 leading byte (N)` 噪声,
|
||
并把整个文件(模型可达数十 MB)解码成一份 String 拷贝后丢弃。
|
||
**修复**:只在需要文本时解码。跨端集成探针第一次注意到这条噪声。
|
||
|
||
**新增跨端集成验收**(`build/accept-integration.sh`):用**发布包里的客户端**
|
||
(`--main-pack` 加载包内 pck)打**真实 Web 服务端**,逐项比对契约与物理参数,
|
||
并跑通「真 VMD → 内部标准 33 点 → 客户端解析 2901 帧」的完整闭环。
|
||
详见 [`tasks/019-cross-end-integration.md`](tasks/019-cross-end-integration.md)。
|
||
|
||
此外修掉 `tests/test_api_integration.gd` 里一处**写死的契约版本**(`0.1.0`):
|
||
该用例只在服务端可达时才跑,平时一直 skip,于是契约升到 0.5.0 后
|
||
没有任何地方发现它。现在改为从仓库 `contracts/VERSION` 读期望值并与服务端 `/health` 比对。
|
||
|
||
## v0.2.2 修订(发布包 exe 的版本元数据)
|
||
|
||
**缺陷**:v0.2.1 的包在 Windows 上「右键 → 属性 → 详细信息」显示
|
||
**文件版本 0.1.0 / 产品版本 0.1.0**,而产物名与 Git tag 都是 v0.2.1。
|
||
|
||
**根因**:`export_presets.cfg` 的 `application/file_version` /
|
||
`application/product_version` 被硬编码在 `0.1.0`,而它们**不会**跟随
|
||
`project.godot` 的 `application/config/version` 自动更新;`check-version.sh`
|
||
当时只比 `project.godot` ↔ `project.yaml`(两处一致),所以静态校验全绿、
|
||
产物却是错的 —— 漂移发生在没人看的第三个位置。
|
||
|
||
**修复**:
|
||
|
||
| 环节 | 做法 |
|
||
|--|--|
|
||
| 同步 | `build/sync-export-version.sh`(新增):导出前把版本号写进导出预设(幂等,拒绝非 `X.Y.Z`) |
|
||
| 静态防线 | `build/check-version.sh`:新增对导出预设两个键的校验 |
|
||
| 产物防线 | `build/check-pe-version.sh`(新增):直接读 exe 的 PE 版本资源 —— `VS_FIXEDFILEINFO`(签名 `0xFEEF04BD` 后 16 字节)+ `StringFileInfo`(`strings -el`,属性页显示的就是它)。两处都查:本机实测固定信息里的 `ProductVersion` 恒为 `0.0.0.0` |
|
||
| 打包 | `build/package-release.sh`:打包前校验源 exe、解包后再校验包内 exe |
|
||
| 一键验证 | `build/verify.sh`:新增步骤 1/5(未导出则跳过) |
|
||
| 单元测试 | `tests/test_about.gd`:预设两个键必须等于版本号唯一来源 |
|
||
|
||
实测修复后产物:`VS_FIXEDFILEINFO FileVersion = 0.2.2.0`、
|
||
`StringFileInfo FileVersion/ProductVersion = 0.2.2`。详见
|
||
[`tasks/017-pe-version.md`](tasks/017-pe-version.md)。
|
||
|
||
## v0.2.1 修订(两处修复 + 一批"真机等价"测试)
|
||
|
||
1. **场景包围盒不再用 `global_transform`**:`SceneRuntime._compute_aabb()` 原本在
|
||
`_assemble()` 里调用,而那时运行时节点**还没入树** —— Godot 会打一行
|
||
`ERROR: Condition "!is_inside_tree()" is true`,并且算出来的只是各网格的
|
||
**局部** AABB。后果:把网格摆在非原点位置的场景,重力/风区域与兜底地面会算得
|
||
偏小或偏位(**风力可能作用不到物体上**)。现改为沿树累乘 `Node3D.transform`,
|
||
与是否入树无关,结果与运行时挂在单位变换父节点下完全一致;
|
||
2. **新增 21 项"真机等价"测试**(无头即可跑,不需要设备):
|
||
* `tests/test_scene_config.gd`:在代码里手写合法 GLB(三角形 + BIN chunk)后
|
||
**真实加载**,核对三种 `colliders` 取值的碰撞体、重力/风 Area3D 数值、
|
||
以及**带位移网格**的包围盒是否跟着走(这一条正是上述缺陷的回归测试);
|
||
* `tests/test_pose_server.gd`:**时间维度**验证保活 —— 一个只发应用层 `ping`、
|
||
不发关键点帧的客户端在 17 秒(> 15 秒静默超时)后**仍保持连接**,
|
||
而完全不发数据的客户端被 `close(1001)` 断开。这正是"画面里没人就掉线、
|
||
然后疯狂重连"的根因对照;
|
||
3. **新增版本号一致性校验** `build/check-version.sh`:强制
|
||
`project.godot` 的 `application/config/version`(「关于」清单与 .pck 的来源)
|
||
与 `project.yaml`、发布版本号三者一致;已接入 `build/package-release.sh`
|
||
(打包前)与 `build/verify.sh`(0/4 步),并做过反向验证。
|
||
|
||
## 快速开始
|
||
|
||
```bash
|
||
# 验证(编译 -> 测试 -> 启动)
|
||
GODOT_BIN=/path/to/godot bash build/verify.sh
|
||
|
||
# 导出 Windows 可执行文件
|
||
GODOT_BIN=/path/to/godot bash build/export-windows.sh release
|
||
|
||
# 打包发布产物(单个完整 zip + SHA256SUMS)
|
||
bash build/package-release.sh 0.2.5
|
||
|
||
# 发布包级验收(对**发布包**跑,而不是对源码;文档 §7.5)
|
||
GODOT_BIN=/path/to/godot bash build/accept-release.sh dist-release/opendance-windows-v0.2.5.zip
|
||
```
|
||
|
||
`accept-release.sh` 校验 sha256 → 解压 → 文件清单(并检查没有 `.partN` 残留)→
|
||
exe 的 PE 版本资源 → **用 Godot 直接加载包内 pck 跑 180 帧** → 包内运行时探针
|
||
(`build/probe-release.gd`:包内版本号、`AboutScreen.app_version()`、主场景、autoload、
|
||
15 个关键资源、以及**包内 `Credits.CONTRACT_VERSIONS` 与 `contracts/VERSION` 快照是否一致**)。
|
||
它读的全是包内资源,因此结论就是「用户装到机器上的那一份能不能用」。
|
||
已对**已发布的 v0.2.2 资产**(重新下载)跑通,并用「篡改包内 contracts/VERSION」与
|
||
「期望版本写错」两个负例反向验证过。
|
||
|
||
### 关键点帧跨实现一致性验收(T-WIN-021)
|
||
|
||
`web-server/src/preprocess/keypoint-codec.ts` 是契约 §2 的**参考实现**
|
||
(其 `decodeBinaryFrame` 注释就是「Windows 端参考实现(T-WIN-004)」)。
|
||
Android 已用「金标准字节向量」与它对齐;而**本端的解码器此前只跟自己比过**
|
||
(自己编码、自己解码)。这条边由下列脚本补上:
|
||
|
||
```bash
|
||
GODOT_BIN=/path/to/godot bash build/accept-frame-vectors.sh dist-release/opendance-windows-v0.2.5.zip
|
||
# 没有 web 仓 / 未装 node_modules 时:... --skip-regen
|
||
```
|
||
|
||
1. 用**参考实现**(`npm run vectors:frame`)重新生成向量,与仓库锁定的
|
||
`build/integration/frame-vectors.json` 比对 —— 参考实现变了却不同步向量,这里就会失败;
|
||
2. 用**包内 pck** 解码同一批字节,与参考解码结果**逐值精确比对**
|
||
(float32 表示精度内):实测 **12 用例 / 396 点 / 0 差异**;
|
||
3. 附带比对两边编码器:本端不是发送方(`encode()` 只用于自测),差异只归类记录,
|
||
但**必须只出现在「量化前恰好 .5」的用例上**。
|
||
|
||
> 已测量并归类的一处差异:JS `Math.round` 与 Kotlin `Math.round` 对 `-2.5` 取 `-2`
|
||
> (半数朝 +∞),而本端 `roundi` 取 `-3`(半数远离零)。生产发送方是 Android,
|
||
> 与参考实现一致;Windows 是接收方,`encode()` 不在生产链路上。
|
||
> 详见 [`tasks/021-frame-vector-conformance.md`](tasks/021-frame-vector-conformance.md)。
|
||
|
||
### WebSocket 契约一致性验收(T-WIN-020)
|
||
|
||
`tests/test_pose_server.gd` 用的是**同引擎内的客户端**,而真实对端是
|
||
**Android(Kotlin/OkHttp)** —— 同引擎自测会共享同样的假设(超时判定、分包、
|
||
文本/二进制边界、关闭语义、子协议协商)。因此另配一套**独立实现**的客户端:
|
||
|
||
```bash
|
||
GODOT_BIN=/path/to/godot bash build/accept-ws.sh dist-release/opendance-windows-v0.2.5.zip
|
||
# 跳过耗时的两个场景(保活 19s / 静默超时 15s):
|
||
GODOT_BIN=... bash build/accept-ws.sh <zip> --skip-slow
|
||
```
|
||
|
||
* `build/integration/probe-ws.gd`:把**包内** PoseServer 跑起来并汇总事件成 JSON(自身不判定);
|
||
* `build/integration/ws-client.mjs`:Node 实现的契约一致性客户端(无依赖,用内置 `WebSocket`),
|
||
逐条跑 §1 §1.1 §2 §3.1-§3.6 §5 §6 §7 的场景;
|
||
* `build/accept-ws.sh`:编排两者,并核对**客户端看不到的那一半**(服务端侧
|
||
`frames_dropped` / `malformed_frames` / 控制帧计数 / 去重事件 / 关闭原因)。
|
||
|
||
**契约边界场景**(配合 Web v0.7.5):① **不可用模型**(真实 PMX 经 Assimp 转 GLB 后入库)
|
||
必须 `usable=false`、**不进入** `catalog/export`、不影响 `defaultModelId`,且包内客户端
|
||
`bundle.models` 里没有任何非 VRM 模型;② 场景 `config` **只给最少字段**时,服务端补齐的默认值
|
||
与客户端解析出的默认值**逐项一致**(碰撞体 `auto`、地面 `0.6/45°`、静风、相机 `follow`…)。
|
||
|
||
实测(已发布的 v0.2.5 资产):客户端 **40 通过 / 0 失败** + 服务端侧 16 项事实全过,
|
||
其中最关键的是**保活对照**——只发应用层 `ping` 持续 19 秒(> 静默超时 15 秒)连接
|
||
**仍保持**,而完全不发数据的连接在 **15011 ms**(阈值 15000 ms)被 `close(1001, "timeout")`。
|
||
反向验证用一个**故意不合规的服务端**(`build/integration/probe-ws-bad.gd`:welcome 不声明
|
||
保活字段、不回 pong、不做去重)跑同一客户端 → 14 项 ✗、退出码 1,证明断言不是空转。
|
||
详见 [`tasks/020-ws-contract-conformance.md`](tasks/020-ws-contract-conformance.md)。
|
||
|
||
当前版本:**v0.2.5**(`project.godot` 的 `application/config/version`,
|
||
与 `project.yaml`、`export_presets.cfg`、导出产物名、Git tag、「关于」清单展示、
|
||
以及 exe 的 PE 版本资源全部一致)。
|
||
|
||
详细使用说明见 [`README.txt`](README.txt),工程约束见 [`DSH.md`](DSH.md)。
|
||
|
||
## 初次部署(从零跑起来)
|
||
|
||
本节面向**第一次拿到本仓库**的人:从安装引擎到跑通「Web 服务端 + 本端 + Android」
|
||
三端链路。地址与端口一律使用占位符或产品默认值。
|
||
|
||
### 1. 前置条件
|
||
|
||
| 项 | 要求 |
|
||
|--|--|
|
||
| 操作系统 | 运行客户端:Windows 10/11(x86_64)。开发与导出:Windows 或 Linux 均可 |
|
||
| Godot | **4.7.x**(`project.godot` 声明特性 `4.7`,本仓库按 4.7.2 验证)。使用**标准版**(GDScript), **不需要 .NET / C# 版** |
|
||
| 显卡与图形界面 | 渲染后端为 **GL Compatibility**,运行需支持 OpenGL 3.3 的显卡与驱动。仅无头测试与导出可无 GPU、可无图形界面 |
|
||
| 导出模板 | **仅打包导出时需要**:必须安装与编辑器**完全同版本**的 export templates(`.tpz`) |
|
||
| 磁盘与网络 | Godot 编辑器 + 导出模板约数百 MB;导出产物 `OpenDance.exe` 约 104 MB、`OpenDance.pck` 约 107 KB。本端需能访问 Web 服务端 API(默认 TCP 3000),并与 Android 设备处于同一局域网、放行本端 **TCP 8765 入站** |
|
||
|
||
### 2. 获取代码
|
||
|
||
```bash
|
||
git clone https://gitea.kasugano.cn/zijunchiang/opendance-windows
|
||
cd opendance-windows
|
||
```
|
||
|
||
### 3. 首次运行(编辑器内)
|
||
|
||
1. 启动 Godot 4.7.x,在项目管理器选择「导入」,指向本仓库的 `project.godot`。
|
||
2. **首次导入会执行资源导入**(生成 `.godot/`),耗时从数十秒到数分钟不等;请等待完成,不要在导入中途关闭编辑器。
|
||
3. 主场景已在 `project.godot` 中配置为 `res://scenes/main.tscn`,直接按 **F5**(或「运行项目」)即可。
|
||
4. 界面全部由 GDScript 代码构建,不需要在编辑器里编辑场景,这是刻意设计(见 `DSH.md` 6)。
|
||
5. 首次运行会在用户数据目录生成配置与日志:
|
||
|
||
```
|
||
%APPDATA%\Godot\app_userdata\OpenDance Studio\opendance.cfg
|
||
%APPDATA%\Godot\app_userdata\OpenDance Studio\logs\godot.log
|
||
```
|
||
|
||
### 4. 配置服务端地址
|
||
|
||
- 位置:主界面顶部「**服务端地址**」输入框,填好后点「**保存并测试**」。
|
||
- 配置字段名(`opendance.cfg`):`api_base_url`。
|
||
- **端口区分**:客户端只访问 Web 服务端的 **API 端口(默认 3000)**;管理端页面在**管理端口(默认 3001)**,客户端不使用该端口。
|
||
- 示例占位符:
|
||
|
||
```
|
||
http://<服务器地址>:3000
|
||
http://192.0.2.10:3000
|
||
```
|
||
|
||
> `<服务器地址>` 是**你们自己部署的 Web 服务端地址**(部署方决定,不是被脱敏的域名);
|
||
> 上面第二行是 RFC 5737 文档用示例 IP,照抄时请替换。
|
||
|
||
保存后点「**刷新目录**」;顶部应显示「服务端正常 | DB ...」,左侧列出已发布的曲目 / 动作 / 模型。
|
||
|
||
### 5. 与另外两端的联通
|
||
|
||
| 对端 | 方向 | 本端配置 | 说明 |
|
||
|--|--|--|--|
|
||
| Android 辅助 App | Android -> 本端(WebSocket) | `listen_host`(默认 `0.0.0.0`)、`listen_port`(默认 **8765**)、`ws_subprotocol`(默认 `opendance.pose.v1`) | 本端是 WebSocket **服务端**; Android 端填写本端主界面显示的「接收地址」,形如 `ws://<本机局域网地址>:8765/pose` |
|
||
| Web 服务端 | 本端 -> Web(HTTP) | `api_base_url`(默认 `http://127.0.0.1:3000`) | 需先在服务端**发布**曲目 / 动作 / 模型 / 场景,动作还须**完成关键点预处理**,否则目录为空或该项不可选 |
|
||
|
||
> **场景不是必需项**:服务端没发布任何可用场景(或缺 GLB)时,本端回退到内置空场景
|
||
> 并在界面提示,**不会阻塞开始跟练**(文档 §5.6 第 4 条)。
|
||
|
||
- **Android 侧无本端可配置项**:接收地址由 Android App 自己填写;本端只监听固定端口 8765(可在 `opendance.cfg` 改 `listen_port` 后点「重新连接接收服务」生效)。
|
||
- 已知限制:Godot 的 `WebSocketPeer` 不暴露握手请求路径,本端**接受任意 path**,只校验子协议 `opendance.pose.v1`(见「已知限制」1)。
|
||
- 接收状态可用主界面「**查看接收状态**」查看:连接数、**去重后设备数**、累计帧、
|
||
丢帧、畸形帧、保活参数,以及每台设备的状态(契约 §8)与首帧时间。
|
||
|
||
### 6. 打包导出
|
||
|
||
```bash
|
||
# Linux 上直接导出 Windows 可执行文件(推荐)
|
||
GODOT_BIN=/path/to/godot bash build/export-windows.sh release
|
||
|
||
# Windows 上兜底导出
|
||
powershell -ExecutionPolicy Bypass -File build\export-windows.ps1
|
||
|
||
# 打成可分发的 zip(单个完整包 + SHA256SUMS)
|
||
bash build/package-release.sh 0.2.5
|
||
```
|
||
|
||
| 产物 | 位置 | 说明 |
|
||
|--|--|--|
|
||
| `OpenDance.exe` | `build/windows/` | 主程序,约 104 MB;脚本校验前 2 字节为 `MZ`(合法 PE) |
|
||
| `OpenDance.pck` | `build/windows/` | 资源包,约 107 KB; **必须与 exe 同目录** |
|
||
| `OpenDance.console.exe` | `build/windows/` | 控制台包装器,可能生成,非必需 |
|
||
| `opendance-windows-v0.2.5.zip` | `dist-release/` | 发布包:exe + pck + `THIRD_PARTY.md` + `README.txt` + `README.md` + `contracts/` |
|
||
| `SHA256SUMS` | `dist-release/` | 裸文件名,与 zip 同目录时可 `sha256sum -c SHA256SUMS` 直接校验 |
|
||
|
||
导出前必须安装与编辑器同版本的 export templates,否则脚本会报「未找到导出模板」。
|
||
|
||
### 7. 自检清单
|
||
|
||
| 检查 | 命令 | 预期输出 |
|
||
|--|--|--|
|
||
| 一键验证(编译 + 测试 + 启动) | `GODOT_BIN=/path/to/godot bash build/verify.sh` | 末行「验证全部通过」,退出码 0 |
|
||
| 无头单元 / 集成测试 | `$GODOT --headless --path . --script res://tests/run_tests.gd` | `== 结果:687 通过 / 0 失败 ==` 与 `全部通过`(仓库基线 687 项 / 10 个套件;缺 VRM 素材或服务端不可达时相关用例显式跳过,数字会减少) |
|
||
| 启动冒烟(180 帧) | `$GODOT --headless --path . --quit-after 180` | 无 `SCRIPT ERROR` / `ERROR:`,进程退出码 0 |
|
||
| 日志检查 | 打开 `%APPDATA%\Godot\app_userdata\OpenDance Studio\logs\godot.log` | 无 `SCRIPT ERROR`、无 `ERROR:` |
|
||
| 导出产物校验 | `bash build/export-windows.sh release` | 输出「PE 头校验通过」,且 `build/windows/OpenDance.exe` 前 2 字节为 `4d5a` |
|
||
| 服务端联通 | 主界面「服务端地址」->「保存并测试」 | 顶部显示「服务端正常 | DB ...」 |
|
||
|
||
### 8. 常见问题
|
||
|
||
| 现象 | 原因 | 处理 |
|
||
|--|--|--|
|
||
| 游戏界面提示「模型加载失败:缺少骨骼」或「骨骼绑定失败,模型可能不受支持」 | 模型没有 `Skeleton3D`,或骨骼命名不在 humanoid 映射表内;驱动至少需要 hips / spine / neck / head + 四肢共 16 根核心骨骼 | 换用标准 VRM(VRoid Studio 导出);看「人形覆盖率」,低于 60% 会提示「骨骼覆盖偏低,动作可能不完整」;必要时在 `src/model/humanoid_map.gd` 增加命名映射 |
|
||
| 顶部「服务端不可达」或「目录拉取失败」 | Web 服务端未启动、地址或端口写错(误填管理端口 3001)、或网络不通 | 确认 `api_base_url` 为 `http://<服务器地址>:3000`;浏览器访问 `http://<服务器地址>:3000/health` 应返回 200;再点「保存并测试」 |
|
||
| 导出失败:「未找到导出模板」 | 未安装 export templates,或版本 / 路径与编辑器不一致 | 编辑器「编辑器 -> 管理导出模板 -> 下载并安装」;Linux 放 `~/.local/share/godot/export_templates/<版本>.stable/`, Windows 放 `%APPDATA%\Godot\export_templates\<版本>.stable\` |
|
||
| 提示「接收服务启动失败(端口 8765 可能被占用)」 | TCP 8765 被占用或未放行 | 改 `opendance.cfg` 的 `listen_port` 后点「重新连接接收服务」,并同步修改 Android 端填写的接收地址 |
|
||
| 手机已连接但分数一直是 0 | Android 端未开始推流,或帧内关键点置信度都低于 0.5 | 「查看接收状态」确认累计帧在增长、畸形帧为 0;确认 Android 端子协议为 `opendance.pose.v1` |
|
||
|
||
## 技术栈
|
||
|
||
| 组件 | 选型 |
|
||
|--|--|
|
||
| 引擎 | Godot 4.7.x(MIT) |
|
||
| VRM 加载 | Godot 内置 GLTFDocument |
|
||
| 通信 | WebSocketPeer(服务端)+ HTTPRequest |
|
||
| 评分 | DTW + 关节角度差异(纯 GDScript) |
|
||
|
||
## 架构
|
||
|
||
```
|
||
Android 辅助 App ──239 字节关键点帧──▶ 本端 WebSocket 服务端:8765
|
||
│
|
||
Web 服务端 ──GET /api/catalog/export────────┤
|
||
──GET /api/motions/:id/keypoints ┘
|
||
▼
|
||
VRM 驱动 + DTW 评分
|
||
```
|
||
|
||
```
|
||
src/
|
||
├── autoload/ AppConfig / CatalogStore / PoseReceiver / ScoreService
|
||
├── net/ 239 字节编解码、WebSocket 服务、REST 客户端、NDJSON 解析
|
||
├── model/ VRM 加载、humanoid 骨骼名映射
|
||
├── anim/ 关键点 -> 骨骼重定向
|
||
├── scene/ 场景实例化与物理落地(重力/风/碰撞/环境/机位)
|
||
├── score/ 特征提取、DTW、评分规则
|
||
└── ui/ 主界面 / 游戏界面 / 结算 / 关于 / 来源展示 / 场景参数面板
|
||
```
|
||
|
||
界面**全部用 GDScript 代码构建**(不手写 `.tscn`),因此可被无头导入与单元测试覆盖。
|
||
|
||
## 依赖的契约(opendance-web 冻结,逐项独立版本)
|
||
|
||
| 契约 | 版本 |
|
||
|--|--|
|
||
| `rest-api` | 0.5.0 |
|
||
| `data-models` | 0.7.0 |
|
||
| `websocket-keypoints` | 0.2.0 |
|
||
| `skeleton-topology` | 0.2.0 |
|
||
| `model-formats` | 0.5.0 |
|
||
| `database` | 0.4.0 |
|
||
|
||
- [`contracts/websocket-keypoints.md`](contracts/websocket-keypoints.md) - 239 字节关键点帧与文本控制消息(本端为**接收方**)
|
||
- [`contracts/skeleton-topology.md`](contracts/skeleton-topology.md) - MediaPipe 33 点拓扑、父节点表、9 个关节角特征
|
||
- [`contracts/rest-api.yaml`](contracts/rest-api.yaml) - 目录与关键点序列 API
|
||
- [`contracts/data-models.md`](contracts/data-models.md) - 资源字段与 VRM 优先规则
|
||
- [`contracts/model-formats.md`](contracts/model-formats.md) - 模型与动作格式、关键点序列结构
|
||
- [`contracts/database.md`](contracts/database.md) - 数据库表结构(客户端只读快照,不直连数据库)
|
||
|
||
`contracts/` 是**只读快照**。任何跨端契约变更必须先回到
|
||
[`opendance-web`](https://gitea.kasugano.cn/zijunchiang/opendance-web) 修改并提升版本,
|
||
再在本仓库创建适配任务。
|
||
|
||
## 已知限制
|
||
|
||
1. Godot 的 `WebSocketPeer` 不暴露握手请求路径,因此**无法校验 path 是否为 `/pose`**,
|
||
实现接受任意 path,仅校验子协议 `opendance.pose.v1`。
|
||
2. 文档原选型 `godot-vrm` GDExtension 不发布预编译产物,改用 Godot 内置 glTF 导入器
|
||
+ 自实现骨骼名映射;理由见 `DSH.md` 6。
|
||
3. `build/export-windows.ps1` 未在真实 Windows 上执行过(本环境无 Windows)。
|
||
两条路径共用同名导出预设与相同的 PE 头校验逻辑。
|
||
4. 真实 VRM 样例仅放在 `_scratch/`(已 gitignore),版权原因不入库;
|
||
测试在找不到模型时显式跳过而非伪装通过。
|
||
5. **Gitea 附件上传上限**:本项目 Gitea 前置 IIS 的默认
|
||
`maxAllowedContentLength = 30,000,000` 字节(≈28.6 MiB),实测 16.4 MB 通过、
|
||
30.5 MB / 37 MB / 40 MB 上传返回 **HTTP 413**。`v0.1.0` 因此被拆成
|
||
`zip.part0/1/2`(无法直接解压)。现改为**只上传单个完整包**,超限时打包脚本
|
||
直接告警并要求运维放宽上限,不再回退分卷;做法见
|
||
`opendance-web/deploy/proxy/README.md` 第九节。
|
||
6. **v0.2.0 / v0.2.1 / v0.2.2 的 zip 用的是 LZMA 压缩**(`ZIP_METHOD=lzma`
|
||
打出来的),因为这几次发布时 IIS 上限都尚未放开:整包从约 38 MB 降到约 26 MB
|
||
才能传上去。**代价:Windows 10 的资源管理器不支持 LZMA(method 14) 的 zip,
|
||
必须用 7-Zip / WinRAR / PeaZip 解压**(Win11 24H2+ 起资源管理器原生支持 7z
|
||
等格式,但本项目未在 Windows 上实测)。
|
||
若 IIS 上限已放开、或你能把包放到别处分发(例如 Web 服务端的 `/static`,
|
||
不受 IIS 那个限制约束),请改用资源管理器可直接双击解压的 deflate 包:
|
||
`ZIP_METHOD=deflate bash build/package-release.sh 0.2.5`(约 38 MB)。
|
||
7. **发布包 exe 的版本元数据**(T-WIN-017):v0.2.1 之前在 Windows
|
||
「属性 → 详细信息」里显示的是 `0.1.0`,因为 `export_presets.cfg` 的
|
||
`application/file_version` / `application/product_version` 被硬编码且无人校验。
|
||
v0.2.2 起由 `build/sync-export-version.sh` 在导出前同步、由
|
||
`build/check-pe-version.sh` 直接读 exe 的 PE 版本资源复核
|
||
(`VS_FIXEDFILEINFO` + `StringFileInfo` 两处都查),
|
||
`check-version.sh` / `package-release.sh` / `verify.sh` / `test_about.gd` 均已接入。
|
||
|
||
## 第三方开源声明
|
||
|
||
见 [`THIRD_PARTY.md`](THIRD_PARTY.md),或客户端「关于」页面。
|
||
未使用 SMPL 模型及其衍生拓扑,统一采用 MediaPipe 33 点拓扑。
|