UNPKG

mylingo3d

Version:

Lingo3D is a React/Vue 3d game development framework that ships with a complete visual editor

420 lines (365 loc) 12.3 kB
import { CameraHelper, PerspectiveCamera, Quaternion } from "three" import ObjectManager from "../ObjectManager" import { debounceInstance, last } from "@lincode/utils" import { scaleDown } from "../../../engine/constants" import { ray, euler, quaternion, quaternion_, halfPi } from "../../utils/reusables" import pillShape from "../PhysicsObjectManager/cannon/shapes/pillShape" import ICameraBase, { MouseControl } from "../../../interface/ICameraBase" import { deg2Rad, Point3d } from "@lincode/math" import { MIN_POLAR_ANGLE, MAX_POLAR_ANGLE } from "../../../globals" import { Reactive } from "@lincode/reactivity" import MeshItem from "../MeshItem" import { Cancellable } from "@lincode/promiselikes" import mainCamera from "../../../engine/mainCamera" import scene from "../../../engine/scene" import { onSelectionTarget, emitSelectionTarget } from "../../../events/onSelectionTarget" import { bokehDefault } from "../../../states/useBokeh" import { bokehApertureDefault } from "../../../states/useBokehAperture" import { bokehFocusDefault } from "../../../states/useBokehFocus" import { bokehMaxBlurDefault } from "../../../states/useBokehMaxBlur" import { setBokehRefresh } from "../../../states/useBokehRefresh" import { pushCameraList, pullCameraList } from "../../../states/useCameraList" import { getCameraRendered } from "../../../states/useCameraRendered" import { pullCameraStack, getCameraStack, pushCameraStack } from "../../../states/useCameraStack" import makeCameraSprite from "../utils/makeCameraSprite" import getWorldPosition from "../../utils/getWorldPosition" import getWorldQuaternion from "../../utils/getWorldQuaternion" import getWorldDirection from "../../utils/getWorldDirection" export default abstract class CameraBase<T extends PerspectiveCamera> extends ObjectManager implements ICameraBase { protected override _physicsShape = pillShape protected midObject3d = this.outerObject3d public constructor(protected camera: T) { super() this.object3d.add(camera) camera.userData.manager = this pushCameraList(camera) this.then(() => { pullCameraStack(camera) pullCameraList(camera) }) this.createEffect(() => { if ( getCameraRendered() !== mainCamera || getCameraRendered() === camera ) return const helper = new CameraHelper(camera) scene.add(helper) const sprite = makeCameraSprite() helper.add(sprite.outerObject3d) const handle = onSelectionTarget(({ target }) => { target === sprite && emitSelectionTarget(this as any) }) return () => { helper.dispose() scene.remove(helper) sprite.dispose() handle.cancel() } }, [getCameraRendered]) } public override lookAt(target: MeshItem | Point3d): void public override lookAt(x: number, y: number | undefined, z: number): void public override lookAt(a0: any, a1?: any, a2?: any) { super.lookAt(a0, a1, a2) const angle = euler.setFromQuaternion(this.outerObject3d.quaternion) angle.x += Math.PI angle.z += Math.PI this.outerObject3d.setRotationFromEuler(angle) } public get fov() { return this.camera.fov } public set fov(val) { this.camera.fov = val this.camera.updateProjectionMatrix?.() } public get zoom() { return this.camera.zoom } public set zoom(val) { this.camera.zoom = val this.camera.updateProjectionMatrix?.() } public get near() { return this.camera.near } public set near(val) { this.camera.near = val this.camera.updateProjectionMatrix?.() } public get far() { return this.camera.far } public set far(val) { this.camera.far = val this.camera.updateProjectionMatrix?.() } public get active() { return last(getCameraStack()) === this.camera } public set active(val) { pullCameraStack(this.camera) val && pushCameraStack(this.camera) } public get transition() { return this.camera.userData.transition as boolean | number | undefined } public set transition(val) { this.camera.userData.transition = val } public get bokeh() { return this.camera.userData.bokeh ?? bokehDefault } public set bokeh(val) { this.camera.userData.bokeh = val setBokehRefresh({}) } public get bokehFocus() { return this.camera.userData.bokehFocus ?? bokehFocusDefault } public set bokehFocus(val) { this.camera.userData.bokehFocus = val setBokehRefresh({}) } public get bokehMaxBlur() { return this.camera.userData.bokehMaxBlur ?? bokehMaxBlurDefault } public set bokehMaxBlur(val) { this.camera.userData.bokehMaxBlur = val setBokehRefresh({}) } public get bokehAperture() { return this.camera.userData.bokehAperture ?? bokehApertureDefault } public set bokehAperture(val) { this.camera.userData.bokehAperture = val setBokehRefresh({}) } protected override getRay() { return ray.set( getWorldPosition(this.camera), getWorldDirection(this.camera) ) } public override append(object: MeshItem) { this._append(object) this.camera.add(object.outerObject3d) } public override attach(object: MeshItem) { this._append(object) this.camera.attach(object.outerObject3d) } public override get width() { return super.width } public override set width(val) { const num = val * scaleDown this.object3d.scale.x = num this.camera.scale.x = 1 / num } public override get height() { return super.height } public override set height(val) { const num = val * scaleDown this.object3d.scale.y = num this.camera.scale.y = 1 / num } public override get depth() { return super.depth } public override set depth(val) { const num = val * scaleDown this.object3d.scale.z = num this.camera.scale.z = 1 / num } protected orbitMode?: boolean private _gyrate(movementX: number, movementY: number, inner?: boolean) { const manager = inner ? this.object3d : this.midObject3d euler.setFromQuaternion(manager.quaternion) euler.y -= movementX * 0.002 euler.y = Math.max( halfPi - this._maxAzimuthAngle * deg2Rad, Math.min(halfPi - this._minAzimuthAngle * deg2Rad, euler.y) ) euler.x -= movementY * 0.002 euler.x = Math.max( halfPi - this._maxPolarAngle * deg2Rad, Math.min(halfPi - this._minPolarAngle * deg2Rad, euler.x) ) manager.setRotationFromEuler(euler) !inner && this.rotationUpdate?.updateXYZ() } private gyrateHandle?: Cancellable public gyrate(movementX: number, movementY: number, noDamping?: boolean) { if (this.enableDamping) { movementX *= 0.5 movementY *= 0.5 } if (this.orbitMode) this._gyrate(movementX, movementY) else { this._gyrate(movementX, 0) this._gyrate(0, movementY, true) } if (!this.enableDamping || noDamping || !(movementX || movementY)) return this.gyrateHandle?.cancel() let factor = 1 const handle = (this.gyrateHandle = this.beforeRender(() => { factor *= 0.95 this._gyrate(movementX * factor, movementY * factor) factor <= 0.001 && handle.cancel() })) } private static updateAngle = debounceInstance( (target: CameraBase<PerspectiveCamera>) => target.gyrate(0, 0), 0, "trailing" ) protected updateAngle() { CameraBase.updateAngle(this, this) } private _minPolarAngle = MIN_POLAR_ANGLE public get minPolarAngle() { return this._minPolarAngle } public set minPolarAngle(val) { this._minPolarAngle = val this.updateAngle() } private _maxPolarAngle = MAX_POLAR_ANGLE public get maxPolarAngle() { return this._maxPolarAngle } public set maxPolarAngle(val) { this._maxPolarAngle = val this.updateAngle() } private _minAzimuthAngle = -Infinity public get minAzimuthAngle() { return this._minAzimuthAngle } public set minAzimuthAngle(val) { this._minAzimuthAngle = val this.updateAngle() } private _maxAzimuthAngle = Infinity public get maxAzimuthAngle() { return this._maxAzimuthAngle } public set maxAzimuthAngle(val) { this._maxAzimuthAngle = val this.updateAngle() } public setPolarAngle(angle: number) { const { _minPolarAngle, _maxPolarAngle } = this this.minPolarAngle = this.maxPolarAngle = angle this.queueMicrotask(() => { this.minPolarAngle = _minPolarAngle this.maxPolarAngle = _maxPolarAngle }) } public setAzimuthAngle(angle: number) { const { _minAzimuthAngle, _maxAzimuthAngle } = this this.minAzimuthAngle = this.maxAzimuthAngle = angle this.queueMicrotask(() => { this.minAzimuthAngle = _minAzimuthAngle this.maxAzimuthAngle = _maxAzimuthAngle }) } private _polarAngle?: number public get polarAngle() { return this._polarAngle } public set polarAngle(val) { this._polarAngle = val val && this.setPolarAngle(val) } private _azimuthAngle?: number public get azimuthAngle() { return this._azimuthAngle } public set azimuthAngle(val) { this._azimuthAngle = val val && this.setAzimuthAngle(val) } public enableDamping = false protected mouseControlState = new Reactive<MouseControl>(false) private mouseControlInit?: boolean public get mouseControl() { return this.mouseControlState.get() } public set mouseControl(val) { this.mouseControlState.set(val) if (!val || this.mouseControlInit) return this.mouseControlInit = true import("./enableMouseControl").then((module) => module.default.call(this) ) } private _gyroControl?: boolean public get gyroControl() { return !!this._gyroControl } public set gyroControl(val) { this._gyroControl = val const deviceEuler = euler const deviceQuaternion = quaternion const screenTransform = quaternion_ const worldTransform = new Quaternion( -Math.sqrt(0.5), 0, 0, Math.sqrt(0.5) ) const quat = getWorldQuaternion(this.object3d) const orient = 0 const cb = (e: DeviceOrientationEvent) => { this.object3d.quaternion.copy(quat) deviceEuler.set( (e.beta ?? 0) * deg2Rad, (e.alpha ?? 0) * deg2Rad, -(e.gamma ?? 0) * deg2Rad, "YXZ" ) this.object3d.quaternion.multiply( deviceQuaternion.setFromEuler(deviceEuler) ) const minusHalfAngle = -orient * 0.5 screenTransform.set( 0, Math.sin(minusHalfAngle), 0, Math.cos(minusHalfAngle) ) this.object3d.quaternion.multiply(screenTransform) this.object3d.quaternion.multiply(worldTransform) } val && window.addEventListener("deviceorientation", cb) this.cancelHandle( "gyroControl", val && (() => new Cancellable(() => window.removeEventListener("deviceorientation", cb) )) ) } }