实现仿地图操作
前言
真正准备用Babylon搭场景后才发现,这操作有点怪。与常规的地图操作还是有点区别的,虽然不大,但却非常不舒服。
Babylon沙盒
地图操作
一、需求分析
1.1 旋转
效果基本一样,Babylon默认使用左键触发,常规地图用右键,这个应该简单。
1.2 缩放
效果基本一样,都是滚轮触发。
1.3 拖拽(平移)
这里有很细微的不同,也正是这点不同,让我操作起来感觉别扭,接着深度分析两者区别。
地图:点击地图上某一点,鼠标不松进行移动,鼠标移动多少,地图平移多少!可以看到从开始移动到结束,鼠标始终指在同一个位置!
Babylon:点击空间任意点,鼠标不松进行移动,鼠标移动多少和空间移动多少好像有一个特别的关系!这样的操作方式特别难准确拖拽内容。特别是越靠近时,拖拽幅度很大;很远离时,拖拽幅度又很小。
备注:看代码后知道,默认的拖拽是可以设置偏移值的,但是固定值,所以不能简单通过设置合理值来解决。
二、简单介绍ArcRotateCamera相机
2.1 hello world
// 创建相机的配置项:name, alpha, beta, radius, target position, scene
const camera = new BABYLON.ArcRotateCamera("Camera", 0, 0, 10, new BABYLON.Vector3(0, 0, 0), scene);
// 通过设置相机位置来覆盖 alpha, beta, radius
camera.setPosition(new BABYLON.Vector3(0, 0, 20));
// 将相机绑定到画布上面
camera.attachControl(canvas, true);
2.2 属性介绍
三、重写ICameraInput
说明:这块内容主要介绍重写的逻辑,稍微有点难度,不适应可以直接跳过看第四段,怎么使用。至于怎么想到要重新这个类能满足效果,是通过参考很多相似案例得出结论的。
3.1 完整代码
// @ts-nocheck
/* eslint-disable */
// import * as BABYLON from 'babylonjs'
/**
* 仿地图操作,拖拽、旋转。(不包括缩放)
*/
const isIPointerEvent = (event: BABYLON.IEvent): event is BABYLON.IPointerEvent => 'pointerId' in event
export class CustomPointersInput implements BABYLON.ICameraInput<BABYLON.ArcRotateCamera> {
camera: BABYLON.ArcRotateCamera;
_scene: BABYLON.Nullable<BABYLON.Scene> = null;
_pointerObserver: BABYLON.Nullable<BABYLON.Observer<BABYLON.PointerInfo>> = null;
_beforeRenderObserver: BABYLON.Nullable<BABYLON.Observer<BABYLON.Scene>> = null;
_pickedPoints: Map<number, BABYLON.Vector3> = new Map();
_pickedDoublePoints: Map<number, BABYLON.Vector3> = new Map();
_targetTarget: BABYLON.Vector3 = BABYLON.Vector3.Zero();
_targetRadius = 0;
_targetAlpha = 0;
_targetBeta = 0;
// 因版本升级,在出现精灵是,down事件会出问题,所以使用该标志位,替代down事件记录point,改到move事件记录。
firstDown = false;
_debugSpheres: Map<string, BABYLON.Mesh> = new Map();
getClassName (): 'CustomPointersInput' { return 'CustomPointersInput' }
getSimpleName (): 'pointers' { return 'pointers' }
constructor () {
}
// 绑定
attachControl (noPreventDefault?: boolean, defaultTarget?: BABYLON.Vector3, radius?: number): void {
if (this._scene) this.detachControl()
if (defaultTarget) {
this._targetTarget = defaultTarget
}
if (radius) {
this.camera.radius = radius
}
noPreventDefault = BABYLON.Tools.BackCompatCameraNoPreventDefault(arguments)
this.camera.mapPanning = true
this._scene = this.camera.getScene()
this._targetRadius = this.camera.radius
this._targetAlpha = this.camera.alpha
this._targetBeta = this.camera.beta
// 鼠标事件绑定
this._pointerObserver = this._scene.onPointerObservable.add(
(p: BABYLON.PointerInfo, s: BABYLON.EventState) => {
if (!noPreventDefault) {
p.event.preventDefault()
}
// 重点重构
this._handlePointers(p, noPreventDefault)
},
BABYLON.PointerEventTypes.POINTERDOWN |
BABYLON.PointerEventTypes.POINTERUP |
BABYLON.PointerEventTypes.POINTERMOVE
)
// 场景渲染前事件绑定
this._beforeRenderObserver = this._scene.onBeforeRenderObservable.add(() => {
this.camera._target = this._targetTarget
this.camera.alpha = this._targetAlpha
this.camera.beta = this._targetBeta
})
}
// 注销方法
detachControl (): void {
if (this._pointerObserver) {
this._scene.onPointerObservable.remove(this._pointerObserver)
}
if (this._beforeRenderObserver) {
this._scene.onBeforeRenderObservable.remove(this._beforeRenderObserver)
}
this._scene = null
this._pointerObserver = null
this._beforeRenderObserver = null
this._pickedPoints = new Map()
}
checkInputs(): void {}
_handlePointers ({ type, event, pickInfo: { ray } }: BABYLON.PointerInfo, noPreventDefault?: boolean /* , s: BABYLON.EventState */) {
if (!isIPointerEvent(event)) {
return
}
const { buttons, pointerId } = event
// 第一次按下, 需要触发MOVE事件记录this._pickedPoints
switch (type) {
case BABYLON.PointerEventTypes.POINTERDOWN:
this.firstDown = true
this._targetTarget = this.camera.target.clone()
this._targetAlpha = this.camera.alpha
this._targetBeta = this.camera.beta
if (!noPreventDefault) {
event.preventDefault()
}
break
case BABYLON.PointerEventTypes.POINTERUP:
this.firstDown = false
this._pickedPoints.delete(pointerId)
this._pickedDoublePoints.delete(pointerId)
if (!noPreventDefault) {
event.preventDefault()
}
break
case BABYLON.PointerEventTypes.POINTERMOVE:
if ( this.firstDown ) {
// console.log('存储一次位置')
const point = ray.intersectsAxis('y')
this._pickedPoints.set(pointerId, point)
console.log('存储:',point._x)
this.firstDown = false
}
if (!noPreventDefault) {
event.preventDefault()
}
if (this._pickedPoints.size === 0) {
return
}
if (this._pickedPoints.size === 1) {
// get the previous picked point from the "store"
const prevPickedPoint = this._pickedPoints.get(pointerId)
// get the current picked point, we'll store it later
const pickedPoint = ray.intersectsAxis('y')
// 1、左键; 2、右键
if (buttons === 1) {
// we'll move the camera's target to this point before the next render
if (pickedPoint && prevPickedPoint) {
this._targetTarget.addInPlace(prevPickedPoint.subtract(pickedPoint))
}
return
}
if (buttons === 2) {
const { lowerAlphaLimit, upperAlphaLimit, lowerBetaLimit, upperBetaLimit, angularSensibilityX, angularSensibilityY } = this.camera
const offsetX = event.movementX || event.mozMovementX || event.webkitMovementX || event.msMovementX || 0
const offsetY = event.movementY || event.mozMovementY || event.webkitMovementY || event.msMovementY || 0
this._targetAlpha -= offsetX / 500
this._targetBeta -= offsetY / 500
if (lowerAlphaLimit && this._targetAlpha < lowerAlphaLimit) {
this._targetAlpha = lowerAlphaLimit
}
if (upperAlphaLimit && this._targetAlpha > upperAlphaLimit) {
this._targetAlpha = upperAlphaLimit
}
if (lowerBetaLimit && this._targetBeta < lowerBetaLimit) {
this._targetBeta = lowerBetaLimit
}
if (upperBetaLimit && this._targetBeta > upperBetaLimit) {
this._targetBeta = upperBetaLimit
}
return
}
return
}
break
default:
// throw new Error(`Unexpected pointer event type ${type}`)
}
}
}
3.2 结构解析
3.2.1 ICameraInput 重构接口
- camera: // 属性
- getClassName(): string; // 随便写个命名
- getSimpleName(): string; // 随便写个命名
- attachControl(noPreventDefault?: boolean): void; // 核心方法,在相机执行attachControl方法中,会循环调用input的attachControl方法。可以理解为绑定功能。
- detachControl(): void; // 注销,相机注销时会调研到这个方法。
- checkInputs?: () => void; // 暂时没用到,看说明是性能优化
3.2.2 attachControl解析
- 声明几个必要字段,记录相机的radius、Alpha、Beta,用于后续使用。
- 给场景增加两个事件onPointerObservable和onBeforeRenderObservable。
onPointerObservable:鼠标触发事件,对鼠标的POINTERDOWN、POINTERUP、POINTERMOVE事件进行监听。核心方法。
onBeforeRenderObservable:在场景每次渲染前,触发该监听事件。对radius、Alpha、Beta进行赋值,保障内部记录的属性严格控制相机这个三个属性。
3.2.3 onPointerObservable事件逻辑
两个关键方法:
// 1、获取鼠标发射的射线与y平面的交点point。 大部分场景中y平面刚好是地图平铺的面。
const point = ray.intersectsAxis('y')
// 2、subtract是计算两个相位的差值。 addInPlace是增加制定的相位值。 所以这个方法是_targetTarget增加prevPickedPoint和pickedPoint的差值。
// 类似 _targetTarget = _targetTarget + (prevPickedPoint - pickedPoint)
this._targetTarget.addInPlace(prevPickedPoint.subtract(pickedPoint))
平移和旋转的逻辑解析:
- 鼠标按下,并不松开:开始一次平移或旋转。
- 鼠标移动:如果鼠标已经按下并未松开,触发平移或者旋转。
- 鼠标松开。结束一次平移或旋转。
然后根据以上事件逻辑,编写出代码就是上面那部分。
POINTERDOWN事件中记录一个标志位firstDown和初始点prevPickedPoint;
POINTERMOVE事件中根据标志位firstDown判断是否触发平移或旋转;根据左右键按钮buttons值判断是平移或旋转。
POINTERUP事件中清理标志位和初始点。
四、如何使用重构类
// 3.![请添加图片描述](https://img-blog.csdnimg.cn/df986ebdf2b54f3cba938c4a4d0738bb.gif)
1完整代码
import { CustomPointersInput } from '../../../functions/common/CustomPointersInput'
// 创建相机的配置项:name, alpha, beta, radius, target position, scene
const camera = new BABYLON.ArcRotateCamera("Camera", 0, 0, 10, new BABYLON.Vector3(0, 0, 0), scene);
// 移除原来input
camera.inputs.remove(camera.inputs.attached.pointers)
this.cameraControl = new CustomPointersInput()
camera.inputs.add(this.cameraControl)
// 将相机绑定到画布上面
camera.attachControl(canvas, true);
总结
真正实现起来不难,主要是要知道先分析需求,再找到几个核心方法。