diff --git a/DSH.md b/DSH.md index 555227c..b4dee0f 100644 --- a/DSH.md +++ b/DSH.md @@ -1,4 +1,4 @@ -# DSH.md -- opendance-windows(OpenDance Studio 项目二:Windows 客户端) +# DSH.md -- opendance-windows(OpenDance Studio 项目二:Windows 客户端) ## 0. 本文件的作用 @@ -11,7 +11,7 @@ Windows 客户端是核心体验载体:接收 Android 辅助 App 推送的 33 驱动 VRM 角色,并与 Web 服务端下发的参考动作做实时 DTW 评分。 ``` -Android 辅助 App ──239 字节关键点帧──▶ 本端(WebSocket 服务端:8765) +Android 辅助 App ──239 字节关键点帧──▶ 本端(WebSocket 服务端:8765) │ Web 服务端 ──GET /api/catalog/export──────┤ ──GET /api/motions/:id/keypoints┘ @@ -25,7 +25,7 @@ Web 服务端 ──GET /api/catalog/export──────┤ | 组件 | 选型 | 说明 | |--|--|--| -| 引擎 | Godot 4.7.2(MIT) | 可在 Linux 上导出 Windows exe | +| 引擎 | Godot 4.7.2(MIT) | 可在 Linux 上导出 Windows exe | | VRM 加载 | **Godot 内置 GLTFDocument** | 见 6 的选型变更说明 | | 多格式导入 | Godot 内置 glTF 导入器 | FBX 由管理端转 BVH 后下发 | | 通信 | WebSocketPeer + HTTPRequest | 无第三方依赖 | @@ -66,7 +66,7 @@ opendance-windows/ ├── export_presets.cfg # ★ 必须位于项目根目录(Godot 硬性要求) ├── project.yaml / DSH.md ├── tasks/ # 001~011 单任务说明书 -├── contracts/ # 只读契约快照(来源 opendance-web v0.1.0) +├── contracts/ # 只读契约快照(来源 opendance-web v0.1.0) ├── scenes/main.tscn # 仅一个挂载 main.gd 的根节点 ├── src/ │ ├── autoload/ # AppConfig / CatalogStore / PoseReceiver / ScoreService @@ -76,7 +76,7 @@ opendance-windows/ │ ├── score/ # 特征提取、DTW、评分规则 │ └── ui/ # 主界面 / 游戏界面 / 结算 / 关于 / 来源展示 ├── tests/ # 无头测试框架 + 用例 -└── build/ # 导出脚本(sh / ps1) +└── build/ # 导出脚本(sh / ps1) ``` ## 6. 界面构建方式与选型变更(重要) @@ -121,7 +121,7 @@ $GODOT --headless --path. --script res://tests/run_tests.gd # 3. 应用可启动(autoload + UI 构建无报错) $GODOT --headless --path. --quit-after 180 -# 4. Windows 导出(需先安装 export templates) +# 4. Windows 导出(需先安装 export templates) bash build/export-windows.sh release ``` diff --git a/README.md b/README.md index 164454e..e393cde 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # opendance-windows -OpenDance Studio **阶段二**:Windows 客户端(Godot 4)。 +OpenDance Studio **阶段二**: Windows 客户端(Godot 4)。 接收 Android 辅助 App 通过 WebSocket 推送的 33 点骨骼关键点,驱动 VRM 角色, 并与 Web 管理服务端下发的参考动作做实时 DTW 评分。 @@ -9,14 +9,14 @@ OpenDance Studio **阶段二**:Windows 客户端(Godot 4)。 ## 状态 -**阶段二已完成**:T-WIN-001 -> T-WIN-011 全部落地。 +**阶段二已完成**: T-WIN-001 -> T-WIN-011 全部落地。 | 验收项 | 结果 | |--|--| -| 脚本编译 | OK 全部通过(无 SCRIPT ERROR) | -| 无头测试 | OK **580 项全部通过**(8 个测试套件) | +| 脚本编译 | OK 全部通过(无 SCRIPT ERROR) | +| 无头测试 | OK **580 项全部通过**(8 个测试套件) | | 应用启动 | OK 180 帧无运行时错误 | -| Windows 导出 | OK 生成 `OpenDance.exe`(104 MB,PE 头校验通过)+ `OpenDance.pck` | +| Windows 导出 | OK 生成 `OpenDance.exe`(104 MB, PE 头校验通过)+ `OpenDance.pck` | | 真实 VRM 验证 | OK 154 骨骼 VRoid 模型,humanoid 覆盖率 **100%** | | 骨骼重定向 | OK 关键点方向与骨骼实际指向点积 **1.0000** | | 跨实现交叉校验 | OK 关节角与 Web 服务端 TypeScript 实现**逐帧一致** | @@ -45,10 +45,10 @@ bash build/package-release.sh 0.1.0 | 项 | 要求 | |--|--| -| 操作系统 | 运行客户端:Windows 10/11(x86_64)。开发与导出:Windows 或 Linux 均可 | -| Godot | **4.7.x**(`project.godot` 声明特性 `4.7`,本仓库按 4.7.2 验证)。使用**标准版**(GDScript),**不需要 .NET / C# 版** | +| 操作系统 | 运行客户端: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`) | +| 导出模板 | **仅打包导出时需要**:必须安装与编辑器**完全同版本**的 export templates(`.tpz`) | | 磁盘与网络 | Godot 编辑器 + 导出模板约数百 MB;导出产物 `OpenDance.exe` 约 104 MB、`OpenDance.pck` 约 107 KB。本端需能访问 Web 服务端 API(默认 TCP 3000),并与 Android 设备处于同一局域网、放行本端 **TCP 8765 入站** | ### 2. 获取代码 @@ -75,7 +75,7 @@ cd opendance-windows - 位置:主界面顶部「**服务端地址**」输入框,填好后点「**保存并测试**」。 - 配置字段名(`opendance.cfg`):`api_base_url`。 -- **端口区分**:客户端只访问 Web 服务端的 **API 端口(默认 3000)**;管理端页面在**管理端口(默认 3001)**,客户端不使用该端口。 +- **端口区分**:客户端只访问 Web 服务端的 **API 端口(默认 3000)**;管理端页面在**管理端口(默认 3001)**,客户端不使用该端口。 - 示例占位符: ``` @@ -89,8 +89,8 @@ http://192.0.2.10:3000 | 对端 | 方向 | 本端配置 | 说明 | |--|--|--|--| -| 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`) | 需先在服务端**发布**曲目 / 动作 / 模型,动作还须**完成关键点预处理**,否则目录为空或动作不可选 | +| 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`) | 需先在服务端**发布**曲目 / 动作 / 模型,动作还须**完成关键点预处理**,否则目录为空或动作不可选 | - **Android 侧无本端可配置项**:接收地址由 Android App 自己填写;本端只监听固定端口 8765(可在 `opendance.cfg` 改 `listen_port` 后点「重新连接接收服务」生效)。 - 已知限制:Godot 的 `WebSocketPeer` 不暴露握手请求路径,本端**接受任意 path**,只校验子协议 `opendance.pose.v1`(见「已知限制」1)。 @@ -111,8 +111,8 @@ bash build/package-release.sh 0.1.0 | 产物 | 位置 | 说明 | |--|--|--| -| `OpenDance.exe` | `build/windows/` | 主程序,约 104 MB;脚本校验前 2 字节为 `MZ`(合法 PE) | -| `OpenDance.pck` | `build/windows/` | 资源包,约 107 KB;**必须与 exe 同目录** | +| `OpenDance.exe` | `build/windows/` | 主程序,约 104 MB;脚本校验前 2 字节为 `MZ`(合法 PE) | +| `OpenDance.pck` | `build/windows/` | 资源包,约 107 KB; **必须与 exe 同目录** | | `OpenDance.console.exe` | `build/windows/` | 控制台包装器,可能生成,非必需 | | `opendance-windows-v0.1.0.zip` | `dist-release/` | 发布包:exe + pck + `THIRD_PARTY.md` + `README.txt` + `contracts/` | @@ -133,9 +133,9 @@ bash build/package-release.sh 0.1.0 | 现象 | 原因 | 处理 | |--|--|--| -| 游戏界面提示「模型加载失败:缺少骨骼」或「骨骼绑定失败,模型可能不受支持」 | 模型没有 `Skeleton3D`,或骨骼命名不在 humanoid 映射表内;驱动至少需要 hips / spine / neck / head + 四肢共 16 根核心骨骼 | 换用标准 VRM(VRoid Studio 导出);看「人形覆盖率」,低于 60% 会提示「骨骼覆盖偏低,动作可能不完整」;必要时在 `src/model/humanoid_map.gd` 增加命名映射 | +| 游戏界面提示「模型加载失败:缺少骨骼」或「骨骼绑定失败,模型可能不受支持」 | 模型没有 `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\` | +| 导出失败:「未找到导出模板」 | 未安装 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` | @@ -143,10 +143,10 @@ bash build/package-release.sh 0.1.0 | 组件 | 选型 | |--|--| -| 引擎 | Godot 4.7.x(MIT) | +| 引擎 | Godot 4.7.x(MIT) | | VRM 加载 | Godot 内置 GLTFDocument | | 通信 | WebSocketPeer(服务端)+ HTTPRequest | -| 评分 | DTW + 关节角度差异(纯 GDScript) | +| 评分 | DTW + 关节角度差异(纯 GDScript) | ## 架构 @@ -173,7 +173,7 @@ src/ ## 依赖的契约(opendance-web v0.1.0 冻结) -- [`contracts/websocket-keypoints.md`](contracts/websocket-keypoints.md) - 239 字节关键点帧与文本控制消息(本端为**接收方**) +- [`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 优先规则 @@ -185,7 +185,7 @@ src/ ## 已知限制 -1. Godot 的 `WebSocketPeer` 不暴露握手请求路径,因此**无法校验 path 是否为 `/pose`**, +1. Godot 的 `WebSocketPeer` 不暴露握手请求路径,因此**无法校验 path 是否为 `/pose`**, 实现接受任意 path,仅校验子协议 `opendance.pose.v1`。 2. 文档原选型 `godot-vrm` GDExtension 不发布预编译产物,改用 Godot 内置 glTF 导入器 + 自实现骨骼名映射;理由见 `DSH.md` 6。 diff --git a/README.txt b/README.txt index 3ccb189..bde6ab8 100644 --- a/README.txt +++ b/README.txt @@ -43,7 +43,7 @@ OpenDance.exe 的用户看第一节即可。 1. 前置条件 - - 操作系统:Windows 10/11(x86_64)。开发与导出可在 Windows 或 Linux。 + - 操作系统:Windows 10/11(x86_64)。开发与导出可在 Windows 或 Linux。 - Godot 4.7.x 标准版(GDScript)。不需要 .NET / C# 版。 - 渲染后端为 GL Compatibility,运行需 OpenGL 3.3 级别的显卡与驱动。 - 导出模板:仅打包导出时需要,必须与编辑器完全同版本。 @@ -68,7 +68,7 @@ OpenDance.exe 的用户看第一节即可。 4. 配置服务端地址 - 主界面顶部「服务端地址」填好后点「保存并测试」,配置字段名为 api_base_url: + 主界面顶部「服务端地址」填好后点「保存并测试」,配置字段名为 api_base_url: http://<服务器地址>:3000 http://192.0.2.10:3000 @@ -81,7 +81,7 @@ OpenDance.exe 的用户看第一节即可。 - Android 辅助 App -> 本端 WebSocket:本端是服务端,默认监听 0.0.0.0:8765,路径 /pose,子协议 opendance.pose.v1。Android 端填写 本端主界面显示的「接收地址」,形如 ws://<本机局域网地址>:8765/pose。 - 本端可在 opendance.cfg 改 listen_host / listen_port / ws_subprotocol, + 本端可在 opendance.cfg 改 listen_host / listen_port / ws_subprotocol, 改后点「重新连接接收服务」生效。 - Web 服务端:需先在服务端发布曲目 / 动作 / 模型,动作还须完成关键点 预处理,客户端才有可选内容。 @@ -94,7 +94,7 @@ OpenDance.exe 的用户看第一节即可。 # Windows(兜底) powershell -ExecutionPolicy Bypass -File build\export-windows.ps1 - 产物在 build/windows/:OpenDance.exe(约 104 MB,PE 头 MZ 校验通过) + 产物在 build/windows/: OpenDance.exe(约 104 MB, PE 头 MZ 校验通过) + OpenDance.pck(约 107 KB,必须与 exe 同目录);可能附带 OpenDance.console.exe。 @@ -121,7 +121,7 @@ OpenDance.exe 的用户看第一节即可。 8. 部署相关常见问题 Q: 模型加载失败:缺少骨骼 - A: 模型无 Skeleton3D,或骨骼命名不在映射表内;换用标准 VRM(VRoid + A: 模型无 Skeleton3D,或骨骼命名不在映射表内;换用标准 VRM(VRoid Studio 导出)。人形覆盖率低于 60% 会提示「骨骼覆盖偏低」。 Q: 连不上服务端 / 显示「服务端不可达」 @@ -130,7 +130,7 @@ OpenDance.exe 的用户看第一节即可。 Q: 导出失败:未找到导出模板 A: 安装与编辑器同版本的 export templates。Linux 放 - ~/.local/share/godot/export_templates/<版本>.stable/; + ~/.local/share/godot/export_templates/<版本>.stable/; Windows 放 %APPDATA%\Godot\export_templates\<版本>.stable\。 Q: 接收服务启动失败(端口 8765 可能被占用) @@ -169,12 +169,12 @@ OpenDance.exe 的用户看第一节即可。 可配置项: - api_base_url Web 服务端地址(默认 http://127.0.0.1:3000) - listen_host WebSocket 监听地址(默认 0.0.0.0) - listen_port WebSocket 监听端口(默认 8765) - ws_subprotocol 子协议(默认 opendance.pose.v1) + api_base_url Web 服务端地址(默认 http://127.0.0.1:3000) + listen_host WebSocket 监听地址(默认 0.0.0.0) + listen_port WebSocket 监听端口(默认 8765) + ws_subprotocol 子协议(默认 opendance.pose.v1) dtw_band DTW 带宽,单位帧(默认 15 ~ 0.5 秒抖动容忍) - score_tolerance 评分容差:平均每步代价达到该值即 0 分(默认 0.25) + score_tolerance 评分容差:平均每步代价达到该值即 0 分(默认 0.25) mirror_preview 镜像预览 -- @@ -196,9 +196,9 @@ OpenDance.exe 的用户看第一节即可。 - 每帧特征 = 33 点 x (x,y,z) + 9 个关节角余弦 = 108 维 (与 Web 服务端预处理产物、Android 端推流结构完全一致) - 置信度低于 0.5 的关键点不参与代价计算,避免遮挡导致误判 -- 采用带约束 DTW(|i-j| <= band)对齐,能吸收一定的节奏快慢差异 +- 采用带约束 DTW(|i-j| <= band)对齐,能吸收一定的节奏快慢差异 - 得分 = clamp(1 - 平均代价 / 容差, 0, 1) x 100,并做指数平滑 -- 等级:S >= 95,A >= 85,B >= 70,C >= 55,D 其余 +- 等级:S >= 95, A >= 85, B >= 70, C >= 55, D 其余 -- 七、从源码构建 @@ -212,7 +212,7 @@ OpenDance.exe 的用户看第一节即可。 # Windows 上兜底导出 powershell -ExecutionPolicy Bypass -File build\export-windows.ps1 -产物位于 build/windows/:OpenDance.exe + OpenDance.pck +产物位于 build/windows/: OpenDance.exe + OpenDance.pck (脚本会校验产物存在且为合法 PE 文件) 运行测试: @@ -230,8 +230,8 @@ OpenDance.exe 的用户看第一节即可。 export_presets.cfg 导出预设(必须在项目根目录) project.yaml 任务清单与状态 DSH.md 本项目 DSH 执行约束 - tasks/ 单任务说明书(T-WIN-001 ~ 011) - contracts/ 只读契约快照(来源 opendance-web v0.1.0) + tasks/ 单任务说明书(T-WIN-001 ~ 011) + contracts/ 只读契约快照(来源 opendance-web v0.1.0) scenes/main.tscn 根节点 src/autoload/ AppConfig / CatalogStore / PoseReceiver / ScoreService src/net/ 239 字节编解码、WebSocket 服务、REST 客户端 @@ -255,7 +255,7 @@ A: 动作必须先在管理端完成「预处理」(BVH -> 关键点序列) Q: 模型加载失败 / 动作不完整 A: 游戏界面会显示「人形覆盖率」。低于 60% 说明模型的骨骼命名不被识别, - 请换用标准 VRM(VRoid Studio 导出)模型。 + 请换用标准 VRM(VRoid Studio 导出)模型。 Q: 连上了手机但分数一直是 0 A: 确认手机端已开始推流;主界面的「查看接收状态」会显示累计帧数与丢帧数。 diff --git a/THIRD_PARTY.md b/THIRD_PARTY.md index 58fbe61..82debc9 100644 --- a/THIRD_PARTY.md +++ b/THIRD_PARTY.md @@ -11,7 +11,7 @@ - 版权:Copyright 2023 Google LLC - 许可证:Apache License 2.0 - 源链接:https://github.com/google-ai-edge/mediapipe -- 用途:33 点骨骼拓扑定义(契约来源,**本端不包含其代码**) +- 用途:33 点骨骼拓扑定义(契约来源,**本端不包含其代码**) ## 2. Three.js @@ -109,7 +109,7 @@ ## 许可合规说明 1. 本客户端自身代码以 MIT 许可发布。 -2. 运行时依赖仅为 **Godot Engine**(MIT);其余条目为同项目其他组件或服务端依赖, +2. 运行时依赖仅为 **Godot Engine**(MIT);其余条目为同项目其他组件或服务端依赖, 按契约要求一并声明。 3. 本客户端**未使用 SMPL 模型及其衍生拓扑**,统一采用 MediaPipe 33 点拓扑, 以规避 SMPL 的非商用许可限制。 diff --git a/contracts/data-models.md b/contracts/data-models.md index f377aea..be11338 100644 --- a/contracts/data-models.md +++ b/contracts/data-models.md @@ -5,7 +5,7 @@ Web 服务端是唯一权威定义,本文件是另两端消费的**稳定视 ## 1. 通用约定 -- 所有资源返回 `id: number`(BIGINT,超出 2^53 前不会发生)。 +- 所有资源返回 `id: number`(BIGINT,超出 2^53 前不会发生)。 - 所有时间为 ISO 8601 UTC 字符串,例:`2026-09-14T10:00:00.000Z`。 - 列表接口统一分页响应: @@ -117,7 +117,7 @@ rejected 已驳回 ``` 0. 环境变量 DEFAULT_MODEL_ID 指定,且该模型存在且已发布 1. 源码内写死的默认模型种子(web-server/src/config/default-model.ts 的 - DEFAULT_MODEL_SEED):按 **sourceSha256(上传压缩包的 sha256)** 匹配已发布模型, + DEFAULT_MODEL_SEED):按 **sourceSha256(上传压缩包的 sha256)** 匹配已发布模型, 换库/重建库后仍然有效;没有压缩包时退回按产物 sha256,最后按名称兜底。 ⚠ 不能用产物 sha256 当主键:实测 Assimp 的 GLB 导出不是字节级确定的, 同一份 zip 每次转换得到的文件 sha256 都不同(BIN chunk 有十几字节浮动) @@ -145,7 +145,7 @@ rejected 已驳回 | `comment` | string\|null | 审核意见 | | `createdAt` | string | | -## 6. `CatalogBundle`(客户端一次性拉取,`GET /api/catalog/export`) +## 6. `CatalogBundle`(客户端一次性拉取,`GET /api/catalog/export`) ```json { diff --git a/contracts/database.md b/contracts/database.md index 161f404..53762cd 100644 --- a/contracts/database.md +++ b/contracts/database.md @@ -3,11 +3,11 @@ ## 环境 - 类型:MySQL 兼容实例(外部提供,含 MariaDB 10.5+),TypeORM - 使用 `mysql` driver(`mysql2`)连接,SQL 语法限制在 MySQL 5.7 / MariaDB 10.5+ + 使用 `mysql` driver(`mysql2`)连接,SQL 语法限制在 MySQL 5.7 / MariaDB 10.5+ 共同子集内,不使用 MySQL 8 专有语法(窗口函数 CTE 递归除外)。 -- 数据库名:由部署方提供,通过 `DB_NAME` 注入(本文档示例统一用 `opendance`) +- 数据库名:由部署方提供,通过 `DB_NAME` 注入(本文档示例统一用 `opendance`) - 字符集:`utf8mb4` / `utf8mb4_general_ci`(沿用现有实例默认排序规则) -- 迁移工具:TypeORM migration,**不使用 `DB_SYNCHRONIZE`** +- 迁移工具:TypeORM migration, **不使用 `DB_SYNCHRONIZE`** ## 连接信息(由部署方提供) @@ -25,10 +25,10 @@ DB_LOGGING=false ## 约定 -- 所有表名使用 `snake_case`,复数形式(`songs` / `motions` / `models`) +- 所有表名使用 `snake_case`,复数形式(`songs` / `motions` / `models`) - 主键统一为 `BIGINT AUTO_INCREMENT`,字段名 `id` -- 时间字段统一 `DATETIME`,`created_at` / `updated_at`,`updated_at` 由应用层维护 -- 软删除统一使用 `deleted_at DATETIME NULL`(`@DeleteDateColumn`) +- 时间字段统一 `DATETIME`, `created_at` / `updated_at`, `updated_at` 由应用层维护 +- 软删除统一使用 `deleted_at DATETIME NULL`(`@DeleteDateColumn`) - 布尔字段使用 `TINYINT(1)` - 来源(出处)字段为 5 元组,任何资产入库必须齐备: `source_type` / `source_name` / `source_url` / `license` / `author` @@ -36,24 +36,24 @@ DB_LOGGING=false - 每个 migration 必须可逆(提供 `down` 方法) - 迁移文件名格式:`-.ts` -## 增量迁移(v0.2.0 / v0.3.0) +## 增量迁移(v0.2.0 / v0.3.0) | 迁移 | 变更 | |--|--| -| `AddModelConversionFields` | `models` 增加 `source_asset_id BIGINT NULL`(上传的压缩包,用于溯源)与 `conversion_note VARCHAR(500) NULL`(转换说明,见 model-formats.md 1.1) | +| `AddModelConversionFields` | `models` 增加 `source_asset_id BIGINT NULL`(上传的压缩包,用于溯源)与 `conversion_note VARCHAR(500) NULL`(转换说明,见 model-formats.md 1.1) | | `AddModelStats` | `models` 增加 `stats TEXT NULL`(模型分析结果:面数、骨骼树、humanoid 覆盖、可驱动性),供审核界面展示 | -## 表清单(v0.1.0) +## 表清单(v0.1.0) | 表 | 用途 | 任务 | |--|--|--| | `songs` | 曲目元数据 | T-WEB-003 | | `assets` | 上传文件元数据(去重、sha256、物理路径) | T-WEB-004 | -| `motions` | 动作数据(BVH/FBX...)+ 来源 + 预处理产物 | T-WEB-004 / 006 | +| `motions` | 动作数据(BVH/FBX...)+ 来源 + 预处理产物 | T-WEB-004 / 006 | | `models` | 角色模型(VRM 优先)+ 来源 + 压缩包转换溯源 | T-WEB-005 | | `review_records` | 上传审核流水 | T-WEB-007 | -## 状态机(`status` 列,v0.1.0) +## 状态机(`status` 列,v0.1.0) ``` draft ──submit──▶ pending ──approve──▶ published diff --git a/contracts/model-formats.md b/contracts/model-formats.md index 4155755..b6a50c2 100644 --- a/contracts/model-formats.md +++ b/contracts/model-formats.md @@ -1,21 +1,21 @@ # contracts/model-formats.md - 模型与动作格式契约 v0.4.0 -## 1. 角色模型(models) +## 1. 角色模型(models) **系统以 GLB / VRM 为主力格式**(两个客户端都原生加载),其余格式一律在服务端转换。 | 格式 | 扩展名 | 支持级别 | 服务端行为 | |--|--|--|--| -| glTF Binary | `.glb` | **主力(P0)** | **直接导入**,不重编码 | -| VRM | `.vrm` | **主力(P0)** | **直接导入**;= GLB + VRM 扩展(humanoid / 表情 / springBone / meta) | -| glTF | `.gltf` (+ `.bin`, 贴图) | P1 | 直接导入(分离文件在 zip 内可用;单文件上传会缺.bin) | +| glTF Binary | `.glb` | **主力(P0)** | **直接导入**,不重编码 | +| VRM | `.vrm` | **主力(P0)** | **直接导入**; = GLB + VRM 扩展(humanoid / 表情 / springBone / meta) | +| glTF | `.gltf` (+ `.bin`, 贴图) | P1 | 直接导入(分离文件在 zip 内可用;单文件上传会缺.bin) | | MMD 压缩包 | `.zip` | P1 | **自动搜索包内可用模型**:优先直接导入 `.vrm`/`.glb`/`.gltf`,否则转换 `.pmx`/`.fbx`,见 1.1 | | MMD 模型 | `.pmx` | P2(输入) | 服务端转换(优先 GLB、其次 VRM);单文件上传无贴图 | -| FBX | `.fbx` | P2(输入) | 服务端转换(优先 GLB、其次 VRM) | +| FBX | `.fbx` | P2(输入) | 服务端转换(优先 GLB、其次 VRM) | ### 1.1 上传与转换策略 -**`.glb` / `.vrm` / `.gltf` 直接导入**(不重编码,零损失);**`.pmx` / `.fbx` 由服务端转换**: +**`.glb` / `.vrm` / `.gltf` 直接导入**(不重编码,零损失);**`.pmx` / `.fbx` 由服务端转换**: **优先 GLB,其次 VRM**。 #### 单文件上传 @@ -28,7 +28,7 @@ #### 压缩包上传(自动搜索) -不再"只找 `.pmx`",而是**按格式优先级自动搜索包内可用模型**: +不再"只找 `.pmx`",而是**按格式优先级自动搜索包内可用模型**: ``` .vrm ->.glb ->.gltf (直接导入,优先) @@ -38,7 +38,7 @@ 同一优先级内:`MODEL_CONVERT_PMX` 指定的文件名优先,其次目录最浅,最后文件最大。 压缩包原件以 `model-archive` 资产留存并写入 `models.source_asset_id`。 -#### 转换与校验(PMX / FBX) +#### 转换与校验(PMX / FBX) | 步骤 | 规则 | |--|--| @@ -46,7 +46,7 @@ | 2. glTF 打包 | 上一步失败时 `assimp export ->.gltf`,由 `gltf-packer.ts` 把 `.gltf` + `.bin` + 贴图自行合成 GLB | | 3. VRM 封装 | `MODEL_CONVERT_FORMAT=vrm` 时,在 GLB 之上补 `VRMC_vrm` 扩展(humanoid 骨骼来自骨骼名映射,见 `model-inspector.ts` 的候选表,与 Godot 客户端一致);**humanoid 不全则回退交付 GLB**,缺失骨骼写进 `conversion_note` | | 回读校验 | 每一步产物都要 `assimp info` 回读。Assimp 会"写出损坏文件"(实测 FBX 导出退出码 11、产物回读报 `FBX-DOM no Objects dictionary found`),只看退出码会把坏文件入库 | -| 说明 | 每次尝试都写进 `models.conversion_note`(例如 `Hatsune Miku.pmx -> GLB,已内嵌 2 张贴图`) | +| 说明 | 每次尝试都写进 `models.conversion_note`(例如 `Hatsune Miku.pmx -> GLB,已内嵌 2 张贴图`) | | 失败 | 包内没有可用模型、Assimp 不可用、两条路径都失败 -> **HTTP 422**,不产生模型记录 | #### 入库后(自动) @@ -63,14 +63,14 @@ | 项 | 规则 | |--|--| | 确定性 | **转换产物不是字节级确定的**:同一份 zip 连续转换 3 次得到 3 个不同 sha256(大小相同,BIN chunk 有十几字节浮动)。因此默认模型种子以**上传压缩包的 sha256** 为匹配键,产物 sha256 只作记录 | -| 安全 | 解压前逐条校验路径,拒绝绝对路径与 `..`(zip-slip);拒绝加密条目;只支持 STORE / DEFLATE | +| 安全 | 解压前逐条校验路径,拒绝绝对路径与 `..`(zip-slip);拒绝加密条目;只支持 STORE / DEFLATE | **VRM 优先**的选取顺序见 `data-models.md` 4.1。 -VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)或 -`extensions.VRMC_vrm`(1.0),写入 `models.vrm_version`。 +VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)或 +`extensions.VRMC_vrm`(1.0),写入 `models.vrm_version`。 -## 2. 动作数据(motions) +## 2. 动作数据(motions) | 格式 | 扩展名 | 预处理支持 | 说明 | |--|--|--|--| @@ -82,7 +82,7 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x) ### 2.1 预处理产物(冻结) -- 格式:NDJSON(`application/x-ndjson`),每行一帧,UTF-8 无 BOM +- 格式:NDJSON(`application/x-ndjson`),每行一帧,UTF-8 无 BOM - 一行结构: ```json @@ -93,9 +93,9 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x) - `angles` 为 `skeleton-topology.md` 5 定义的 9 个关节角余弦,顺序固定: `[shoulder_L, shoulder_R, elbow_L, elbow_R, hip_L, hip_R, knee_L, knee_R, spine]` - 帧率:保留源帧率;`fps` 写入 `motions.fps`,缺省 30 -- 坐标系换算:所有 3D 动作数据统一转到 **Y 轴向上、右手系、骨盆为原点**, +- 坐标系换算:所有 3D 动作数据统一转到 **Y 轴向上、右手系、骨盆为原点**, 再投影到 MediaPipe 归一化图像空间: - `x_img = 0.5 + x_world * scale`,`y_img = 0.5 - y_world * scale`,`z_img = -z_world * scale`, + `x_img = 0.5 + x_world * scale`, `y_img = 0.5 - y_world * scale`, `z_img = -z_world * scale`, `scale = 0.35 / max(|x|,|y|,|z|)`(按整段动作的最大范围自适应,保证不出画) - 缺失关节(BVH 中不存在对应骨骼):`confidence = 0.0`,坐标沿用最近可用父节点 @@ -122,18 +122,18 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x) 未映射到的点按 `skeleton-topology.md` 5 的层级由父节点推算, 并置 `confidence` 降低(`0.5`),表示估算而非实测。 -### 2.3 VMD(MikuMikuDance)解析约定 +### 2.3 VMD(MikuMikuDance)解析约定 -VMD 是二进制格式:文件头 `Vocaloid Motion Data 0002`(30 字节)+ 模型名(20 字节, -Shift-JIS)+ 骨骼关键帧数组(每条 **111 字节**:15 字节骨骼名、uint32 帧号、 +VMD 是二进制格式:文件头 `Vocaloid Motion Data 0002`(30 字节)+ 模型名(20 字节, +Shift-JIS)+ 骨骼关键帧数组(每条 **111 字节**: 15 字节骨骼名、uint32 帧号、 float32x3 位移、float32x4 旋转四元数、64 字节贝塞尔插值参数)+ morph / 相机 / 灯光 / 自阴影 / IK / 可见性段落。**VMD 不含骨骼长度**,只有旋转(少数骨骼带位移), 因此服务端必须自带一套标准 MMD 骨架才能做 FK。 处理链路:`vmd-parser.ts`(二进制 + Shift-JIS + 贝塞尔插值)-> `mmd-skeleton.ts` -(标准骨架)-> `mmd-pose.ts`(FK + 腿部 IK)-> `retarget.ts` 的 +(标准骨架)-> `mmd-pose.ts`(FK + 腿部 IK)-> `retarget.ts` 的 `worldFramesToKeypointFrames()`(与 BVH 完全共用骨盆归零 / 自适应缩放 / 图像空间投影 / -9 个关节角)。固定 **30 fps**(MMD 帧率恒定,上传时填写的 `fps` 被忽略)。 +9 个关节角)。固定 **30 fps**(MMD 帧率恒定,上传时填写的 `fps` 被忽略)。 | MediaPipe 点 | MMD 标准骨骼 | 说明 | |--|--|--| @@ -149,11 +149,11 @@ float32x3 位移、float32x4 旋转四元数、64 字节贝塞尔插值参数) 骨骼名按 `mmd-skeleton.ts` 的别名表归一(含全角 `IK` / 半角 `IK` 两种写法); 未纳入标准骨架的骨骼(裙摆、物理、表情等)被忽略但会计数并写入预处理日志。 -**腿部驱动判定**:`足IK` 轨道存在非零位移 -> 走 IK(用两骨骼解析 IK 把踝拉到 IK +**腿部驱动判定**: `足IK` 轨道存在非零位移 -> 走 IK(用两骨骼解析 IK 把踝拉到 IK 目标点,膝盖朝角色正面弯,不可达时收敛为伸直);否则走 FK(部分动作数据把腿烘焙成 FK 旋转)。两份示例文件分别命中两种路径。 -**坐标系**:MMD 原生是左手系(正面为 -Z),本项目通路是右手系(正面为 +Z、+X 为角色 +**坐标系**: MMD 原生是左手系(正面为 -Z),本项目通路是右手系(正面为 +Z、+X 为角色 左侧)。VMD 的四元数与位移统一做 Z 镜像(`(x,y,z,w) -> (-x,-y,z,w)`、位移 z 取反)后 送入标准骨架。该选择由真实数据对照实验确定:腿部烘焙为 FK 的样本中,`zMirrored` 下 膝盖朝前占比 94.1%,不转换时仅 5.9%(见 `test/mmd-vmd.spec.ts`)。 @@ -167,15 +167,15 @@ FK 旋转)。两份示例文件分别命中两种路径。 | 项 | 值 | |--|--| -| 单文件上限 | 512 MiB(`UPLOAD_MAX_BYTES` 可覆盖) | +| 单文件上限 | 512 MiB(`UPLOAD_MAX_BYTES` 可覆盖) | | 允许扩展名 | 上表全部;其他一律 422 | | 存储路径 | `${STORAGE_DIR}/{kind}/{yyyy}/{mm}/{sha256前2位}/{sha256}.{ext}` | | 去重 | 相同 sha256 复用同一 `assets` 记录,不重复落盘 | | 文件名 | 服务端不使用原始文件名做路径,只存 `assets.original_name` | -## 4. 开源依赖与许可证(引用 `open-source-credits.md`) +## 4. 开源依赖与许可证(引用 `open-source-credits.md`) -- `bvh`(MIT)- BVH 解析参考实现 -- `Assimp`(BSD-3-Clause)- FBX/glTF 转换,**可选外部依赖** -- `@pixiv/three-vrm`(MIT)- 管理端 VRM 预览 -- `three`(MIT)- 管理端 3D 预览 +- `bvh`(MIT)- BVH 解析参考实现 +- `Assimp`(BSD-3-Clause)- FBX/glTF 转换,**可选外部依赖** +- `@pixiv/three-vrm`(MIT)- 管理端 VRM 预览 +- `three`(MIT)- 管理端 3D 预览 diff --git a/contracts/skeleton-topology.md b/contracts/skeleton-topology.md index 2fd6624..0130e2d 100644 --- a/contracts/skeleton-topology.md +++ b/contracts/skeleton-topology.md @@ -47,7 +47,7 @@ | 33 | neck | `midpoint(left_shoulder(11), right_shoulder(12))` | | 34 | pelvis | `midpoint(left_hip(23), right_hip(24))` | -## 3. 父节点表(`parent[i]`,-1 表示世界根) +## 3. 父节点表(`parent[i]`, -1 表示世界根) 对称树,便于骨骼重定向与关节角计算: @@ -91,16 +91,16 @@ ## 4. 坐标系 -- **归一化图像坐标**:`x, y ∈ [0, 1]`,原点左上角,`y` 向下;`x` 相对图像宽,`y` 相对图像高。 -- **深度**:`z` 与 `x` 同尺度,原点位于髋中心,**负值朝向相机**。 -- **镜像**:Android 前置摄像头默认输出镜像画面;是否镜像由帧头 `flags.bit0` 声明, - 接收端据此决定是否翻转 `x`(`x' = 1 - x`)。 -- **置信度**:`visibility ∈ [0, 1]`,落盘/传输为 `uint8`(`round(v * 255)`)。 +- **归一化图像坐标**: `x, y ∈ [0, 1]`,原点左上角,`y` 向下;`x` 相对图像宽,`y` 相对图像高。 +- **深度**: `z` 与 `x` 同尺度,原点位于髋中心,**负值朝向相机**。 +- **镜像**: Android 前置摄像头默认输出镜像画面;是否镜像由帧头 `flags.bit0` 声明, + 接收端据此决定是否翻转 `x`(`x' = 1 - x`)。 +- **置信度**: `visibility ∈ [0, 1]`,落盘/传输为 `uint8`(`round(v * 255)`)。 归一化比例统一使用 `32767`(见 `websocket-keypoints.md`)。 ## 5. 派生特征(DTW 输入,Windows 端实现,服务端预处理产物同构) -服务端 T-WEB-006 预处理产出的关键点序列与 Android 端输出的帧**结构完全一致**, +服务端 T-WEB-006 预处理产出的关键点序列与 Android 端输出的帧**结构完全一致**, 因此 Windows 端 DTW 引擎只有一条输入通路。 关节角特征(弧度,余弦值便于插值): diff --git a/contracts/websocket-keypoints.md b/contracts/websocket-keypoints.md index ce1ec53..f6ed088 100644 --- a/contracts/websocket-keypoints.md +++ b/contracts/websocket-keypoints.md @@ -10,17 +10,17 @@ ws://:8765/pose ``` - Windows 客户端监听,Android App 主动连接。 -- 子协议:`opendance.pose.v1`(`Sec-WebSocket-Protocol`) +- 子协议:`opendance.pose.v1`(`Sec-WebSocket-Protocol`) - 二进制帧传输关键点,文本帧传输控制消息(JSON)。 - 默认 30 FPS;发送端允许丢帧,接收端必须按 `frame_id` 检测丢包并插值补齐。 ## 2. 二进制帧:239 字节(冻结) -**小端序(little-endian)**。 +**小端序(little-endian)**。 ``` 偏移 长度 字段 类型 说明 -0 2 magic u16 LE 固定 0x4F44(ASCII "OD") +0 2 magic u16 LE 固定 0x4F44(ASCII "OD") 2 1 version u8 固定 0x01 3 1 flags u8 bit0 = 镜像画面(mirrored) bit1 = 本帧含低置信度点(<0.5) @@ -32,7 +32,7 @@ ws://:8765/pose 合计 239 ``` -### 单个关键点(7 字节,`i` 为 0..32) +### 单个关键点(7 字节,`i` 为 0..32) ``` 偏移 长度 字段 类型 换算 @@ -43,7 +43,7 @@ ws://:8765/pose ``` - 点索引与坐标语义见 `skeleton-topology.md`。 -- 编码:`x = clamp(round(x_norm * 32767), -32768, 32767)`,`y`、`z` 同理。 +- 编码:`x = clamp(round(x_norm * 32767), -32768, 32767)`, `y`、`z` 同理。 - 低置信度点(`conf < 128`)坐标无意义,接收端必须插值,不得直接使用。 - `i16` 为有符号数:`0x8000` 表示 `-32768`。 @@ -68,7 +68,7 @@ func decode(buf: PackedByteArray) -> Dictionary: return {"flags": flags, "frame_id": frame_id, "points": pts} ``` -## 3. 文本控制帧(JSON,UTF-8) +## 3. 文本控制帧(JSON, UTF-8) ### 3.1 `hello` - Android -> Windows,连接后立即发送 diff --git a/tasks/001-scaffold.md b/tasks/001-scaffold.md index b325652..96f45bb 100644 --- a/tasks/001-scaffold.md +++ b/tasks/001-scaffold.md @@ -1,6 +1,6 @@ # T-WIN-001 Godot 项目骨架 -- **状态**:done +- **状态**: done - **依赖**:无 ## 目标 @@ -22,7 +22,7 @@ scenes/main.tscn | 文件 | 说明 | |--|--| | `project.godot` | 工程配置;autoload = AppConfig / CatalogStore / PoseReceiver / ScoreService | -| `scenes/main.tscn` | 只含一个挂载 `src/ui/main.gd` 的 Control 根节点(界面用代码构建,见 DSH.md 6) | +| `scenes/main.tscn` | 只含一个挂载 `src/ui/main.gd` 的 Control 根节点(界面用代码构建,见 DSH.md 6) | | `src/ui/main.gd` | 应用外壳:构建布局、切换页面、启动接收服务 | | `src/autoload/app_config.gd` | 客户端配置,持久化到 `user://opendance.cfg` | | `src/autoload/catalog_store.gd` | 目录缓存 + 模型下载缓存 + 关键点序列缓存 | diff --git a/tasks/002-rest-client.md b/tasks/002-rest-client.md index f2b0e33..3833fd9 100644 --- a/tasks/002-rest-client.md +++ b/tasks/002-rest-client.md @@ -1,7 +1,7 @@ # T-WIN-002 调用 Web 服务端 API -- **状态**:done -- **依赖**:T-WIN-001 +- **状态**: done +- **依赖**: T-WIN-001 ## 目标 diff --git a/tasks/003-websocket-receiver.md b/tasks/003-websocket-receiver.md index 766597d..f89368a 100644 --- a/tasks/003-websocket-receiver.md +++ b/tasks/003-websocket-receiver.md @@ -1,7 +1,7 @@ # T-WIN-003 接收 Android 关键点帧 -- **状态**:done -- **依赖**:T-WIN-001 +- **状态**: done +- **依赖**: T-WIN-001 ## 目标 @@ -18,7 +18,7 @@ src/autoload/pose_receiver.gd ## 交付物 -- `src/net/pose_server.gd`:TCPServer + `WebSocketPeer.accept_stream()` 实现服务端 +- `src/net/pose_server.gd`: TCPServer + `WebSocketPeer.accept_stream()` 实现服务端 - `src/autoload/pose_receiver.gd`:绑定 AppConfig,暴露 `latest_frame` 与统计 ## 能力 @@ -36,7 +36,7 @@ src/autoload/pose_receiver.gd ## 已知限制 -Godot 的 `WebSocketPeer` 不暴露握手请求路径,因此**无法校验 path 是否为 `/pose`**, +Godot 的 `WebSocketPeer` 不暴露握手请求路径,因此**无法校验 path 是否为 `/pose`**, 实现接受任意 path,仅校验子协议。已记录于 DSH.md 与任务文档。 ## 验收 diff --git a/tasks/004-keypoint-deserialize.md b/tasks/004-keypoint-deserialize.md index 5b6592c..9a5ad47 100644 --- a/tasks/004-keypoint-deserialize.md +++ b/tasks/004-keypoint-deserialize.md @@ -1,7 +1,7 @@ # T-WIN-004 239 字节帧反序列化 -- **状态**:done -- **依赖**:T-WIN-001 +- **状态**: done +- **依赖**: T-WIN-001 ## 目标 @@ -24,20 +24,20 @@ src/net/keypoint_frame.gd 2 1 version u8 = 1 3 1 flags u8 bit0 镜像 bit1 低置信度 bit2 上半身 4 4 frame_id u32 LE -8 231 keypoints 33 x 7B:s16 x, s16 y, s16 z, u8 conf +8 231 keypoints 33 x 7B:s16 x, s16 y, s16 z, u8 conf 合计 239 ``` ## 交付物 -`src/net/keypoint_frame.gd`:`PoseFrame` 内部类 + `decode` / `encode` / `validate` / `from_json_dict`。 +`src/net/keypoint_frame.gd`: `PoseFrame` 内部类 + `decode` / `encode` / `validate` / `from_json_dict`。 ## 关键实现 - 归一化系数统一 32767,与 opendance-web 的 TypeScript 参考实现逐位对齐 -- 编解码往返误差 < 1e-4(定点精度 1/32767 ~ 3.05e-5) +- 编解码往返误差 < 1e-4(定点精度 1/32767 ~ 3.05e-5) - `unmirrored_points()` 按 flags.bit0 翻转 x -- 低置信度阈值 0.5:`is_reliable()` 与 `compute_low_confidence()` +- 低置信度阈值 0.5: `is_reliable()` 与 `compute_low_confidence()` - 极值(±1.0)不溢出 int16 ## 验收 diff --git a/tasks/005-vrm-loader.md b/tasks/005-vrm-loader.md index 4ac14e9..f70ccb7 100644 --- a/tasks/005-vrm-loader.md +++ b/tasks/005-vrm-loader.md @@ -1,7 +1,7 @@ # T-WIN-005 VRM 模型加载 -- **状态**:done -- **依赖**:T-WIN-001 +- **状态**: done +- **依赖**: T-WIN-001 ## 目标 @@ -19,23 +19,23 @@ src/model/humanoid_map.gd ## 选型变更 -文档原选型 `godot-vrm`(V-Sekai)GDExtension **不发布任何预编译产物**, +文档原选型 `godot-vrm`(V-Sekai)GDExtension **不发布任何预编译产物**, 需用 godot-cpp 本地编译原生库,会破坏「Linux 导出 Windows exe」的验收路径。 -改用 **Godot 内置 GLTFDocument**:VRM 即 glTF 2.0 容器,实测可完整解析 +改用 **Godot 内置 GLTFDocument**: VRM 即 glTF 2.0 容器,实测可完整解析 154 骨骼的 VRoid 模型,且 `GLTFState.json` 可直接读取 VRM 扩展判定版本。 ## 骨骼名映射 `HumanoidMap` 覆盖 VRoid / VRM 0.x / Mixamo 三类命名,归一化后精确比较: -- 剥离前缀 `jbip` / `mixamorig` / `bip01`(**归一化之后**的形态) -- 生成 compact(left->l)与 expanded(l->left)两种变体比较 -- **故意不剥离 `J_Aim_` / `J_Roll_` / `J_Sec_`**:VRoid 用它们标记扭转辅助骨, +- 剥离前缀 `jbip` / `mixamorig` / `bip01`(**归一化之后**的形态) +- 生成 compact(left->l)与 expanded(l->left)两种变体比较 +- **故意不剥离 `J_Aim_` / `J_Roll_` / `J_Sec_`**: VRoid 用它们标记扭转辅助骨, 剥离后 `J_Aim_L_UpperLeg` 会与真正的 `J_Bip_L_UpperLeg` 撞名 ## 验收 -`tests/test_vrm_loader.gd`: +`tests/test_vrm_loader.gd`: - 三类命名规范的识别(含辅助骨不遮蔽真骨的回归用例) -- 真实 VRM:154 骨骼 / 3 网格 / VRM 1.0,**humanoid 覆盖率 100%** +- 真实 VRM: 154 骨骼 / 3 网格 / VRM 1.0, **humanoid 覆盖率 100%** - 缺失文件、普通 glTF 不误判 diff --git a/tasks/006-skeleton-retarget.md b/tasks/006-skeleton-retarget.md index e85b202..08e05b8 100644 --- a/tasks/006-skeleton-retarget.md +++ b/tasks/006-skeleton-retarget.md @@ -1,7 +1,7 @@ # T-WIN-006 关键点 -> VRM 骨骼重定向 -- **状态**:done -- **依赖**:T-WIN-005、T-WIN-004 +- **状态**: done +- **依赖**: T-WIN-005、T-WIN-004 ## 目标 @@ -18,20 +18,20 @@ src/anim/skeleton_retarget.gd ## 算法 1. 每根骨骼指定一条骨链(起点关键点,终点关键点) -2. 取骨骼静止朝向 `rest_dir`(由该骨与首个子骨的位置差得出,**不假设模型骨骼轴向**) +2. 取骨骼静止朝向 `rest_dir`(由该骨与首个子骨的位置差得出,**不假设模型骨骼轴向**) 3. 目标朝向由两个关键点在世界空间的方向得出 -4. `delta` = rest_dir -> target_dir 的最小旋转四元数(保留扭转/roll) +4. `delta` = rest_dir -> target_dir 的最小旋转四元数(保留扭转/roll) 5. `desired = Basis(delta) x rest_global_basis` 6. `pose_local = parent_global_basis⁻¹ x desired` -**第 6 步的合成公式是实测标定的**:Godot 文档未写明 rest/pose 的合成顺序, +**第 6 步的合成公式是实测标定的**: Godot 文档未写明 rest/pose 的合成顺序, 在真实 VRoid 模型上暴力枚举 5 种候选公式,只有该式误差为 1.9e-8(其余 ~0.14)。 已有回归测试守护,不得凭记忆改写。 ## 坐标转换 契约 4 的 MediaPipe 归一化坐标 -> Godot 世界方向: -`Vector3(x x aspect, -y, -z x aspect)`(x/z 以图像宽为基准故乘宽高比)。 +`Vector3(x x aspect, -y, -z x aspect)`(x/z 以图像宽为基准故乘宽高比)。 ## 验收 diff --git a/tasks/007-scoring-dtw.md b/tasks/007-scoring-dtw.md index c112f06..143135c 100644 --- a/tasks/007-scoring-dtw.md +++ b/tasks/007-scoring-dtw.md @@ -1,7 +1,7 @@ # T-WIN-007 DTW 评分引擎 -- **状态**:done -- **依赖**:T-WIN-004、T-WIN-002 +- **状态**: done +- **依赖**: T-WIN-004、T-WIN-002 ## 目标 @@ -30,9 +30,9 @@ src/score/scoring.gd 每帧只计算带内一行,总体复杂度 O(n m band) 但摊到每帧 O(m band) - 归一化:代价 / max(i,j)+1,得到「平均每步代价」 -## 评分规则(`scoring.gd`) +## 评分规则(`scoring.gd`) -`score = clamp(1 - cost/tolerance, 0, 1) x 100`,EMA 平滑抑制抖动, +`score = clamp(1 - cost/tolerance, 0, 1) x 100`, EMA 平滑抑制抖动, 等级 S(>=95) / A(>=85) / B(>=70) / C(>=55) / D,连击阈值 70。 ## 验收 diff --git a/tasks/008-game-ui.md b/tasks/008-game-ui.md index 13659e9..74bed9c 100644 --- a/tasks/008-game-ui.md +++ b/tasks/008-game-ui.md @@ -1,11 +1,11 @@ # T-WIN-008 主界面 + 游戏界面 + 结算 -- **状态**:done -- **依赖**:T-WIN-002、T-WIN-003、T-WIN-006、T-WIN-007 +- **状态**: done +- **依赖**: T-WIN-002、T-WIN-003、T-WIN-006、T-WIN-007 ## 目标 -三页式体验:主菜单(选曲/选动作/选模型)-> 游戏页(3D 角色 + 实时评分 HUD)-> 结算页。 +三页式体验:主菜单(选曲/选动作/选模型)-> 游戏页(3D 角色 + 实时评分 HUD)-> 结算页。 ## load diff --git a/tasks/009-source-display.md b/tasks/009-source-display.md index f5e42e4..6616edc 100644 --- a/tasks/009-source-display.md +++ b/tasks/009-source-display.md @@ -1,7 +1,7 @@ # T-WIN-009 来源信息展示 -- **状态**:done -- **依赖**:T-WIN-008 +- **状态**: done +- **依赖**: T-WIN-008 ## 目标 @@ -26,7 +26,7 @@ src/net/catalog_types.gd - 资产名 | 来源名称 - 作者 - 许可 + **可点击外链**跳转到 `sourceUrl` -- **五元组不齐备时显式告警**("⚠ 来源信息不完整,请联系管理员补全后再使用"),不静默展示 +- **五元组不齐备时显式告警**("⚠ 来源信息不完整,请联系管理员补全后再使用"),不静默展示 ## 使用位置 @@ -37,4 +37,4 @@ src/net/catalog_types.gd ## 验收 `tests/test_api_integration.gd` 对真实服务端断言曲目与模型的来源五元组齐备 -(`song.source.is_complete()`),保证展示组件拿到的数据完整。 +(`song.source.is_complete()`),保证展示组件拿到的数据完整。 diff --git a/tasks/010-about-page.md b/tasks/010-about-page.md index f69a186..a4b42cb 100644 --- a/tasks/010-about-page.md +++ b/tasks/010-about-page.md @@ -1,7 +1,7 @@ # T-WIN-010 关于页面 + 开源声明 -- **状态**:done -- **依赖**:T-WIN-008 +- **状态**: done +- **依赖**: T-WIN-008 ## 目标 @@ -19,7 +19,7 @@ src/ui/credits.gd ## 交付物 - `src/ui/credits.gd`:与 `contracts/open-source-credits.md` 逐条对应的数据表,渲染为 BBCode -- `src/ui/about_screen.gd`:应用信息 + 契约版本 + 隐私说明 + 可滚动声明(RichTextLabel) +- `src/ui/about_screen.gd`:应用信息 + 契约版本 + 隐私说明 + 可滚动声明(RichTextLabel) ## 要点 diff --git a/tasks/011-build-script.md b/tasks/011-build-script.md index 4780635..1f5a6da 100644 --- a/tasks/011-build-script.md +++ b/tasks/011-build-script.md @@ -1,7 +1,7 @@ # T-WIN-011 Windows 打包脚本 -- **状态**:done -- **依赖**:T-WIN-001 ~ 010 +- **状态**: done +- **依赖**: T-WIN-001 ~ 010 ## 目标 @@ -20,7 +20,7 @@ export_presets.cfg | 文件 | 说明 | |--|--| -| `export_presets.cfg` | 导出预设(**必须在项目根目录**,Godot 硬性要求) | +| `export_presets.cfg` | 导出预设(**必须在项目根目录**, Godot 硬性要求) | | `build/export-windows.sh` | Linux 导出:定位 Godot -> 检查模板 -> 导入 -> 导出 -> **校验 PE 头** | | `build/export-windows.ps1` | Windows 兜底:同上,产物路径与校验逻辑一致 | @@ -28,15 +28,15 @@ export_presets.cfg - `GODOT_BIN` 环境变量或 PATH 自动定位 Godot,缺失时给出可执行的安装指引 - 导出模板缺失时明确报错并给出下载与安装路径 -- 导出命令返回非 0 时**仍检查产物是否存在**(Godot 有时非 0 返回但产物有效) -- 读取产物前 2 字节校验 `MZ`(PE 头),防止导出静默产出空壳 +- 导出命令返回非 0 时**仍检查产物是否存在**(Godot 有时非 0 返回但产物有效) +- 读取产物前 2 字节校验 `MZ`(PE 头),防止导出静默产出空壳 -## 验收(文档 5.4) +## 验收(文档 5.4) | 验收项 | 结果 | |--|--| -| Linux:`bash build/export-windows.sh` 成功生成 exe | ✅ 生成 `OpenDance.exe` 104 MB + `OpenDance.pck` 107 KB,PE 头校验通过 | -| Windows:`powershell build/export-windows.ps1` 成功生成 exe | ⚠️ 脚本就绪,本环境无 Windows,未实机执行 | +| Linux: `bash build/export-windows.sh` 成功生成 exe | ✅ 生成 `OpenDance.exe` 104 MB + `OpenDance.pck` 107 KB, PE 头校验通过 | +| Windows: `powershell build/export-windows.ps1` 成功生成 exe | ⚠️ 脚本就绪,本环境无 Windows,未实机执行 | | 生成的 exe 在 Windows 10/11 可运行 | ⚠️ 本环境无 Windows;已校验为合法 PE32+ x86_64 可执行文件 | | `THIRD_PARTY.md` 包含所有开源项目与源链接 | ✅ 15 项,与契约表逐条一致 | @@ -53,6 +53,6 @@ opendance-windows-v0.1.0.zip ## 环境记录 -- Godot 4.7.2.stable(`ed1daf0bf`) -- 导出模板 `4.7.2.stable`(``windows_release_x86_64.exe``) +- Godot 4.7.2.stable(`ed1daf0bf`) +- 导出模板 `4.7.2.stable`(``windows_release_x86_64.exe``) - 首次导出失败原因:`export_presets.cfg` 放在 `build/` 下,Godot 只在**项目根目录**查找