4.2 拾取与交互:射线进入流水线


4.2 拾取与交互:射线进入流水线

本节摘要:拾取把二维的鼠标坐标翻译成三维世界的命中判断:从相机发射一条穿过点击点的射线,与场景网格求交。本节走完这条链路,实现点击高亮与平面拖拽两个高频交互,并给出拾取的帧成本与节流策略。承接 2.3 的包围盒(求交前的粗筛),通向 4.3 的物理射线与第6章的游戏交互。

一、一次点击的旅程

用户在屏幕上点了一下,浏览器给出的是画布坐标系的两个整数。三维世界怎么知道"点的是谁"?链路分四步:坐标换算成裁剪空间的一对小数;从相机位置发射一条穿过该点的射线;射线与场景里的网格求交——先用包围盒粗筛(2.3 节的老朋友),再逐三角形精算;取距离最近的命中者,连同命中点、命中面法线一起返回。

图 4-2 拾取射线:从鼠标坐标到三维命中

图 4-2 拾取射线:从鼠标坐标到三维命中

引擎把这四步封装成一次调用,事件层面还提供了动作管理器这个声明式捷径:

// 写法一:手动拾取——适合一次点击影响多个对象或需要自定义逻辑 scene.onPointerObservable.add((pi) => { if (pi.type !== BABYLON.PointerEventTypes.POINTERPICK) return; const pick = scene.pick(scene.pointerX, scene.pointerY); if (pick.hit) { console.log("命中", pick.pickedMesh.name, "坐标", pick.pickedPoint); } }); // 写法二:声明式动作——单一对象的单次点击用这个更省 box.actionManager = new BABYLON.ActionManager(scene); box.actionManager.registerAction( new BABYLON.ExecuteCodeAction(BABYLON.ActionManager.OnPickTrigger, () => { console.log("方块被点了"); }) );

两种写法的选择标准:动作管理器省代码但绑定在单个网格上;手动拾取一次拿到全场结果,做多选、框选时只有它顶用。

二、两个高频交互的实现

案例一:点击高亮。背景:展厅里几十件展品,点击谁谁发光。操作:拾取命中后切换高亮层——引擎的高亮渲染把选中物描上一层辉光,比换材质轻:

const hl = new BABYLON.HighlightLayer("hl", scene); scene.onPointerObservable.add((pi) => { if (pi.type !== BABYLON.PointerEventTypes.POINTERPICK) return; const pick = scene.pick(scene.pointerX, scene.pointerY); if (pick.hit && pick.pickedMesh.metadata?.isExhibit) { hl.removeAllMeshes(); // 先清旧高亮 hl.addMesh(pick.pickedMesh, BABYLON.Color3.Yellow()); } else { hl.removeAllMeshes(); } });

结果:点击任意展品黄边点亮,再点空白处熄灭。解读:网格的 metadata 字段是给对象挂业务数据的正规位置,判断"是不是展品"不该靠名字字符串硬解析。变式:悬停高亮把事件换成 POINTERMOVE 并加节流,见本节末尾。

案例二:平面拖拽。背景:要把家具拖到地面任意位置。操作:拾取射线改与"地面平面"求交,命中点即目标位置——这是拖拽的核心技巧,射线打到哪儿,物体跟到哪儿:

// 拖拽三板斧:按下记录、移动跟随、抬起释放 let dragging = null; scene.onPointerObservable.add((pi) => { const pick = scene.pick(scene.pointerX, scene.pointerY); if (pi.type === BABYLON.PointerEventTypes.POINTERDOWN && pick.hit) { dragging = pick.pickedMesh; // 记住抓的是谁 } if (pi.type === BABYLON.PointerEventTypes.POINTERMOVE && dragging) { // 不与网格求交,改与 y 等于 0 的水平面求交:拖拽永远贴地 const ray = scene.createPickingRay(scene.pointerX, scene.pointerY); const hit = ray.intersectsPlane(BABYLON.Plane.FromPositionAndNormal( BABYLON.Vector3.Zero(), BABYLON.Axis.Y)); if (hit) dragging.position.copyFrom(hit); } if (pi.type === BABYLON.PointerEventTypes.POINTERUP) dragging = null; });

结果:按住家具在地面滑行,松手停住。解读:拖拽的精髓是"换求交对象"——与网格求交是"点选",与平面求交是"移动"。变式:垂直墙面挂画就换成法线朝外的墙面平面,思路完全同构。

⚠️ 常见坑:移动端与触控板上 POINTERMOVE 每秒触发上百次,逐次拾取等于每秒上百次全场求交。节流是必需品——两次拾取间隔小于 30 毫秒就复用上次结果,或只在命中网格变化时才执行后续逻辑。

三、成本账与纪律

拾取是"事件驱动"的,不点不花;但悬停类交互把它变成了每帧开销。全场求交的成本近似与网格数成正比,粗筛靠包围盒但架不住上千实例。三条纪律:能关的网格关掉拾取开关(isPickable 设否,纯装饰物全部关掉);悬停节流;框选类批量操作用包围盒预筛,只对候选精算。

本节要点回顾

  • 拾取链路四步:坐标换算、发射射线、包围盒粗筛加逐三角形精算、取最近命中
  • 结果四字段:命中与否、网格、坐标、法线,各服务一类交互
  • 手动拾取与声明式动作分工:全场逻辑用前者,单对象点击用后者
  • 拖拽换求交对象:与平面求交实现贴地移动,与网格求交实现点选
  • 节流与关拾取开关:悬停交互的两条成本红线

点击的世界打通了,但点击推不倒积木。下一节接上物理引擎,让碰撞、重力与弹跳接管"动画做不了的那部分真实"。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U