在Babylonjs中有没有什么设置能让Scene总是更新Scene.pointerX和Scene.pointerY,而不是说只在Scene.onPointerObservable发生时才更新?
没有。
这个可以有吧?
这个真没有!
1. 需求背景
很多 3D 在线编辑器或展厅都支持:
“用户从资源面板 / 操作系统文件夹,把 glTF/GLB/FBX 拖到画布中央 → 立即在对应位置出现模型”。
看似简单,但真动手才发现:
-
浏览器 Drop 事件 ≠ Babylon.js 的 POINTERUP
-
scene.pointerX / pointerY 经常“跑飞”
-
不同浏览器事件顺序还不一样
本文把踩过的坑一次性总结,给出可复制的代码模板。
2. 事件链条 101
表格
复制
阶段 | 由谁产生 | 典型回调 | 坐标来源 |
---|---|---|---|
dragstart | 源元素(可拖到外面的 dom) | — | — |
dragover | 画布(canvas) | canvas.ondragover | DragEvent.clientX |
drop | 画布(canvas) | canvas.ondrop | DragEvent.clientX |
pointermove | 浏览器 → Babylon.js | scene.onPointerObservable(POINTERMOVE) | scene.pointerX |
pointerup | 浏览器 → Babylon.js | scene.onPointerObservable(POINTERUP) | scene.pointerX |
关键区别
Drop 事件属于 HTML Drag & Drop API
POINTER 事件属于 Pointer Events*
两条管线互不保证顺序,Babylon 也不会在 Drop 时帮你更新 scene.pointerX。
3. 第一大坑:scene.pointerX 在 Drop 里读出来是 0
3.1 为什么
Babylon 只在 pointermove / pointerdown / pointerup
里写 scene.pointerX/Y
。
drop
发生时,浏览器不会派发新的 pointer 事件,于是:
canvas.addEventListener('drop', () => {
console.log(scene.pointerX, scene.pointerY); // 0, 0 或旧值
});
3.2 正确姿势
用 Drop 事件自己算坐标,再 scene.pick
:
const canvas = engine.getRenderingCanvas();
const rect = canvas.getBoundingClientRect();
canvas.addEventListener('dragover', e => e.preventDefault()); // 必须阻止默认行为
canvas.addEventListener('drop', async (ev: DragEvent) => {
ev.preventDefault();
// 1. 先算画布坐标
const x = ev.clientX - rect.left;
const y = ev.clientY - rect.top;
// 2. 再做射线检测
const pick = scene.pick(x, y);
const insertPoint = pick.hit ? pick.pickedPoint : null;
// 3. 读文件
const file = ev.dataTransfer.files[0];
const url = URL.createObjectURL(file);
const loaded = await SceneLoader.LoadAssetContainerAsync('', url, scene);
loaded.addAllToScene();
if (insertPoint) {
loaded.meshes[0].position.copyFrom(insertPoint);
}
});
4. 第二大坑:等 POINTERUP 再加载?
有些产品希望“鼠标真正松开”才放置,于是写:
let pendingDrop: { x: number; y: number; file: File } | null = null;
canvas.addEventListener('drop', ev => {
pendingDrop = {
x: ev.clientX - rect.left,
y: ev.clientY - rect.top,
file: ev.dataTransfer.files[0]
};
});
scene.onPointerObservable.add((p, type) => {
if (type === PointerEventTypes.POINTERUP && pendingDrop) {
loadModel(pendingDrop);
pendingDrop = null;
}
});
问题
若拖拽起点在画布外,Babylon 不会收到 POINTERUP,模型永远不被加载。
兜底方案
同时监听 window
的 pointerup
,或干脆在 drop
里直接加载,再加一个撤销/重做系统。
5. 第三大坑:事件顺序差异
Chrome/Edge:
dragover → drop → pointermove → pointerup
Firefox:
dragover → pointerup → drop
Safari:
dragover → drop → pointerup
(有时 pointerup 根本不触发)
结论:不要依赖固定顺序,始终用 drop
里的坐标。
6. 完整可复用片段(TypeScript)
export const enableDragDropModel = (scene: Scene) => {
const canvas = scene.getEngine().getRenderingCanvas();
const rect = canvas.getBoundingClientRect();
canvas.addEventListener('dragover', e => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
canvas.addEventListener('drop', async (ev: DragEvent) => {
ev.preventDefault();
const file = ev.dataTransfer.files[0];
if (!file) return;
// 计算画布坐标
const x = ev.clientX - rect.left;
const y = ev.clientY - rect.top;
// 射线检测
const pick = scene.pick(x, y);
const point = pick.hit ? pick.pickedPoint : undefined;
// 加载
const url = URL.createObjectURL(file);
const asset = await SceneLoader.LoadAssetContainerAsync('', url, scene);
asset.addAllToScene();
const root = asset.meshes[0];
if (point) root.position.copyFrom(point);
root.name = file.name;
// 清理
URL.revokeObjectURL(url);
});
};
7. 经验清单
表格
复制
经验 | 说明 |
---|---|
✅ 永远用 ev.clientX - rect.left 而不是 scene.pointerX | 避免 0 或旧值 |
✅ 在 dragover 里 preventDefault | 否则浏览器直接禁止放置 |
✅ 同时监听 drop 和 window.pointerup | 防止画布外拖拽导致 POINTERUP 丢失 |
✅ 文件加载完立即 URL.revokeObjectURL | 释放内存 |
✅ 拖放光标给提示 | e.dataTransfer.dropEffect = 'copy' |
8. 结语
浏览器 Drag-and-Drop 与 Babylon.js 的指针事件是两条平行管线。
只要记住“坐标自己算、文件自己读、事件顺序别硬猜”,就能稳稳地把模型拖进 3D 世界。
Happy dropping!