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

15 KiB
Raw Blame History

机器人手臂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) 再全量射线求交 L32668L32673、L32642L32652 每帧≥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 对应段。

/* 改动前 */
function loop() {
  resize();
  if (cv.style.display !== 'none') { updCam(); rend.render(scene, cam); }
  requestAnimationFrame(loop);
}
/* 改动后 */
/* 按需渲染: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(...) → 复用模块级数组
    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):
    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() 去分配

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)

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 回收

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)