Babylon.js内置行为介绍之三:MR 交互三件套——SurfaceMagnetism + Follow + HandConstraint 实战

一、为什么这三个 Behavior 是 MR 的「三位一体」?

Behavior 在 MR 场景里的作用 比喻
SurfaceMagnetismBehavior 把物体「粘」在墙上、桌面、地板 磁铁
FollowBehavior UI 永远漂在你眼前,不挡视线 无人机
HandConstraintBehavior 工具面板长在手掌上,随叫随到 手表

HoloLens 3 和 Quest 3 的 MR 模式里,所有成功应用都在用这三件套:

  • Follow 管「看」------信息总在视野黄金区;

  • HandConstraint 管「调」------按钮在掌心,抬手就操作;

  • SurfaceMagnetism 管「放」------模型往墙上一扔,自动贴平。

三者组合 = 零代码实现「看-调-放」完整闭环。


二、三件套快速画像

TypeScript 复制代码
import {
  SurfaceMagnetismBehavior, FollowBehavior, HandConstraintBehavior,
  WebXRHandTracking, WebXRFeaturesManager
} from '@babylonjs/core';

// 1. 吸附:往地板/墙上一扔,自动对齐
const magnet = new SurfaceMagnetismBehavior();
magnet.target = floorMesh; // 只吸附到 floor
magnet.rotationAlignment = SurfaceMagnetismBehavior.ROTATION_ALIGNMENT.ALIGN_TO_SURFACE_NORMAL; // 法线对齐

// 2. 跟随:UI 面板永远在你眼前 1.5 米
const follow = new FollowBehavior();
follow.defaultDistance = 1.5;
follow.pitchOffset = -10; // 略往下看 10°,不挡脸
follow.maximumVerticalViewOffset = 30; // 低头 30° 内才跟,太高就不跟

// 3. 手掌:工具栏长在左手背上
const hand = new HandConstraintBehavior();
hand.hand = WebXRHandTracking.HANDEDNESS.LEFT;
hand.handConstraint = HandConstraintBehavior.HAND_CONSTRAINT.PALM; // 掌心朝上
hand.handConstraintZone = HandConstraintBehavior.HAND_CONSTRAINT_ZONE.WRIST; // 手腕区

三、实战:一个「MR 家具摆放」完整场景

需求:用户左手抬起来出现「家具菜单」,右手抓取「沙发」模型,往墙上一放,自动贴墙。

3.1 初始化三件套

TypeScript 复制代码
export class MRManager {
  private xr: WebXRDefaultExperience;
  private handTracker: WebXRHand;
  private toolPalette: Mesh; // 左手 UI
  private selectedModel: Mesh | null = null;

  async initAsync(scene: Scene) {
    this.xr = await scene.createDefaultXRExperienceAsync({
      handTracking: { enabled: true } // 必须开手势追踪
    });

    this.handTracker = this.xr.baseExperience.featuresManager.enableFeature(
      WebXRFeatureName.HAND_TRACKING
    ) as WebXRHand;

    // 1. 左手掌 UI
    this.toolPalette = this._createToolPalette();
    const handBhv = new HandConstraintBehavior();
    handBhv.handConstraint = HandConstraintBehavior.HAND_CONSTRAINT.PALM;
    handBhv.handConstraintZone = HandConstraintBehavior.HAND_CONSTRAINT_ZONE.WRIST;
    this.toolPalette.addBehavior(handBhv);

    // 2. 选中模型的跟随(抓取前)
    this.followBhv = new FollowBehavior();
    this.followBhv.defaultDistance = 0.5; // 半米距离,方便对准
    this.followBhv.lerpTime = 0.05; // 平滑系数
  }
}

3.2 右手抓取 → 模型到手 → 关闭吸附

TypeScript 复制代码
private _onRightGrab(mesh: Mesh) {
  // 模型到手,不再跟随相机,改跟右手
  mesh.removeBehavior(this.followBhv);
  
  const sixDoF = new SixDoFDragBehavior();
  sixDoF.allowMultiPointer = true;
  mesh.addBehavior(sixDoF);

  // 监听释放
  sixDoF.onDragEndObservable.add(() => {
    // 释放瞬间:如果靠近墙,就吸附;否则继续跟随
    const wall = this._getNearestWall(mesh);
    if (wall) {
      sixDoF.detach();
      mesh.addBehavior(this._createMagnet(wall)); // 见下节
    } else {
      sixDoF.detach();
      mesh.addBehavior(this.followBhv); // 回到跟随
    }
  });
}

