【Babylon提升】重写相机控制器,实现仿地图操作


前言

真正准备用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解析

  1. 声明几个必要字段,记录相机的radius、Alpha、Beta,用于后续使用。
  2. 给场景增加两个事件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))

平移和旋转的逻辑解析:

  1. 鼠标按下,并不松开:开始一次平移或旋转。
  2. 鼠标移动:如果鼠标已经按下并未松开,触发平移或者旋转。
  3. 鼠标松开。结束一次平移或旋转。

然后根据以上事件逻辑,编写出代码就是上面那部分。
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);

请添加图片描述

总结

真正实现起来不难,主要是要知道先分析需求,再找到几个核心方法。

Babylon.js是一个用于创建3D游戏和图形的开源框架,它提供了许多功能强大的工具和功能,以便开发者能够轻松地构建各种3D场景和交互体验。为了方便将Babylon.js中创建的3D场景导出为其他格式,如.glTF、.OBJ等,Babylon.js提供了一个导出器(Exporter)的功能。 Babylon.js导出器允许开发者将他们在Babylon.js中创建的3D模型、纹理、材质以及其他相关资源导出到其他3D开发工具或平台上进行进一步编辑和使用。导出器实质上是一个插件,开发者可以根据自己的需求选择相应的导出器。例如,如果你想将Babylon.js场景导出为.glTF格式,你可以选择glTF导出器。 使用Babylon.js导出器非常简单,只需几行代码即可完成导出操作。首先,你需要引入相应的导出器插件,并将其初始化。然后,你需要指定要导出的场景或模型以及目标文件的路径和格式。最后,调用导出方法执行导出操作,将场景或模型导出到指定的文件中。 通过使用Babylon.js导出器,开发者可以将他们在Babylon.js中创建的3D体验分享给其他人,或者与其他3D开发工具进行集成。导出器的功能丰富且灵活,支持多种3D格式的导出,使得开发者能够更加自由地选择他们喜欢的导出格式。同时,导出器也提供了一些参数和选项,让开发者能够对导出的过程和结果进行一定程度的定制和调整。 总而言之,Babylon.js导出器是一个强大而便利的工具,它使得开发者能够轻松地将Babylon.js中创建的3D场景导出为其他格式,为他们的创作和开发带来了更多可能性和灵活性。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值