Files
Robot_Arm/.trae/documents/查看器3D性能优化方案.md
T

258 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 机器人手臂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)