Three.js 如何实现悬停高亮与提示框
上一篇讲拖动关节时屏幕上那条辅助线。这一篇讲鼠标还没按下时的两件事:亮起来的那个零件,和跟着指针走的那个小框。
命中检测在拖动关节那篇里讲过 —— pointermove 发一条射线,从命中的 Mesh 往上找出 link 和 joint。这一篇从命中之后开始。
难点不在显示,在信息怎么从三维场景里出去:命中的结果在控制器手里,提示框却是画布外面一个普通的 DOM 节点。
1. 高亮:换一份自发光颜色
1 | const material = mesh.material as Material |
悬停时把这个 link 下所有 Mesh 的材质换成一个加了自发光的克隆。
不直接改原材质,是因为材质可能被多个 link 共用 —— 直接加 emissive,别的零件会跟着一起亮。克隆只作用在当前 link 上,原材质留在 userData.material 里备用。
移出时要成对收尾:把原材质换回去,再 dispose() 掉克隆的那份。不释放的话每悬停一次漏一份材质,来回扫几百次就是几百份。
emissiveIntensity 给 0.3 是让零件”亮起来”而不是”发光”。实际亮度还受原贴图影响,深色模型上要调高才看得出来。
2. 用一个事件把结果送出去
控制器的做法是自己派一个事件:
1 | // 触发鼠标在机器人上移动的事件 |
要求这个类继承 Three.js 的 EventDispatcher。UI 侧订阅:
1 | urdfDragControls.addEventListener("pointerMoveOnRobot", ({ x, y, joint, link }) => { |
用事件而不是回调,是为了两边互不认识:控制器不知道谁在听、有几个在听;组件挂载时订阅、卸载时取消,不用管控制器的生命周期。进入和离开也分成两个事件,控制器就不用维护”上一条说在悬停、这一条要不要翻过来”的状态。
两个容易踩的点:
- 传的是
link和joint对象,不是名字。控制器不替 UI 决定显示什么 —— 要名字取.name,要当前值取.userData.value。 - 这是 Three.js 的
EventDispatcher,不是 DOM 事件:不冒泡、不经过 DOM 树,type之外的字段直接挂在事件对象上。照CustomEvent的习惯读e.detail.link会拿到undefined。
3. 提示框:跟着鼠标,用 display 开关
跟随就是把事件的 x、y 写进 left / top。这要求定位参照物就是画布容器:
1 | <div id="canvasWrapper" ref="canvasWrapperRef"> |
#robotTip 得是 position: absolute,#canvasWrapper 得是 position: relative,否则 left / top 参照更外层的定位祖先,鼠标一动框就跑偏。提示框也必须在 canvas 外面 —— 它是 DOM,画布里的文字没法选中也没法跟系统字体。
开关用 display: block / none 而不是 v-if:v-if 为假时节点不存在,没有 left / top 可写,得先渲染出来、下一帧才能定位,中间会有一帧框停在左上角。display 的节点一直在(none 不参与布局),位置可以先写好再显示,出现的第一帧就在正确位置。
if (joint) 那个分支不是防御性写法,是真会走到:findNearestJoint 跳过 fixed 关节往上找第一个能动的,如果指针停在的 link 到根节点之间全是固定关节(基座、底盘这类没有自由度的部分),找一圈也没有可动的,joint 就是空。
但鼠标确实指在了一个零件上,高亮该亮。所以悬停命中和”悬停到可拖的关节上”是两件事 —— 没有 joint 就显示 null,而不是把框藏起来,藏起来用户会以为没扫到东西。link.name 则无条件取,link 一定有。
4. 三套状态:谁在拖,谁才有权限
画布上挂着三套交互:悬停检测、关节拖拽(urdfDragControls)、相机轨道控制(orbitControls)。后两个都响应”按下并拖动”,得约定一次只生效一个 —— 悬停在关节上时,按下应该是拖关节,不是转相机。
1 | urdfDragControls.addEventListener('finishDrag', () => { |
相机开始时,用户要转相机,关节拖拽让路;但前提是指针没停在关节上 —— curHover 为真说明指针正指着可拖的关节,这时该拖的是关节。这行用 && 短路当 if 用,展开是 if (!curHover) enabled = false。相机结束和关节拖拽结束时,各自无条件把对方放回来。
没有统一的调度器,就是谁开始谁按情况关掉对方、谁结束谁把对方放回来。容易出问题的是每一对”关”都得配一个”开” —— 少一处那个功能就永久失效,而且只在特定操作顺序下复现,比如拖过一次关节再想转相机。
5. 数值固定四位小数
1 | tipMsgValue.jointValue = joint.userData.value.toFixed(4) |
挡两件事。一是浮点噪声:关节角是旋转量累加出来的,一连串三角运算之后,本该是 0.3 的值可能是 0.30000000000000004。二是框会抖:框宽由内容撑开、每帧都可能更新,位数一变宽度就变,右边缘跟着跳;固定小数位,宽度就稳了。
四位是精度和宽度的折中。弧度制下约 0.0001 弧度,换算到末端是亚毫米级,对实时跟随鼠标的提示框足够了。
参考
- Three.js
EventDispatcher:https://threejs.org/docs/#api/en/core/EventDispatcher - Three.js
Raycaster:https://threejs.org/docs/#api/en/core/Raycaster - 高亮的另一种做法(OutlinePass 轮廓线):https://wywzixin.github.io/threejs/outline-pass/
- 上一篇:Three.js 如何生成关节的运动轨迹 —— https://wywzixin.github.io/threejs/urdf-joint-drag-path/
- 下一篇:Three.js 如何显示坐标系、碰撞体和质心 —— https://wywzixin.github.io/threejs/urdf-helpers/
Three.js 如何实现悬停高亮与提示框