3d-tiles-renderer
Version:
https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification
330 lines (211 loc) • 10.3 kB
JavaScript
import { Clock, EventDispatcher, MathUtils, OrthographicCamera, PerspectiveCamera, Quaternion, Vector3 } from 'three';
const _forward = /* @__PURE__ */ new Vector3();
const _vec = /* @__PURE__ */ new Vector3();
const _orthographicCamera = /* @__PURE__ */ new OrthographicCamera();
const _targetOffset = /* @__PURE__ */ new Vector3();
const _perspOffset = /* @__PURE__ */ new Vector3();
const _orthoOffset = /* @__PURE__ */ new Vector3();
const _quat = /* @__PURE__ */ new Quaternion();
const _targetQuat = /* @__PURE__ */ new Quaternion();
export class CameraTransitionManager extends EventDispatcher {
get animating() {
return this._alpha !== 0 && this._alpha !== 1;
}
get alpha() {
// the transition alpha towards the target camera
return this._target === 0 ? 1 - this._alpha : this._alpha;
}
get camera() {
if ( this._alpha === 0 ) return this.perspectiveCamera;
if ( this._alpha === 1 ) return this.orthographicCamera;
return this.transitionCamera;
}
get mode() {
return this._target === 0 ? 'perspective' : 'orthographic';
}
set mode( v ) {
if ( v === this.mode ) {
return;
}
const prevCamera = this.camera;
if ( v === 'perspective' ) {
this._target = 0;
this._alpha = 0;
} else {
this._target = 1;
this._alpha = 1;
}
this.dispatchEvent( { type: 'camera-change', camera: this.camera, prevCamera: prevCamera } );
}
constructor( perspectiveCamera = new PerspectiveCamera(), orthographicCamera = new OrthographicCamera() ) {
super();
this.perspectiveCamera = perspectiveCamera;
this.orthographicCamera = orthographicCamera;
this.transitionCamera = new PerspectiveCamera();
// settings
this.orthographicPositionalZoom = true;
this.orthographicOffset = 50;
this.fixedPoint = new Vector3();
this.duration = 200;
this.autoSync = true;
this.easeFunction = x => x;
this._target = 0;
this._alpha = 0;
this._clock = new Clock();
}
toggle() {
// reset the clock for cases where we're not calling "update" every frame
this._target = this._target === 1 ? 0 : 1;
this._clock.getDelta();
this.dispatchEvent( { type: 'toggle' } );
}
update( deltaTime = Math.min( this._clock.getDelta(), 64 / 1000 ) ) {
// update transforms
if ( this.autoSync ) {
this.syncCameras();
}
// perform transition
const { perspectiveCamera, orthographicCamera, transitionCamera, camera } = this;
const delta = deltaTime * 1e3;
if ( this._alpha !== this._target ) {
const direction = Math.sign( this._target - this._alpha );
const step = direction * delta / this.duration;
this._alpha = MathUtils.clamp( this._alpha + step, 0, 1 );
this.dispatchEvent( { type: 'change', alpha: this.alpha } );
}
// find the new camera
const prevCamera = camera;
let newCamera = null;
if ( this._alpha === 0 ) {
newCamera = perspectiveCamera;
} else if ( this._alpha === 1 ) {
newCamera = orthographicCamera;
} else {
newCamera = transitionCamera;
this._updateTransitionCamera();
}
if ( prevCamera !== newCamera ) {
if ( newCamera === transitionCamera ) {
this.dispatchEvent( { type: 'transition-start' } );
}
this.dispatchEvent( { type: 'camera-change', camera: newCamera, prevCamera: prevCamera } );
if ( prevCamera === transitionCamera ) {
this.dispatchEvent( { type: 'transition-end' } );
}
}
}
syncCameras() {
const fromCamera = this._getFromCamera();
const { perspectiveCamera, orthographicCamera, transitionCamera, fixedPoint } = this;
_forward.set( 0, 0, - 1 ).transformDirection( fromCamera.matrixWorld ).normalize();
if ( fromCamera.isPerspectiveCamera ) {
// offset the orthographic camera backwards based on user setting to avoid cases where the ortho
// camera position will clip into terrain when once transitioned
if ( this.orthographicPositionalZoom ) {
orthographicCamera.position.copy( perspectiveCamera.position ).addScaledVector( _forward, - this.orthographicOffset );
orthographicCamera.rotation.copy( perspectiveCamera.rotation );
orthographicCamera.updateMatrixWorld();
} else {
const orthoDist = _vec.subVectors( fixedPoint, orthographicCamera.position ).dot( _forward );
const perspDist = _vec.subVectors( fixedPoint, perspectiveCamera.position ).dot( _forward );
_vec.copy( perspectiveCamera.position ).addScaledVector( _forward, perspDist );
orthographicCamera.rotation.copy( perspectiveCamera.rotation );
orthographicCamera.position.copy( _vec ).addScaledVector( _forward, - orthoDist );
orthographicCamera.updateMatrixWorld();
}
// calculate the necessary orthographic zoom based on the current perspective camera position
const distToPoint = Math.abs( _vec.subVectors( perspectiveCamera.position, fixedPoint ).dot( _forward ) );
const projectionHeight = 2 * Math.tan( MathUtils.DEG2RAD * perspectiveCamera.fov * 0.5 ) * distToPoint;
const orthoHeight = orthographicCamera.top - orthographicCamera.bottom;
orthographicCamera.zoom = orthoHeight / projectionHeight;
orthographicCamera.updateProjectionMatrix();
} else {
// calculate the target distance from the point
const distToPoint = Math.abs( _vec.subVectors( orthographicCamera.position, fixedPoint ).dot( _forward ) );
const orthoHeight = ( orthographicCamera.top - orthographicCamera.bottom ) / orthographicCamera.zoom;
const targetDist = orthoHeight * 0.5 / Math.tan( MathUtils.DEG2RAD * perspectiveCamera.fov * 0.5 );
// set the final camera position so the pivot point is stable
perspectiveCamera.rotation.copy( orthographicCamera.rotation );
perspectiveCamera.position.copy( orthographicCamera.position )
.addScaledVector( _forward, distToPoint )
.addScaledVector( _forward, - targetDist );
perspectiveCamera.updateMatrixWorld();
// shift the orthographic camera position so it aligns with the perspective cameras position as
// calculated by the FoV. This ensures a consistent orthographic position on transition.
if ( this.orthographicPositionalZoom ) {
orthographicCamera.position.copy( perspectiveCamera.position ).addScaledVector( _forward, - this.orthographicOffset );
orthographicCamera.updateMatrixWorld();
}
}
transitionCamera.position.copy( perspectiveCamera.position );
transitionCamera.rotation.copy( perspectiveCamera.rotation );
}
_getTransitionDirection() {
return Math.sign( this._target - this._alpha );
}
_getToCamera() {
const dir = this._getTransitionDirection();
if ( dir === 0 ) {
return this._target === 0 ? this.perspectiveCamera : this.orthographicCamera;
} else if ( dir > 0 ) {
return this.orthographicCamera;
} else {
return this.perspectiveCamera;
}
}
_getFromCamera() {
const dir = this._getTransitionDirection();
if ( dir === 0 ) {
return this._target === 0 ? this.perspectiveCamera : this.orthographicCamera;
} else if ( dir > 0 ) {
return this.perspectiveCamera;
} else {
return this.orthographicCamera;
}
}
_updateTransitionCamera() {
// Perform transition interpolation between the orthographic and perspective camera
// alpha === 0 : perspective
// alpha === 1 : orthographic
const { perspectiveCamera, orthographicCamera, transitionCamera, fixedPoint } = this;
const alpha = this.easeFunction( this._alpha );
// get the forward vector
_forward.set( 0, 0, - 1 ).transformDirection( orthographicCamera.matrixWorld ).normalize();
_orthographicCamera.copy( orthographicCamera );
_orthographicCamera.position.addScaledVector( _forward, orthographicCamera.near );
orthographicCamera.far -= orthographicCamera.near;
orthographicCamera.near = 0;
// compute the projection height based on the perspective camera
_forward.set( 0, 0, - 1 ).transformDirection( perspectiveCamera.matrixWorld ).normalize();
const distToPoint = Math.abs( _vec.subVectors( perspectiveCamera.position, fixedPoint ).dot( _forward ) );
const projectionHeight = 2 * Math.tan( MathUtils.DEG2RAD * perspectiveCamera.fov * 0.5 ) * distToPoint;
// calculate the orientation to transition to
const targetQuat = _targetQuat.slerpQuaternions( perspectiveCamera.quaternion, _orthographicCamera.quaternion, alpha );
// calculate the target distance and fov to position the camera at
const targetFov = MathUtils.lerp( perspectiveCamera.fov, 1, alpha );
const targetDistance = projectionHeight * 0.5 / Math.tan( MathUtils.DEG2RAD * targetFov * 0.5 );
// calculate the offset from the fixed point
const orthoOffset = _orthoOffset.copy( _orthographicCamera.position ).sub( fixedPoint ).applyQuaternion( _quat.copy( _orthographicCamera.quaternion ).invert() );
const perspOffset = _perspOffset.copy( perspectiveCamera.position ).sub( fixedPoint ).applyQuaternion( _quat.copy( perspectiveCamera.quaternion ).invert() );
const targetOffset = _targetOffset.lerpVectors( perspOffset, orthoOffset, alpha );
targetOffset.z -= Math.abs( targetOffset.z ) - targetDistance;
// calculate distances to the target point so the offset can be accounted for in near plane calculations
const distToPersp = - ( perspOffset.z - targetOffset.z );
const distToOrtho = - ( orthoOffset.z - targetOffset.z );
// calculate the near and far plane positions
const targetNearPlane = MathUtils.lerp( distToPersp + perspectiveCamera.near, distToOrtho + _orthographicCamera.near, alpha );
const targetFarPlane = MathUtils.lerp( distToPersp + perspectiveCamera.far, distToOrtho + _orthographicCamera.far, alpha );
const planeDelta = Math.max( targetFarPlane, 0 ) - Math.max( targetNearPlane, 0 );
// NOTE: The "planeDelta * 1e-5" can wind up being larger than either of the camera near planes, resulting
// in some clipping during the transition phase.
// update the camera state
transitionCamera.aspect = perspectiveCamera.aspect;
transitionCamera.fov = targetFov;
transitionCamera.near = Math.max( targetNearPlane, planeDelta * 1e-5 );
transitionCamera.far = targetFarPlane;
transitionCamera.position.copy( targetOffset ).applyQuaternion( targetQuat ).add( fixedPoint );
transitionCamera.quaternion.copy( targetQuat );
transitionCamera.updateProjectionMatrix();
transitionCamera.updateMatrixWorld();
}
}