threepipe
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

FirstPersonControls2.ts 9.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. import {EventDispatcher, MathUtils, Object3D, Spherical, Vector3} from 'three'
  2. import {IEvent, now, serialize} from 'ts-browser-helpers'
  3. import {uiInput, uiPanelContainer, uiToggle} from 'uiconfig.js'
  4. import {ICameraControls} from '../../core'
  5. // eslint-disable-next-line @typescript-eslint/naming-convention
  6. const _lookDirection = new Vector3()
  7. // eslint-disable-next-line @typescript-eslint/naming-convention
  8. const _spherical = new Spherical()
  9. // eslint-disable-next-line @typescript-eslint/naming-convention
  10. const _target = new Vector3()
  11. // eslint-disable-next-line @typescript-eslint/naming-convention
  12. const _changeEvent: IEvent<'change'> = {type: 'change'}
  13. // todo bug - this is not showing in the UI. To test, switch to threeFirstPerson controlsMode for Default Camera in the tweakpane editor
  14. @uiPanelContainer('First Person Controls')
  15. export class FirstPersonControls2 extends EventDispatcher implements ICameraControls<'change'> {
  16. readonly object: Object3D
  17. readonly domElement: HTMLElement | Document
  18. // API
  19. @serialize() @uiToggle() enabled = true
  20. @serialize() @uiToggle() enableKeys = true
  21. @serialize() @uiInput() movementSpeed = 1.0
  22. @serialize() @uiInput() lookSpeed = 0.005
  23. @serialize() @uiToggle() lookVertical = true
  24. @serialize() @uiToggle() autoForward = false
  25. @serialize() @uiToggle() activeLook = true
  26. @serialize() @uiToggle() heightSpeed = false
  27. @serialize() @uiInput() heightCoef = 1.0
  28. @serialize() @uiInput() heightMin = 0.0
  29. @serialize() @uiInput() heightMax = 1.0
  30. @serialize() @uiToggle() constrainVertical = false
  31. @serialize() @uiInput() verticalMin = 0
  32. @serialize() @uiInput() verticalMax = Math.PI
  33. @serialize() @uiToggle() mouseDragOn = false
  34. // internals
  35. autoSpeedFactor = 0.0
  36. pointerX = 0
  37. pointerY = 0
  38. moveForward = false
  39. moveBackward = false
  40. moveLeft = false
  41. moveRight = false
  42. moveUp = false
  43. moveDown = false
  44. viewHalfX = 0
  45. viewHalfY = 0
  46. // private variables
  47. // eslint-disable-next-line @typescript-eslint/naming-convention
  48. private lat = 0
  49. // eslint-disable-next-line @typescript-eslint/naming-convention
  50. private lon = 0
  51. constructor(object: Object3D, domElement: HTMLElement|Document) {
  52. super()
  53. this.object = object
  54. this.domElement = domElement
  55. this.onPointerMove = this.onPointerMove.bind(this)
  56. this.onPointerDown = this.onPointerDown.bind(this)
  57. this.onPointerUp = this.onPointerUp.bind(this)
  58. this.onKeyDown = this.onKeyDown.bind(this)
  59. this.onKeyUp = this.onKeyUp.bind(this)
  60. this.onContextMenu = this.onContextMenu.bind(this)
  61. this.domElement.addEventListener('contextmenu', this.onContextMenu)
  62. ;(this.domElement as HTMLElement).addEventListener('pointermove', this.onPointerMove)
  63. ;(this.domElement as HTMLElement).addEventListener('pointerdown', this.onPointerDown)
  64. ;(this.domElement as HTMLElement).addEventListener('pointerup', this.onPointerUp)
  65. window.addEventListener('keydown', this.onKeyDown)
  66. window.addEventListener('keyup', this.onKeyUp)
  67. this.handleResize()
  68. this.setOrientation()
  69. }
  70. setOrientation() {
  71. const quaternion = this.object.quaternion
  72. _lookDirection.set(0, 0, -1).applyQuaternion(quaternion)
  73. _spherical.setFromVector3(_lookDirection)
  74. this.lat = 90 - MathUtils.radToDeg(_spherical.phi)
  75. this.lon = MathUtils.radToDeg(_spherical.theta)
  76. }
  77. handleResize() {
  78. if (this.domElement === document) {
  79. this.viewHalfX = window.innerWidth / 2
  80. this.viewHalfY = window.innerHeight / 2
  81. } else {
  82. this.viewHalfX = (this.domElement as HTMLElement).offsetWidth / 2
  83. this.viewHalfY = (this.domElement as HTMLElement).offsetHeight / 2
  84. }
  85. }
  86. onPointerDown(event: PointerEvent) {
  87. if (this.domElement !== document) {
  88. (this.domElement as HTMLElement).focus()
  89. }
  90. if (this.activeLook) {
  91. switch (event.button) {
  92. case 0: this.moveForward = true; break
  93. case 2: this.moveBackward = true; break
  94. default: break
  95. }
  96. }
  97. this.mouseDragOn = true
  98. }
  99. onPointerUp(event: PointerEvent) {
  100. if (this.activeLook) {
  101. switch (event.button) {
  102. case 0: this.moveForward = false; break
  103. case 2: this.moveBackward = false; break
  104. default: break
  105. }
  106. }
  107. this.mouseDragOn = false
  108. }
  109. onPointerMove(event: PointerEvent) {
  110. if (this.domElement === document) {
  111. this.pointerX = event.pageX - this.viewHalfX
  112. this.pointerY = event.pageY - this.viewHalfY
  113. } else {
  114. this.pointerX = event.pageX - (this.domElement as HTMLElement).offsetLeft - this.viewHalfX
  115. this.pointerY = event.pageY - (this.domElement as HTMLElement).offsetTop - this.viewHalfY
  116. }
  117. }
  118. onKeyDown(event: KeyboardEvent) {
  119. if (!this.enableKeys) return
  120. switch (event.code) {
  121. case 'ArrowUp':
  122. case 'KeyW': this.moveForward = true; break
  123. case 'ArrowLeft':
  124. case 'KeyA': this.moveLeft = true; break
  125. case 'ArrowDown':
  126. case 'KeyS': this.moveBackward = true; break
  127. case 'ArrowRight':
  128. case 'KeyD': this.moveRight = true; break
  129. case 'KeyR': this.moveUp = true; break
  130. case 'KeyF': this.moveDown = true; break
  131. default: break
  132. }
  133. }
  134. onKeyUp(event: KeyboardEvent) {
  135. if (!this.enableKeys) return
  136. switch (event.code) {
  137. case 'ArrowUp':
  138. case 'KeyW': this.moveForward = false; break
  139. case 'ArrowLeft':
  140. case 'KeyA': this.moveLeft = false; break
  141. case 'ArrowDown':
  142. case 'KeyS': this.moveBackward = false; break
  143. case 'ArrowRight':
  144. case 'KeyD': this.moveRight = false; break
  145. case 'KeyR': this.moveUp = false; break
  146. case 'KeyF': this.moveDown = false; break
  147. default: break
  148. }
  149. }
  150. lookAt(x: number|Vector3, y?: number, z?: number) {
  151. if ((x as Vector3).isVector3) {
  152. _target.copy(x as Vector3)
  153. } else {
  154. if (y === undefined || z === undefined) console.error('FirstPersonControls2.lookAt: y and z parameters are required')
  155. else _target.set(x as number, y, z)
  156. }
  157. this.object.lookAt(_target)
  158. this.setOrientation()
  159. return this
  160. }
  161. // eslint-disable-next-line @typescript-eslint/naming-convention
  162. private targetPosition = new Vector3()
  163. private _lastTime = -1 // in ms
  164. update() {
  165. const time = now() // in ms
  166. const delta = (this._lastTime < 0 ? 16 : Math.min(time - this._lastTime, 1000)) / 1000 // in secs
  167. this._lastTime = time
  168. // console.log(delta)
  169. if (!this.enabled) return
  170. if (this.heightSpeed) {
  171. const y = MathUtils.clamp(this.object.position.y, this.heightMin, this.heightMax)
  172. const heightDelta = y - this.heightMin
  173. this.autoSpeedFactor = delta * (heightDelta * this.heightCoef)
  174. } else {
  175. this.autoSpeedFactor = 0.0
  176. }
  177. const actualMoveSpeed = delta * this.movementSpeed
  178. if (this.moveForward || this.autoForward && !this.moveBackward) this.object.translateZ(-(actualMoveSpeed + this.autoSpeedFactor))
  179. if (this.moveBackward) this.object.translateZ(actualMoveSpeed)
  180. if (this.moveLeft) this.object.translateX(-actualMoveSpeed)
  181. if (this.moveRight) this.object.translateX(actualMoveSpeed)
  182. if (this.moveUp) this.object.translateY(actualMoveSpeed)
  183. if (this.moveDown) this.object.translateY(-actualMoveSpeed)
  184. let actualLookSpeed = delta * this.lookSpeed
  185. if (!this.activeLook) {
  186. actualLookSpeed = 0
  187. }
  188. let verticalLookRatio = 1
  189. if (this.constrainVertical) {
  190. verticalLookRatio = Math.PI / (this.verticalMax - this.verticalMin)
  191. }
  192. this.lon -= this.pointerX * actualLookSpeed
  193. if (this.lookVertical) this.lat -= this.pointerY * actualLookSpeed * verticalLookRatio
  194. this.lat = Math.max(-85, Math.min(85, this.lat))
  195. let phi = MathUtils.degToRad(90 - this.lat)
  196. const theta = MathUtils.degToRad(this.lon)
  197. if (this.constrainVertical) {
  198. phi = MathUtils.mapLinear(phi, 0, Math.PI, this.verticalMin, this.verticalMax)
  199. }
  200. const position = this.object.position
  201. this.targetPosition.setFromSphericalCoords(1, phi, theta).add(position)
  202. this.object.lookAt(this.targetPosition)
  203. this.dispatchEvent(_changeEvent)
  204. }
  205. dispose() {
  206. this.domElement.removeEventListener('contextmenu', this.onContextMenu)
  207. ;(this.domElement as HTMLElement).removeEventListener('pointerdown', this.onPointerDown)
  208. ;(this.domElement as HTMLElement).removeEventListener('pointermove', this.onPointerMove)
  209. ;(this.domElement as HTMLElement).removeEventListener('pointerup', this.onPointerUp)
  210. window.removeEventListener('keydown', this.onKeyDown)
  211. window.removeEventListener('keyup', this.onKeyUp)
  212. }
  213. onContextMenu(event: Event) {
  214. if (!this.enableKeys) return
  215. event.preventDefault()
  216. }
  217. }