3.3 吸附到墙:最后一次抓放的关键

TypeScript 复制代码
private _createMagnet(targetWall: Mesh): SurfaceMagnetismBehavior {
  const magnet = new SurfaceMagnetismBehavior();
  magnet.target = targetWall;
  magnet.rotationAlignment = SurfaceMagnetismBehavior.ROTATION_ALIGNMENT.ALIGN_TO_SURFACE_NORMAL;
  
  // 高级参数:别贴太近,留 2cm 空隙
  magnet.surfaceDistance = 0.02; 
  magnet.surfaceNormalOffset = 0.01;
  
  // 只响应一次:贴完就自毁,避免误触发
  magnet.onMagnetizeObservable.addOnce(() => {
    magnet.detach(); // 使命完成,自己卸载
  });
  
  return magnet;
}

四、避坑指南:MR 独有的陷阱

4.1 坐标系混乱:掌心的"上"不是世界的"上"

HandConstraintBehaviorhandConstraintZone 五个值:

在掌心坐标系的位置 适合放什么
WRIST 手腕,Z 轴指向手指 工具栏
THUMB 拇指根 返回按钮
INDEX 食指根 射线指示器
MIDDLE 中指根 抓取提示
RING 无名指 状态灯

toolPalette.lookAt(camera.position) 会歪,因为掌心坐标系是局部空间
:用 toolPalette.setParent(handTracker.joints[0]) 再调局部坐标。

4.2 深度冲突:UI 和墙面 Z-fighting

FollowBehaviorfarDistance = 1.5 可能和墙面重叠。
:开深度测试或调 shader:

TypeScript 复制代码
toolPalette.material.depthFunction = BABYLON.Constants.ALWAYS; // UI 永远画在最上

4.3 性能:手势追踪 30fps,渲染 90fps

手势数据是 30fps 刷新,但 XR 要求 90fps。
:所有 Behavior 内部自带 lerpTime,别设成 0,让肉眼平滑过渡。


五、彩蛋:把「三件套」做成「MR 设计系统」

TypeScript 复制代码
// 任何 Mesh 只要加这一行,自动变成「可抓、可贴、可跟随」
mesh.metadata = { mrType: 'grabbable' };

// 场景加载后统一扫描
scene.meshes
  .filter(m => m.metadata?.mrType === 'grabbable')
  .forEach(m => MRManager.makeGrabbable(m));

MRManager.makeGrabbable 内部就是 3.1~3.3 的完整逻辑。

从此设计师导出 GLB 只要在 metadata 里写一行 JSON,程序零改动。


六、一行总结

SurfaceMagnetism(贴)+ Follow(看)+ HandConstraint(控)= MR 交互黄金三角

Quest 3 上实测:从空场景到「左手菜单、右手拖拽、墙面吸附」完整体验,87 行代码

下次再看到 HoloLens 的宣传片,你可以直接说:"这玩意儿,我上午就写完了。"

相关推荐
ttod_qzstudio12 小时前
Babylon.js内置行为介绍之一:用 BoundingBoxBehavior + Gizmo 组合打造「零代码」3D 编辑器
babylon.js·boundingbox·gizmo
ttod_qzstudio12 小时前
把“行为”做成乐高——Babylon.js Behavior 开发套路
生命周期·behavior·babylon.js·内存安全·非空断言
ttod_qzstudio1 天前
从一个隐蔽的 Bug 谈 Babylon.js 对象生命周期管理
babylon.js
ttod_qzstudio1 天前
Babylonjs中手搓OutlineLayer:替代HighlightLayer的高性能轮廓线
babylon.js
ttod_qzstudio5 天前
MirrorReflectionBehaviorEditor 开发心得:Babylon.js 镜面反射的实现与优化
babylon.js·mirrortexture
ttod_qzstudio5 天前
从Unity的C#到Babylon.js的typescript:“函数重载“变成“类型魔法“
typescript·c#·重载·babylon.js
ttod_qzstudio11 天前
Babylon.js TransformNode.clone() 的隐形陷阱:当 null 不等于 null
babylon.js
ttod_qzstudio15 天前
备忘录之Babylon.js 子对象获取方法
babylon.js
ttod_qzstudio22 天前
深入理解 Babylon.js:TransformNode.setParent 与 parent 赋值的核心差异
babylon.js