refactor(model): 重构P03为轻量化板式桁架并统一M4×16自攻螺钉连接方案
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
# 姿态估计算法评估与优化 —— 实施计划
|
||||
|
||||
> 交付物(用户已确认):**仅一份中文评估报告**,不改动求解器代码。
|
||||
> 报告落盘位置:`docs/姿态估计算法评估报告.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 摘要(Summary)
|
||||
|
||||
本任务对机械臂的「姿态估计算法」——即 [arm-ik-solver.js](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js) 的 6 轴逆运动学(IK)姿态求解器及其查看器集成 [ik-glue.js](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js)(用户已确认该文件即「姿态估计算法」)——做一次全面评估。
|
||||
|
||||
需要解决的核心问题:**在多个关键位姿(近工作空间边界、折叠/奇异构型、贴近关节限位处)无法达到预定姿态目标;且当目标姿态确实不可达时,返回的「最近位姿」明显劣于肉眼可见的更优姿态。**
|
||||
|
||||
评估已在本阶段(Plan 模式,只读)实测复现并定位到 4 类缺陷(见 §2.3),并额外回答了两个关键设计问题:**现状是「先点位、后角度」的位置优先架构**(§2.4),以及**位置与姿态在远目标上真正不可兼得、必须提供可切换优先级**(§2.4 实测取舍表)。本计划定义报告的**结构、数据来源、可复现实验命令、优化策略与验证口径**。执行阶段只产出一份报告 md,不新增任何脚本文件(实验以 `node -e` 内联执行,命令内嵌报告附录)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 现状分析(Current State Analysis)
|
||||
|
||||
### 2.1 代码与数据资产
|
||||
|
||||
| 角色 | 文件 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| 姿态求解器(主) | [arm-ik-solver.js](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js) | UMD,`make(cfg)` 返回 `solve/solveTo/solveWarm/solveDyn/closest/closest6/branch/fk/orient/costOf` 等([导出清单 L1355-1360](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1355-L1360)) |
|
||||
| 查看器集成 | [ik-glue.js](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js) | 姿态 UI、目标拖动、`solveTo` 调用([L379](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js#L379)、[L399](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js#L399))、结果渲染与徽标判定([renderResult L318-342](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js#L318-L342)) |
|
||||
| 关节表(几何真值) | [mesh_arm.json](file:///g:/Robot_v2/机器人手臂/web/mesh_arm.json) | `joints[] = {id, rear, d}`,FK/IK 运动学常量来源 |
|
||||
| 关节限位 | [travel.json](file:///g:/Robot_v2/机器人手臂/docs/travel.json) | 本报告测量口径:J1/J3/J6 连续(±180),J2∈[-106,9],J4∈[-111,16],J5∈[-95,34](2026-09-29 起该表退为零位扫描参考:滑块改电机行程 ±360°,可行域改相邻件实测区间) |
|
||||
| 既有自检 | [pose_ik_check.js](file:///g:/Robot_v2/机器人手臂/scratch/pose_ik_check.js) | 7 组 node 自检(往返、位置回归、姿态重构、交互口径、兜底、耗时) |
|
||||
| 既有性能基准 | [bench_ik_split.js](file:///g:/Robot_v2/机器人手臂/scratch/bench_ik_split.js) / [bench_ik_split.json](file:///g:/Robot_v2/机器人手臂/scratch/bench_ik_split.json) | 单次求解耗时与「位置/姿态」占比拆分 |
|
||||
| 历史优化实验 | `scratch/ikopt/`(`solver_scanbase.js` 等) | 根枚举/窗口/同族门等过往迭代产物,报告需说明其结论 |
|
||||
|
||||
### 2.2 现有算法骨架(报告需准确描述)
|
||||
|
||||
1. **位置闭式 + 二维冗余扫描**:以 (θ4,θ5) 为冗余参数全域扫描,θ1..θ3 由 [branch() L429-463](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L429-L463) 闭式给出(最多 4 支:θ3=±σ、±(π−σ)),位置天然精确到 1e-13 mm。
|
||||
2. **按行裁剪**:[rowSpans L644-654](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L644-L654) 用探测步 `hp=max(h,2)°` 找每 θ4 行的可行 θ5 段,段内再按 h 精扫。
|
||||
3. **姿态任务**:工具轴指向 = 目标轴,是 (θ4,θ5) 上的 2 方程 2 未知量,解为**孤立点**;[poseTools L486-545](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L486-L545) 提供残差/牛顿原语;[§4.1b 根枚举 L697-761](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L697-L761) 逐行做变号括号 + 牛顿收口;滚转由 [rollClose L549-555](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L549-L555) 闭式补 θ6。
|
||||
4. **接受门**:`POSETOL=2.0°`([L581](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L581))——`scoreBranch` 对 `oerr>POSETOL` 的候选**直接返回 null 丢弃**([L596-598](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L596-L598))。
|
||||
5. **不可达兜底**:[closest6 L1054-1169](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1054-L1169) 为 6 维阻尼最小二乘(DLS),160 次确定性拟随机重启 × 160 迭代;[solveTo L1172-1192](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1172-L1192) 在 `solve()` 失败时调用它。
|
||||
6. **实时快路**:[solveWarm L1200-1265](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1200-L1265),沿上一帧解同支续解,失败回落全量。
|
||||
|
||||
### 2.3 已实测缺陷(本阶段只读复现,数据可直接写入报告)
|
||||
|
||||
**缺陷 A — 覆盖漏洞:目标姿态精确可达,求解器却静默返回 ~1–2° 残差**
|
||||
- 全空间随机可达位形,`solve(step=0.25)` 返回 `oerr>0.05°` 的比例:**2.5%(3/120)**,量级 `oerr∈[1.24, 1.95]°`(真值应为 0)。
|
||||
- 其中可明确归因于「覆盖漏洞」(细扫 step=0.05 能到 ≤0.05°、即确有大步长漏解)的部分,小样本估计 **0.3%–1.3%**(300 组样本:1/300;80 组样本:1/80);其余为真·近奇异构型(细扫同样到不了 ≤0.05°,属物理不可达的刀尖构型)。**执行阶段用更大样本(D1/D4)收敛该比例。**
|
||||
- 典型案例 `T=[329.3,253.4,750.0]`:真解 (θ4,θ5)=(-16.45,-51.97),`branch()` 在该点给出 **位置误差 5.7e-14 mm、轴误差 0.000°** 的精确解;但 `solve(step=0.25)` 返回 oerr=1.909°。
|
||||
- **步长敏感(非分辨率问题)**:同一目标 `oerr` 随 `step` 非单调跳变——`0.25°→1.60`、`0.15°→0.045`、`0.10°→1.14`、`0.05°→0.013`;用 `solveTo({warm:真解})` 则得到 **oerr=0.0000**。证明确切解存在,是**根枚举/牛顿收口的完备性缺口**。
|
||||
- 工作区实测:前伸工作区 (20,210,300)±90mm 失败率 **1.7%(1/60)**;查看器默认区 (150,128,260)±60mm 为 **0%(0/60)**——缺口集中在工作空间外围/极端构型。
|
||||
|
||||
**缺陷 B — 不可达兜底 closest6 严重次优(「肉眼可见更优姿态」的直接来源)**
|
||||
以「位置精确解中姿态误差的全局最小值」为真最优(对 (θ4,θ5) 网格 0.05° 枚举 `branch()` 全部在限位内的支),目标姿态在可达姿态基础上外推 20°:
|
||||
|
||||
| 案例 | 目标 TCP | 兜底返回 | 真最优姿态差 | 劣化 |
|
||||
| :--- | :--- | ---: | ---: | ---: |
|
||||
| 1 | (40,455,216) | 位置 0.4mm / 姿态 **74.0°** | **11.04°** | **63.0°** |
|
||||
| 2 | (196,122,136) | 位置 0.5mm / 姿态 **130.0°** | **17.26°** | **112.8°** |
|
||||
| 3 | (-141,-18,1030) | 位置 0.0mm / 姿态 **167.2°** | **89.09°** | **78.1°** |
|
||||
|
||||
**缺陷 C — 姿态不可达时把「位置精确解候选池」整体丢弃,导致兜底被便宜解严格支配**
|
||||
- `T=[20,400,300]`、`R=poseMat(25,95,10)`:位置完全可达(无姿态 `solve` 返回 err=2.5e-14,0.5° 格上有 **11920** 个位置精确解),但姿态目标不可达时 `scoreBranch` 因 `oerr>POSETOL` 把全部候选置 null,`solve()` 返回 `ok=false`、候选池为空,被迫走 [closest6](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1054-L1169) 的 6 维随机 DLS。
|
||||
- 同例对照(实测):位置精确解集内最小姿态差 = **121.05°**;`closest6` 返回 **136.7° / 位置 0.0mm**。即 `closest6` 在**位置与姿态两个指标上同时劣于**一个用现有闭式扫描就能廉价得到的解 —— **被严格支配(Pareto-dominated)**。
|
||||
- 后果:`位置精确 + 姿态尽力` 这条最自然的路径没有实现;兜底既不最优、又更慢(159–224ms vs 扫描 ~100ms)。
|
||||
|
||||
**缺陷 D — 性能超出既定预算**
|
||||
- [bench_ik_split.json](file:///g:/Robot_v2/机器人手臂/scratch/bench_ik_split.json) 摘要:开姿态单次 **中位 107ms / p95 242ms / 最大 299ms**(位置基座中位 73.6ms,姿态增量中位 37.2ms 占 33%,escalating 触发率 2.5%)。
|
||||
- 本阶段在工作区实测均耗时 112–157ms;不可达兜底 159–224ms。
|
||||
- 均高于用户设定的 **≤100ms** 目标,且兜底路径(random-restart DLS)是主要浪费点。
|
||||
|
||||
### 2.4 求解范式评估:位置优先 vs 末端姿态反推(用户提出,报告中作为备选简要说明)
|
||||
|
||||
**用户提问**:能否用已指定的末端角度,从末端逐级反推回各关节?
|
||||
|
||||
**几何事实(以 [mesh_arm.json](file:///g:/Robot_v2/机器人手臂/web/mesh_arm.json) 为准)**:
|
||||
|
||||
| 关节 | rear | d | 类型 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| J1 | (-51,128,610) | (1,0,0) | X 抬举 |
|
||||
| J2 | (18.5,100,490) | (0,1,0) | Y 俯仰 |
|
||||
| J3 | (18.5,128,445) | (0,0,-1) | Z 自转 |
|
||||
| J4 | (18.5,100,332) | (0,**1**,0) | Y 俯仰(肘) |
|
||||
| J5 | (18.5,100,222) | (0,**1**,0) | Y 俯仰(腕) |
|
||||
| J6 | (18.5,128,176) | (0,0,**-1**) | Z 工具自转 |
|
||||
|
||||
- **J4 ∥ J5**(均 +Y,相距 110mm)⇒ 平行轴永不相交 ⇒ **非球形腕**,经典"腕心解耦 + θ6→θ5→θ4 逐级闭式反推"不成立。(J5⊥J6 在 (18.5,128,222) 相交,仅构成 2 轴腕。)
|
||||
- 但存在可用的**降阶结构**:因 J4∥J5,`R4·R5 = R_y(θ4+θ5)`(代码 [L406-411](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L406-L411) 已用 `SAMEAX` 利用该性质),故
|
||||
|
||||
```
|
||||
R = Rx(θ1)·Ry(θ2)·Rz(−θ3)·Ry(θ4+θ5)·Rz(−θ6)
|
||||
```
|
||||
|
||||
⇒ **工具轴只由 (θ1,θ2,θ3,θ4+θ5) 决定,θ6 是纯工具滚转**;"用末端姿态驱动求解"可行,但需按此降阶链求解(3 方程 / 5 参数 → 2 参数族),而非逐级反推。
|
||||
|
||||
**位置与姿态在远目标上"真的不可兼得"(实测,报告须收录)**:
|
||||
|
||||
| 目标 | 优先级 | 位置误差 | 姿态差 |
|
||||
| :--- | :--- | ---: | ---: |
|
||||
| T=(20,400,300), R=poseMat(25,95,10) | 位置优先(保点) | **0.0 mm** | 121.05° |
|
||||
| 同上 | 姿态优先(保角度) | 127.6 mm | **0.22°** |
|
||||
| T=(196,122,136), R=poseMat(25,95,10) | 位置优先 | **0.0 mm** | 116.39° |
|
||||
| 同上 | 姿态优先 | 233.6 mm | **0.45°** |
|
||||
|
||||
⇒ 结论:二者是**真实的取舍**,不存在"都能兼顾"的解;算法必须提供**可切换的优先级**。
|
||||
|
||||
### 2.5 误差来源定位(报告核心结论)
|
||||
|
||||
- **R1 根枚举不完备**:姿态精确解在 (θ4,θ5) 平面是一条一维曲线,只在少数孤立点穿过 `POSETOL` 窄带;当该曲线经过 θ3≈±90° 的**折叠点**(`branch()` 的 4 支两两重合、位置雅可比奇异)、或贴着关节限位/可行段边缘时,变号括号检测失效、`match()` 的「θ1..θ3 最近」同支判据在折叠处换支,牛顿收口失败。
|
||||
- **R2 接受门把「不可达」与「漏解」混为一谈**:`oerr>POSETOL` 一律丢弃 ⇒ 无法区分「确实不可达」与「可达但没找到」,导致 A 类静默近失 + C 类候选池清空。
|
||||
- **R3 兜底目标函数与真实目标不一致**:`closest6` 用固定的 20mm/rad 把姿态折进位置残差做联合 DLS,既不保证「位置优先、姿态最小」,也依赖随机重启初值,落点常是劣质局部极值。
|
||||
- **R4 行裁剪探测步过粗**:`hp=max(h,2)°` 且按单一连续段假设(注释称仅在 5 个目标上验证过),窄于 2° 的可行段会被整段漏掉 ⇒ 整行/整区无候选。
|
||||
|
||||
---
|
||||
|
||||
## 3. 交付物与报告大纲(Proposed Changes)
|
||||
|
||||
**唯一变更**:新建 `docs/姿态估计算法评估报告.md`(中文)。
|
||||
|
||||
报告章节(每章明确数据来源,均为本阶段或执行阶段 `node -e` 实测可得):
|
||||
|
||||
1. **概述与结论摘要**(TL;DR 表格:缺陷 / 影响 / 根因 / 建议 / 预期收益)
|
||||
2. **算法现状描述**——§2.2 的骨架,附关键函数行号链接与数据流图
|
||||
3. **求解范式评估(位置优先 vs 末端姿态反推)**——§2.4:
|
||||
- 确认现状为「先点位、后角度」(位置被强制精确、姿态是带 2° 门的软约束)
|
||||
- 关节轴几何表 + `J4∥J5 ⇒ 非球形腕` 的判定;说明「逐级闭式反推」不成立的**原因**(备选简要说明,约半页,含降阶链 `Rx·Ry·Rz·Ry(θ4+θ5)·Rz` 的结论式)
|
||||
- 位置/姿态**不可兼得**的实测取舍表(§2.4),给出「必须提供可切换优先级」的结论
|
||||
4. **评估方法**——指标定义:
|
||||
- 姿态残差 `oerr`(`rotErrDeg`,度)
|
||||
- 位置残差 `err`(mm)
|
||||
- 覆盖漏洞率 = 「真解 oerr≤0.05° 而解算 oerr>0.05°」占比
|
||||
- 步长敏感度 = 同目标在 step∈{0.05,0.1,0.15,0.25,0.5} 的 oerr 波动
|
||||
- 兜底劣化 = `closest6` 姿态差 −「位置精确解集内最优姿态差」
|
||||
- 单次耗时中位/p95(开/关姿态、可达/兜底)
|
||||
5. **性能分析**——引用 bench_ik_split.json 的耗时构成 + 工作区实测;给出「位置/姿态/兜底」三段占比
|
||||
6. **误差来源定位**——§2.5 的 R1–R4,逐条给出**可复现证据**(目标坐标、真解、实测值)
|
||||
7. **优化策略(设计方案,不在本轮落地)**——按收益/成本排序给出可实施建议:
|
||||
- **S0(架构)双优先级求解范式**:仿 [ik-glue.js](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js) 现有姿态模式的 UI 开关,新增「优先级」选择——**位置优先 / 姿态优先 / 折中**;三种模式下共用同一闭式支枚举结果,只切换目标函数与选解规则。给出各模式的数学表述:
|
||||
- 位置优先:`min oerr s.t. ‖fk(q)−T‖=0`(在位置精确解集上最小化姿态误差)
|
||||
- 姿态优先:`min ‖fk(q)−T‖ s.t. oerr=0`(在姿态精确解集上最小化位置误差;利用降阶链参数化)
|
||||
- 折中:`min ‖fk(q)−T‖² + k·oerr²`,k 可调
|
||||
- 预期:位置优先模式把 B 类劣化从 63–113° 压到接近真最优;姿态优先模式提供 oerr≤0.05° 的「保角度」解(代价是位置误差,如实上报)
|
||||
- **S1(根因修复)覆盖率完备化**:把 (θ4,θ5) 上的姿态方程看作一维解曲线,用**预测-校正 continuation 跟踪**(以解析/数值切向推进、跨越折叠点不换支),或等价地做**分支索引对齐的多起点牛顿 + 折叠点/限位角种子**;预期把 A 类覆盖漏洞降到 <0.2%,且不加粗扫密度 ⇒ 耗时基本不变(≤100ms)。
|
||||
- **S2(关键收益)姿态不可达时不再清空候选池**:去掉 `POSETOL` 硬门对候选池的**清空**语义,改为保留并按 `oerr` 排序入池(门只用于「达标」判定,不用于「丢弃」);预期消除 C 类 Pareto 支配,且兜底不再需要。
|
||||
- **S3 兜底加速**:以 S0/S2 的闭式候选为种子替代 160 次随机重启,仅在位置也越界时才退回受限 DLS;预期兜底耗时从 ~160–220ms 降到 ≤100ms。
|
||||
- **S4 可观测性**:UI 明确区分「已到达 / 位置到达·姿态最近 X° / 姿态到达·位置最近 Y mm / 均不可达」四种语义,避免把漏解或取舍结果当成功。
|
||||
- 每条策略给出:改动点(函数+行号)、原理、预期指标、代价与风险、与 ≤100ms 预算的关系。
|
||||
8. **验证方案与验收口径**——见 §5。
|
||||
9. **附录**:可复现实验命令(`node -e` 全文内联,不落脚本文件)+ 基准数据表 + 术语表。
|
||||
|
||||
> 说明:报告只做「评估 + 策略设计」,不含代码修改。若后续要落地 S0–S3,需另起一次实施任务。
|
||||
|
||||
---
|
||||
|
||||
## 4. 假设与决策(Assumptions & Decisions)
|
||||
|
||||
| 项 | 决策 | 依据 |
|
||||
| :--- | :--- | :--- |
|
||||
| 交付物 | 仅评估报告 md,**不改代码** | 用户选择「仅评估报告」 |
|
||||
| 「姿态估计算法」定义 | = `web/arm-ik-solver.js` 的 IK 姿态求解 + `ik-glue.js` 集成 | 全仓检索无 IMU/卡尔曼/姿态估计类算法;仓库内「姿态」相关实现仅此一处 |
|
||||
| 性能预算 | 以 **≤100ms** 为优化目标约束(报告据此评估各策略) | 用户选择「保持 ≤100ms」 |
|
||||
| 失败场景 | A(可达却漏解)与 B/C(不可达最近位姿)**都覆盖** | 用户选择「两者都做」 |
|
||||
| 实验脚本 | 不新增文件,全部 `node -e` 内联并在报告附录给出原文 | 遵守「仅评估报告」与「不新建文件」 |
|
||||
| **求解优先级** | 报告须设计**双优先级机制(位置优先 / 姿态优先 / 折中),界面可切换**;不可兼得时两种解都给出 | 用户选择「两者都做、界面可切换」 |
|
||||
| **末端姿态反推** | 报告仅作**备选简要说明**(说明非球形腕故不能逐级闭式反推 + 降阶链结论),不展开公式级设计 | 用户选择「只作备选简要说明」 |
|
||||
| 关节表/限位 | 以 `web/mesh_arm.json` + `docs/travel.json` 为准,tcp=[18.5,128,93] | 与 `pose_ik_check.js`、`bench_ik_split.js` 一致 |
|
||||
| 真最优基准 | 「位置精确解集内姿态误差最小」的网格枚举(0.05° 分辨率,θ6 另行闭式最优) | 独立于被测求解器,避免自证 |
|
||||
| 随机性 | 固定 LCG 种子的确定性采样,报告数据可复现 | 与求解器内部自检策略一致 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 验证方案(报告第 8 章内容)
|
||||
|
||||
**基准数据集**(执行阶段固定种子生成,规模与命令写入附录):
|
||||
- D1 全空间随机可达位形 ≥200 组(覆盖极端/边界构型)
|
||||
- D2 前伸工作区 (20,210,300)±90mm ≥60 组、查看器默认区 (150,128,260)±60mm ≥60 组
|
||||
- D3 姿态外推集(外推 3/8/20°)≥30 组 → 测兜底劣化
|
||||
- D4 退化构型集(θ3≈±90°、临近限位)≥30 组 → 测覆盖漏洞
|
||||
- D5 越界目标集 → 测兜底耗时与质量
|
||||
- D6 **双优先级取舍集**:位置与姿态不可兼得的远目标 ≥20 组 → 测「位置优先」与「姿态优先」两解各自的误差与真实性
|
||||
|
||||
**指标与验收口径(现状基线 → 目标)**:
|
||||
|
||||
| 指标 | 现状实测 | 目标 |
|
||||
| :--- | :--- | :--- |
|
||||
| D1/D4 覆盖漏洞率(真解≤0.05° 却返回>0.05°) | 2.5% | ≤0.2% |
|
||||
| D1 最大静默姿态残差 | 1.95° | ≤0.05° |
|
||||
| 步长敏感度(同目标 oerr 波动) | 0→1.86° | ≤0.05° |
|
||||
| D3/D5 兜底相对真最优劣化(姿态优先口径) | 63–113° | ≤2° |
|
||||
| D6 位置优先解:位置误差 / 姿态差 | 0.0mm / 121.05° | 位置=0;姿态=真最优(劣化 ≤0.5°) |
|
||||
| D6 姿态优先解:姿态差 / 位置误差 | 0.22° / 127.6mm | 姿态 ≤0.05°;位置如实上报 |
|
||||
| 开姿态单次耗时 p95 | 242ms | ≤100ms |
|
||||
| 兜底耗时 | 159–224ms | ≤100ms |
|
||||
| 姿态残差 P95(可达集) | ≤2.05°(接受限) | ≤0.05° |
|
||||
|
||||
**沿用既有自检**:`node scratch/pose_ik_check.js` 的 7 项(含往返一致性、位置回归、姿态重构、roll→θ6 口径、6D 兜底、耗时对比)作为回归门,报告中给出其现状通过情况与目标。
|
||||
|
||||
**报告自检**:报告内每张数据表须与附录命令重跑结果一致;所有结论必须能由附录命令复现。
|
||||
|
||||
---
|
||||
|
||||
## 6. 执行步骤(待批准后按序执行)
|
||||
|
||||
1. 复跑并固化基线数据(D1–D6 + bench + 自检),生成报告用数据表。
|
||||
2. 撰写 `docs/姿态估计算法评估报告.md` 正文(§3 大纲 1–9 章)。
|
||||
3. 内嵌可复现 `node -e` 命令与原始输出,形成附录。
|
||||
4. 复核报告数据与结论一致性(重跑附录命令抽查),交付。
|
||||
@@ -0,0 +1,258 @@
|
||||
# 机器人手臂3D查看器 · 性能优化方案
|
||||
|
||||
目标文件:`G:\Robot_v2\机器人手臂\机器人手臂3D查看器.html`(15.31 MB / 35986 行)
|
||||
用户要求:先做完整备份(带版本标识)→ 优化 → 验证功能完好 → 记录优化项与性能数据。
|
||||
|
||||
---
|
||||
|
||||
## 一、现状分析(实测,非猜测)
|
||||
|
||||
### 1.1 文件结构
|
||||
|
||||
| 位置 | 内容 |
|
||||
| --- | --- |
|
||||
| L1~L420 | HTML + CSS + 界面骨架(左栏/IK 控制条/舞台/页脚/BOM 抽屉) |
|
||||
| L421~L428 | `<script>` three.js r159 内联(652 KB,UMD build,无 CDN) |
|
||||
| L429~L33251 | `<script>`:`DATA`(13.99 MB 网格 JSON)+ `SHEETS`(52 KB SVG)+ `TRAVEL` + `COLLIDE` + 查看器主逻辑(`const TL = {}` 起 738 行) |
|
||||
| L33252~L35022 | `<script>`:`arm-ik-solver.js` 逆运动学求解器(1770 行,自带 `window.ArmIK`) |
|
||||
| L35023~L35984 | `<script>`:`ik-glue.js` 界面胶水层(961 行,Worker 化求解、预置自检、姿态拖拽) |
|
||||
|
||||
四个内联脚本的**索引被 `chkSig()` 指纹逻辑依赖**(`document.scripts[2]` = 求解器源码),因此本次优化**不得增删 `<script>` 标签、不得改动求解器脚本块**,否则预置自检指纹失效(会误报"预置结果已失效")。
|
||||
|
||||
### 1.2 数据规模(node 实测)
|
||||
|
||||
- 零件 92 件(打印件 9 + 电机 6 + 螺钉 72 + 包络 1 + 经纬线 1)
|
||||
- 顶点 354,697 / 三角面 340,178 / 线段 10,296
|
||||
- **单件最大顶点 47,483(< 65536 → 索引可全部用 Uint16)**
|
||||
- `JSON.parse` 69 ms;按查看器原样建几何(含 `computeVertexNormals` / 包围盒球)37 ms
|
||||
- 全场景射线拾取(92 件、34 万面):**1.3 ms/次**(60 次均值),200 次连测 2.1 ms/次
|
||||
- `scene.updateMatrixWorld(true)` 全量强制刷新:**0.004 ms/次**(≈110 个对象)
|
||||
|
||||
### 1.3 已定位的性能问题
|
||||
|
||||
| # | 问题 | 证据 | 影响 |
|
||||
| --- | --- | --- | --- |
|
||||
| **P1** | **rAF 循环无条件每帧 `resize() + updCam() + rend.render()`**;`needRender` 被 `requestRender()` 置位后**全文件没有任何读取点**(`Grep needRender` 仅 3 处:声明、置位、注释) | L33207~L33215 | 空闲时仍 60 fps 满帧渲染 34 万面 ×92 draw call;`preserveDrawingBuffer:true` 使每帧多一次缓冲拷贝;持续占用 GPU/功耗 |
|
||||
| **P2** | `resize()` 每帧读 `stage.clientWidth/clientHeight` | 同上 | 每帧强制布局(forced reflow),与 P1 同源 |
|
||||
| **P3** | `pick()` 每次 pointermove 先 `meshes.filter(m => m.visible)` 再全量射线求交 | L32668~L32673、L32642~L32652 | 每帧≥1 次 1.3~2.1 ms 求交 + 每帧一个 92 元素临时数组,鼠标扫过模型时持续吃 CPU |
|
||||
| **P4** | `applyJoint()` 每次新建 2 个 `Vector3` + 3 个 `Matrix4`,并线性查 `JO.find` | L32506~L32518 | 每写一个关节角 = 6 次调用 = 30 个临时对象;动画/实时追踪每帧一批 |
|
||||
| **P5** | 几何索引一律 `Uint32Array` | L32531、L32546 | 索引显存 340,178×3×4 B = 4.08 MB(Uint16 只需 2.04 MB) |
|
||||
| **P6** | 点选/ESC 时 `for (const tr of tb.children) tr.style.background = ''` 全表刷 style | L32727、L32736、L32745 | 每次点击写 ~92 行样式 |
|
||||
| **P7** | Worker 的 Blob URL 不回收(`URL.createObjectURL` 后无 `revokeObjectURL`) | L35510 附近 | 常驻一份 ~86 KB Blob;构造失败路径同样泄漏 |
|
||||
|
||||
### 1.4 经实测**否决**的优化(不做,写进报告避免后人重复劳动)
|
||||
|
||||
- **全量矩阵刷新**:0.004 ms/次 → 改"6 级关节链更新"只有 13× 相对提速但绝对量 4 μs,风险不值。
|
||||
- **`updXform()`(92 件)/ `updStat()`(92 件 reduce+filter)**:量级为微秒,爆炸滑块拖动时也远低于 1 ms。
|
||||
- **材质合并 / 几何合并**:只有 92 个 draw call,合并会破坏"零件树勾选显隐 + 点选 + 单独高亮"这些核心功能。
|
||||
- **分片/延迟建场景**:初始化总计约 106 ms(69 解析 + 37 建几何),分片只会增加复杂度,不降低总时间。
|
||||
- **实时追踪时的结果面板 DOM 节流**:`renderResult()` 含"场景图残差复算"与"备选构型"两块**验证语义**内容(用户此前专门建立了指纹/预置自检体系),节流会削弱验证可观测性,保持原样。
|
||||
|
||||
### 1.5 关键发现:源文件与成品已漂移(影响改动范围)
|
||||
|
||||
`src/build_viewer.py` 由 `src/viewer_tpl.html` + `web/ik-glue.js` + `web/arm-ik-solver.js` + `web/mesh_arm.json` 拼装成品,但:
|
||||
|
||||
- 成品 L35023~L35984 的 glue 区域(961 行)**比 `web/ik-glue.js`(776 行)新**:Worker 化求解、`PRECOMPUTED_CHK` 预置自检、`refreshJointLabelsLazy` 节流、`±360°` 滑块等**只存在于成品内**。
|
||||
- 成品主逻辑区与 `src/viewer_tpl.html` 的差异只有:① `±360°` 滑块范围(成品新)② `/*__SCREWS__*/` 占位符展开(预期)。
|
||||
|
||||
**已与用户确认的处理方式**:本次**直接改成品 HTML**;同时把"能对得上"的性能补丁**同步回 `src/viewer_tpl.html`**(源文件落后部分保持原状)。在源文件完成反向同步之前**不要重跑 `build_viewer.py`**(会把成品回退到旧功能)——此警告写入报告。
|
||||
|
||||
---
|
||||
|
||||
## 二、改动清单
|
||||
|
||||
### 改动 0:备份(先做,且必须先验证备份完整)
|
||||
|
||||
1. 新建目录 `backups/`,复制原件为
|
||||
`backups/机器人手臂3D查看器.v1_2026-09-29_2151_优化前.html`
|
||||
(v1 = 优化前版本号;2151 取自原件最后写入时间 2026-09-29 21:51)
|
||||
2. 记录 `SHA-256` 与字节数(`Get-FileHash`),校验备份与原件一致后再动原件。
|
||||
3. `.gitignore` 追加 `backups/`,避免 15 MB 备份污染 git 状态。
|
||||
|
||||
### 改动 1(P1+P2,**核心**):按需渲染
|
||||
|
||||
文件:成品 `loop()`(L33207~L33215)与 `src/viewer_tpl.html` 对应段。
|
||||
|
||||
```js
|
||||
/* 改动前 */
|
||||
function loop() {
|
||||
resize();
|
||||
if (cv.style.display !== 'none') { updCam(); rend.render(scene, cam); }
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
/* 改动后 */
|
||||
/* 按需渲染:needRender 由 requestRender() 置位(所有交互/状态变化都已调用它);
|
||||
旧版置位后无人读取,故恒以 60 fps 空转。装配图页(画布隐藏)不清标志,
|
||||
切回三维页由 tab 处理器 resize()+requestRender() 补渲染。 */
|
||||
function loop() {
|
||||
if (needRender && cv.style.display !== 'none') {
|
||||
needRender = false;
|
||||
resize();
|
||||
updCam();
|
||||
rend.render(scene, cam);
|
||||
}
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
```
|
||||
|
||||
要点:
|
||||
- **保留 `preserveDrawingBuffer:true`**:按需渲染后画布不再重绘,必须靠它保住最后一帧图像。
|
||||
- **保留 rAF 循环**:`animTo()` 动画、实时追踪、拖拽都依赖后续 rAF 里的渲染。
|
||||
- 已核对全部渲染状态变更点均有 `requestRender()`(主逻辑 36 处;glue 的 `refreshTarget()`/`setJoints()`/`highlightSpec()`/`setView()` 内部均调用),无遗漏。
|
||||
- `resize()` 仍在渲染前调用一次(进入渲染路径时同步画布尺寸),窗口尺寸变化由 `ResizeObserver → resize() → requestRender()` 覆盖。
|
||||
|
||||
### 改动 2(P3):拾取零分配 + 悬浮限流
|
||||
|
||||
文件:成品 L32642~L32682 与 `src/viewer_tpl.html` 对应段。
|
||||
|
||||
- `pick()`:`meshes.filter(...)` → 复用模块级数组
|
||||
```js
|
||||
const pickList = [], ndcPick = new THREE.Vector2();
|
||||
function pick(e) {
|
||||
const r = cv.getBoundingClientRect();
|
||||
ndcPick.set(((e.clientX-r.left)/r.width)*2-1, -((e.clientY-r.top)/r.height)*2+1);
|
||||
ray.setFromCamera(ndcPick, cam);
|
||||
pickList.length = 0;
|
||||
for (const m of meshes) if (m.visible) pickList.push(m); // 语义与 filter 完全一致
|
||||
const hit = ray.intersectObjects(pickList, false)[0];
|
||||
return hit ? hit.object : null;
|
||||
}
|
||||
```
|
||||
(three r159 的 Raycaster **不检查 `object.visible`**,已在源码中确认,故可见性过滤必须保留。)
|
||||
- `schedHover()`:rAF 节流 → **60 ms 时间配额 + 位置去重**(实测 1.3 ms/次求交,60 次/s → ~16 次/s):
|
||||
```js
|
||||
let hoverPending = false, lastHoverEv = null, hoverT = -1e9, hoverX = -1, hoverY = -1;
|
||||
const HOVER_MS = 60;
|
||||
function schedHover(e) {
|
||||
if (e.clientX === hoverX && e.clientY === hoverY) return; // 未移动不重算
|
||||
hoverX = e.clientX; hoverY = e.clientY; lastHoverEv = e;
|
||||
if (hoverPending) return;
|
||||
hoverPending = true;
|
||||
setTimeout(() => {
|
||||
hoverPending = false;
|
||||
if (!lastHoverEv || drag) return;
|
||||
hoverT = performance.now(); hover(lastHoverEv);
|
||||
}, Math.max(0, HOVER_MS - (performance.now() - hoverT)));
|
||||
}
|
||||
```
|
||||
尾部仍会补一次(指针停住后取最新坐标),tooltip 行为不变;点击/右键拾取仍**立即**执行,不受限流影响。
|
||||
|
||||
### 改动 3(P4):`applyJoint()` 去分配
|
||||
|
||||
```js
|
||||
const JP = {}, JD = {};
|
||||
for (const j of JO) { JP[j.id] = new V3(...VC(j.rear)); JD[j.id] = new V3(...VC(j.d)).normalize(); }
|
||||
const _mT = new THREE.Matrix4(), _mR = new THREE.Matrix4(), _mTi = new THREE.Matrix4();
|
||||
function applyJoint(id) {
|
||||
const P = JP[id], D = JD[id]; if (!P) return;
|
||||
const a = (ang[id] || 0) * Math.PI / 180;
|
||||
JG[id].matrix.copy(_mT.makeTranslation(P.x, P.y, P.z)
|
||||
.multiply(_mR.makeRotationAxis(D, a))
|
||||
.multiply(_mTi.makeTranslation(-P.x, -P.y, -P.z)));
|
||||
JG[id].matrixWorldNeedsUpdate = true;
|
||||
requestRender();
|
||||
}
|
||||
```
|
||||
数学与原式逐位相同(`T(P)·R(D,θ)·T(−P)`),仅把每次新建的 5 个对象换成预计算 + 3 个共享矩阵。
|
||||
|
||||
### 改动 4(P5):索引降位(Uint16)
|
||||
|
||||
```js
|
||||
const sIdx = p.v.length > 65535 ? new Uint32Array(p.s) : new Uint16Array(p.s); // line 分支
|
||||
const idx = p.v.length > 65535 ? new Uint32Array(p.t.length*3) : new Uint16Array(p.t.length*3);
|
||||
```
|
||||
全部 92 件顶点数 ≤ 47,483(实测),Uint16 合法;索引内存 4.08 MB → 2.04 MB,上传带宽减半。保留 >65535 分支作安全兜底。
|
||||
|
||||
### 改动 5(P6):BOM 高亮行只记一行
|
||||
|
||||
新增 `let bomHlRow = null; function bomHlClear() { if (bomHlRow) { bomHlRow.style.background = ''; bomHlRow = null; } }`,替换 L32727、L32736、L32745 三处全表 `tr.style.background = ''` 循环;设置高亮处同步记录 `bomHlRow = tr`。可见结果完全一致。
|
||||
|
||||
### 改动 6(P7):Worker Blob URL 回收
|
||||
|
||||
```js
|
||||
const u = URL.createObjectURL(new Blob([src + boot], { type: 'application/javascript' }));
|
||||
let w; try { w = new Worker(u); } finally { URL.revokeObjectURL(u); }
|
||||
```
|
||||
(URL 只在 Worker 构造时被读取,构造后即可回收;失败路径也不再泄漏。此改动**仅成品有**——`web/ik-glue.js` 仍是旧同步求解版,无 Worker。)
|
||||
|
||||
### 改动 7:源文件同步(用户已确认)
|
||||
|
||||
- `src/viewer_tpl.html`:逐条应用改动 1~5(该文件对应代码段存在,锚点为唯一代码串;`±360°` 差异行不与改动区重叠)。
|
||||
- `web/ik-glue.js`:核对后逐条应用(预计仅"渲染相关"部分不适用——拾取/循环/几何都在模板里;Worker 相关不存在)。
|
||||
- 同步后**不重跑 `build_viewer.py`**(会回退旧功能),源文件只作为"补丁已留存"。
|
||||
|
||||
### 改动 8:文档
|
||||
|
||||
新建 `docs/查看器性能优化报告.md`(与 `docs/姿态估计算法评估报告.md` 同目录同风格,中文):优化项逐条说明(问题/证据/改法/收益)、前后实测数据表、功能验证清单与结论、已否决项、备份路径 + SHA-256、**"源文件漂移未修,勿重跑构建"**警告。
|
||||
|
||||
---
|
||||
|
||||
## 三、假定与决策
|
||||
|
||||
1. 只改代码区,**不动** `DATA` / `SHEETS` / `TRAVEL` / `COLLIDE` 数据行与求解器脚本(指纹依赖)。
|
||||
2. 不新增/不拆分 `<script>` 标签(`document.scripts[2]` 索引依赖)。
|
||||
3. 不改 `antialias` / `setPixelRatio(min(dpr,1.5))` / 灯光 / 材质参数——属画质取舍,且实测 draw call 与面数不构成瓶颈。
|
||||
4. 测量用的"插桩副本"只放在 `scratch/perf/`(已被 `.gitignore` 忽略),成品文件保持干净。
|
||||
5. 备份放项目内 `backups/`(用户已选),并写入 `.gitignore`。
|
||||
|
||||
---
|
||||
|
||||
## 四、验证方案
|
||||
|
||||
### 4.1 静态校验
|
||||
1. `backups/` 内备份与原件 `SHA-256` 一致(改前)。
|
||||
2. 抽取改后文件的 4 个 `<script>` 块到 `scratch/perf/`,逐个 `node --check` 通过。
|
||||
3. 求解器脚本块 + `TRAVEL` / `COLLIDE` 数据行改前/改后**字节一致**(保证指纹)。
|
||||
4. 全文 diff 只落在预期的 8 处锚点内。
|
||||
|
||||
### 4.2 功能验证(浏览器,改前/改后各跑一遍对照)
|
||||
|
||||
用 `scratch/perf/serve.py`(`SimpleHTTPRequestHandler`,把 `/v.html` 映射到项目根的真实成品)起 `http://localhost:8123/v.html`,用浏览器自动化逐项核对并截图:
|
||||
|
||||
| 项 | 判据 |
|
||||
| --- | --- |
|
||||
| 首屏加载 | 无 JS 报错;三维模型正常显示 |
|
||||
| 关节滑块 | 拖动后手臂实时跟随、角度标签同步 |
|
||||
| 爆炸滑块 / 装配视图按钮 | 模型散开与复位正确 |
|
||||
| 线框 / 电机半透 / 螺钉 / 可达包络 / 复位 | 逐项开关生效 |
|
||||
| 装配图页 ↔ 三维页切换 | SVG 显示、切回后三维画布重新渲染(验证"隐藏分支不清标志") |
|
||||
| 运动模拟页 | 进页即求解并动画到位;IK 表格/徽标/备选构型正常 |
|
||||
| 姿态控制 | 开关、取当前、垂直向下、箭头拖拽可用 |
|
||||
| 自检面板 | 显示"指纹 … 匹配";FK/IK/场景图残差仍为 1e-13 mm 量级(验证改动的数学等价性) |
|
||||
| BOM 螺丝行点击 | 高亮 + 相机对焦 + HUD 文案(昨天的功能) |
|
||||
| 悬浮 tooltip / 右键隐藏 / H 键 / ESC | 行为一致 |
|
||||
| 相机预设 / 拖拽旋转 / 滚轮缩放 | 行为一致 |
|
||||
| URL 参数(`?view=motion&tgt=…&j1=…&hide=…`) | 脚本化视图仍生效 |
|
||||
|
||||
### 4.3 性能前后对比(插桩副本,同一浏览器同一机器)
|
||||
|
||||
生成 `scratch/perf/measure_before.html`(备份 + 尾部追加插桩脚本)与 `measure_after.html`(改后 + 同一插桩脚本),插桩内容:包装 `rend.render` 计次与计 CPU 时长、`rend.info`、`performance.memory`、导航时序、交互探针(滑块 input、`IK.setView('motion')`+`IK.run()` 动画、20 次 pointermove 拾取计时),结果输出到页面固定 div + `document.title` 以便截图/读取。
|
||||
|
||||
预期指标:
|
||||
|
||||
| 指标 | 改前(预期) | 改后(预期) |
|
||||
| --- | --- | --- |
|
||||
| 空闲 5 s 渲染次数 | ≈300 | ≈0~1 |
|
||||
| 单次 render CPU 时长 / draw call / 三角面 | 基线 | 相同(画质不变) |
|
||||
| 20 次 pointermove 拾取总耗时 | ≈26~42 ms | ≈7~11 ms(限流 + 零分配) |
|
||||
| 索引显存 | 4.08 MB | 2.04 MB |
|
||||
| 加载(navigation→首次渲染、DOMContentLoaded→load) | 基线 | 持平或略优 |
|
||||
| 交互后渲染次数(滑块/动画) | — | 每次变化 ≥1 次,无丢帧 |
|
||||
|
||||
### 4.4 回滚
|
||||
`Copy-Item backups/机器人手臂3D查看器.v1_2026-09-29_2151_优化前.html 机器人手臂3D查看器.html`(并同步回源文件的 `git checkout`)。
|
||||
|
||||
---
|
||||
|
||||
## 五、执行顺序
|
||||
|
||||
1. 备份 + 校验 + `.gitignore`(改动 0)
|
||||
2. 起静态服务器 + 跑改前功能对照与改前性能插桩(拿到基线数据)
|
||||
3. 改成品 HTML(改动 1→6,逐处 Edit,锚点唯一串)
|
||||
4. 静态校验(4.1)
|
||||
5. 改后功能对照 + 改后性能插桩(4.2/4.3)
|
||||
6. 同步源文件补丁(改动 7)
|
||||
7. 写报告(改动 8)
|
||||
Reference in New Issue
Block a user