Visual LabINTERACTIVE LEARNING
OpenGL 与 3D第 2 / 4 章

控制器骨架与公开接口

组织配置、状态、公开方法、更新循环和资源释放。

OrbitControls相机交互
专题预计 52 分钟查看源文 ↗

controller

import { EventDispatcher, MOUSE, Spherical, Vector2, Vector3,Matrix3,Matrix4 } from 'three'
import {  lookAtMat4v } from './exokit-math.js'
// OrbitControls performs orbiting, dollying (zooming), and panning.
// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).

const _changeEvent = { type: 'change' }
const _startEvent = { type: 'start' }
const _endEvent = { type: 'end' }
const EPSILON = 0.000001
const maxElapsed = 1 / 20
const minElapsed = 1 / 60
const intersectPoint = new Vector3()
const tempVec3 = new Vector3()
const tempVec3b = new Vector3()
const tempMatrix3 = new Matrix3()
const tempDiff = new Vector3()
const tempMatrix4 = new Matrix4()

/**
 * 轨道控制器 封装优化思路
 * 1. 抽象出相机坐标系下的操作,
 * 1.1如pan,dolly,rotate,细分出panLeft,panUp,dollyIn,dollyOut,rotateLeft,rotateUp
 * 1.2 实现触摸、鼠标、快捷键分别控制细分操作
*
 */
class OrbitControls extends EventDispatcher {
  constructor(object, domElement,three) {
    super()

    this.object = object
    this.domElement = domElement
    this.domElement.style.touchAction = 'none' // disable touch scroll

    this.three = three

    // Set to false to disable this control
    this.enabled = true

    // "target" sets the location of focus, where the object orbits around
    this.target = new Vector3()

    // Sets the 3D cursor (similar to Blender), from which the maxTargetRadius takes effect
    this.cursor = new Vector3()

    // How far you can dolly in and out ( PerspectiveCamera only )
    this.minDistance = 0
    this.maxDistance = Infinity

    // This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
    // Set to false to disable zooming
    this.enableZoom = true
    this.zoomSpeed = 1.0
    this.keyDollySpeed = 2.0
    this.shiftSpeed = 4.0
    // Set to false to disable rotating
    this.enableRotate = true
    this.rotateSpeed = 1.0

    // Set to false to disable panning
    this.enablePan = true
    this.panSpeed = 1
    this.keyPanSpeed = 7.0 // pixels moved per arrow key push

    // Set to true to automatically rotate around the target
    // If auto-rotate is enabled, you must call controls.update() in your animation loop
    this.autoRotate = false
    this.autoRotateSpeed = 2.0 // 30 seconds per orbit when fps is 60

    // The four arrow keys 改为数组形式
    this.keys = { LEFT: ['ArrowLeft'], UP: ['ArrowUp'], RIGHT: ['ArrowRight'], BOTTOM: ['ArrowDown'], ZOOM_IN: ['ArrowUp'], ZOOM_OUT: ['ArrowDown'] }

    // Mouse buttons
    this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }

    // for reset
    this.target0 = this.target.clone()
    this.position0 = this.object.position.clone()
    this.zoom0 = this.object.zoom

    // the target DOM element for key events
    this._domElementKeyEvents = null

    this.pivot = new Vector3()

    //
    // public methods
    //
    this.getPolarAngle = function () {
      return spherical.phi
    }

    this.getAzimuthalAngle = function () {
      return spherical.theta
    }

    this.getDistance = function () {
      return this.object.position.distanceTo(this.target)
    }

    this.listenToKeyEvents = function (domElement) {
      domElement.addEventListener('keydown', onKeyDown)
      this._domElementKeyEvents = domElement
    }

    this.stopListenToKeyEvents = function () {
      this._domElementKeyEvents.removeEventListener('keydown', onKeyDown)
      this._domElementKeyEvents = null
    }

    this.saveState = function () {
      scope.target0.copy(scope.target)
      scope.position0.copy(scope.object.position)
      scope.zoom0 = scope.object.zoom
    }

    this.reset = function () {
      scope.target.copy(scope.target0)
      scope.object.position.copy(scope.position0)
      scope.object.zoom = scope.zoom0

      scope.object.updateProjectionMatrix()
      scope.dispatchEvent(_changeEvent)

      scope.update()

      state = STATE.NONE
    }

    // this method is exposed, but perhaps it would be better if we can make it private...
    this.update =  function update(deltaTime = null) {

        if (scope.autoRotate && state === STATE.NONE) {
          rotateLeft(getAutoRotationAngle(deltaTime))
        }
        scope.object.lookAt(scope.target)
      }

    this.dispose = function () {
      scope.domElement.removeEventListener('contextmenu', onContextMenu)

      scope.domElement.removeEventListener('pointerdown', onPointerDown)
      scope.domElement.removeEventListener('pointercancel', onPointerUp)
      scope.domElement.removeEventListener('wheel', onMouseWheel)

      scope.domElement.removeEventListener('pointermove', onPointerMove)
      scope.domElement.removeEventListener('pointerup', onPointerUp)

      scope.domElement.removeEventListener('mousemove', handleMouseMove)

      const document = scope.domElement.getRootNode() // offscreen canvas compatibility

      document.removeEventListener('keydown', interceptControlDown, { capture: true })

      if (scope._domElementKeyEvents !== null) {
        scope._domElementKeyEvents.removeEventListener('keydown', onKeyDown)
        scope._domElementKeyEvents = null
      }

      //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
    }

    //
    // internals
    //

    const scope = this

    const STATE = {
      NONE: -1,
      ROTATE: 0,
      DOLLY: 1,
      PAN: 2,
      TOUCH_ROTATE: 3,
      TOUCH_PAN: 4,
      TOUCH_DOLLY_PAN: 5,
      TOUCH_DOLLY_ROTATE: 6,
    }

    let state = STATE.NONE

    //panning
    const panOffset = new Vector3()
    const panStart = new Vector2()
    const panEnd = new Vector2()
    const panDelta = new Vector3()
    const panTargrt = new Vector3()

    //dollying
    let scale = 1
    const dollyStart = new Vector2()
    const dollyEnd = new Vector2()
    const dollyDelta = new Vector2()

    const pointers = []
    const pointerPositions = {}

    //rotating
    const cameraOffset = new Vector3() // 相机偏移量
    const rotateStart = new Vector2()
    const rotateEnd = new Vector2()
    const rotateDelta = new Vector2()
    const spherical = new Spherical()

    let secsNowLast = null;
    let dollyDelta1 = 0
    let dollyDistFactor = 1.0
    const mouseWheelDollyRate = 100
    const dollyProximityThreshold = 30.0
    const dollyMinSpeed = 0.04
    const dollyMaxSpeed = 20
    const dollyState = {
      isPass:false,
      changePos:true
    }

    let controlActive = false