zijunchiang 14ee5f7ec9 fix(scene): 包围盒沿树累乘变换(修掉入树前的 ERROR 与范围偏位)+ 21 项"真机等价"测试
缺陷:SceneRuntime._compute_aabb() 用 MeshInstance3D.global_transform 计算包围盒,
而它在 _assemble() 里调用 —— 那时运行时节点**还没入树**。两个后果:
- Godot 打一行 `ERROR: Condition "!is_inside_tree()" is true. Returning: Transform3D()`
  (真机每次载入场景都刷一条,与"日志里不应有 ERROR"的排查约定冲突);
- 树外的 global_transform 返回单位变换,算出来的只是各网格的**局部** AABB,
  于是重力/风 Area3D 的碰撞盒与兜底地面会偏小或偏位 —— 对把网格摆在非原点位置的
  场景(导出 GLB 的常见情况)**风力可能作用不到物体上**。

修复:改为从根往下累乘 `Node3D.transform`,不依赖是否入树;挂在单位变换父节点下时
结果与旧语义一致。

新增 21 项无头"真机等价"测试(不需要设备):
- tests/test_scene_config.gd:在代码里手写合法 GLB(三角形 + BIN chunk)落盘后
  **真实加载**:warnings 为空、按 colliders 三种取值分别核对碰撞体(mesh / auto 命中
  *_col / auto 未命中的兜底地面 / none)、重力 Area3D 的 gravity 与方向、
  SPACE_OVERRIDE_REPLACE、风力与 WIND_FORCE_SCALE 对齐,以及**带位移网格**
  (translation [10,0,0])时重力区域与兜底地面都跟着位移(该缺陷的回归测试);
- tests/test_pose_server.gd:**时间维度**验证保活 —— 只发应用层 ping、不发关键点帧的
  客户端在 17 秒(> 15 秒静默超时)后仍保持连接("画面里没人就掉线、疯狂重连"的根因
  对照),而完全不发数据的客户端被 close(1001) 断开。

新增 build/check-version.sh:强制 project.godot 的 application/config/version
(「关于」清单与 .pck 的来源)与 project.yaml、发布版本号三者一致;已接入
build/package-release.sh(打包前)与 build/verify.sh(第 0/4 步),并做过反向验证。
此前没有任何机制保证这两个文件同步 —— 界面显示的版本可能和产物名不一致。

版本升至 0.2.1(project.godot / project.yaml / README / 产物名 / tag 全部一致),
About 清单与 .pck 内版本号均为 0.2.1(已用 strings 核对)。

验证:build/verify.sh 四步全通过(版本一致性 → 编译 → 751 项断言 → 180 帧启动无错误);
产物 opendance-windows-v0.2.1.zip(LZMA,26,627,191 B)
sha256 d49885327047f5a52de6c732431ea080cd1a030ae1c581a8bc0ec0d99bfb792c。
2026-09-17 03:59:29 +08:00

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.1.zip + SHA256SUMS(不拆分卷)
版本号展示 OK 「关于」清单显示 v0.2.0,来源唯一(project.godot)
场景物理与保活 OK 新增真实 GLB 端到端用例 + 17 秒保活对照用例(751 项断言全过)

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 步),并做过反向验证。

快速开始

# 验证(编译 -> 测试 -> 启动)
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.1

当前版本:v0.2.1(project.godot 的 application/config/version, 与 project.yaml、导出产物名、Git tag、「关于」清单展示一致)。

详细使用说明见 README.txt,工程约束见 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. 获取代码

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. 打包导出

# 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.1
产物 位置 说明
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.1.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
服务端联通 主界面「服务端地址」->「保存并测试」 顶部显示「服务端正常

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/ 是只读快照。任何跨端契约变更必须先回到 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 的 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 上限已放开,请改用默认的 deflate 包 (bash build/package-release.sh 0.2.1,产物约 38 MB,双击即可解压)。

第三方开源声明

见 THIRD_PARTY.md,或客户端「关于」页面。 未使用 SMPL 模型及其衍生拓扑,统一采用 MediaPipe 33 点拓扑。

S
Description
OpenDance Studio - opendance-windows
Readme
1 MiB
2026-09-18 02:47:05 +08:00
Languages
GDScript 75.7%
Shell 14.5%
JavaScript 7.3%
Python 1.6%
PowerShell 0.9%