OpenGL 与 3D第 2 / 4 章
控制器骨架与公开接口
组织配置、状态、公开方法、更新循环和资源释放。
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