src/viewer/scene/camera/CameraFlightAnimation.js
import {math} from '../math/math.js';
import {utils} from '../utils.js';
import {core} from '../core.js';
import {Component} from '../Component.js';
const tempVec3 = math.vec3();
const newLook = math.vec3();
const newEye = math.vec3();
const newUp = math.vec3();
const newLookEyeVec = math.vec3();
/**
* @desc Jumps or flies the {@link Scene}'s {@link Camera} to a given target.
*
* * Located at {@link Viewer#cameraFlight}
* * Can fly or jump to its target.
* * While flying, can be stopped, or redirected to a different target.
* * Can also smoothly transition between ortho and perspective projections.
*
*
* A CameraFlightAnimation's target can be:
*
* * specific ````eye````, ````look```` and ````up```` positions,
* * an axis-aligned World-space bounding box (AABB), or
* * an instance or ID of any {@link Component} subtype that provides a World-space AABB.
*
* A target can also contain a ````projection```` type to transition into. For example, if your {@link Camera#projection} is
* currently ````"perspective"```` and you supply {@link CameraFlightAnimation#flyTo} with a ````projection```` property
* equal to "ortho", then CameraFlightAnimation will smoothly transition the Camera into an orthographic projection.
*
* Configure {@link CameraFlightAnimation#fit} and {@link CameraFlightAnimation#fitFOV} to make it stop at the point
* where the target occupies a certain amount of the field-of-view.
*
* ## Flying to an Entity
*
* Flying to an {@link Entity}:
*
* ````Javascript
* var entity = new Mesh(viewer.scene);
*
* // Fly to the Entity's World-space AABB
* viewer.cameraFlight.flyTo(entity);
* ````
* ## Flying to a Position
*
* Flying the CameraFlightAnimation from the previous example to specified eye, look and up positions:
*
* ````Javascript
* viewer.cameraFlight.flyTo({
* eye: [-5,-5,-5],
* look: [0,0,0]
* up: [0,1,0],
* duration: 1 // Default, seconds
* },() => {
* // Done
* });
* ````
*
* ## Flying to an AABB
*
* Flying the CameraFlightAnimation from the previous two examples explicitly to the {@link Boundary3D"}}Boundary3D's{{/crossLink}}
* axis-aligned bounding box:
*
* ````Javascript
* viewer.cameraFlight.flyTo(entity.aabb);
* ````
*
* ## Transitioning Between Projections
*
* CameraFlightAnimation also allows us to smoothly transition between Camera projections. We can do that by itself, or
* in addition to flying the Camera to a target.
*
* Let's transition the Camera to orthographic projection:
*
* [[Run example](/examples/index.html#camera_CameraFlightAnimation_projection)]
*
* ````Javascript
* viewer.cameraFlight.flyTo({ projection: "ortho", () => {
* // Done
* });
* ````
*
* Now let's transition the Camera back to perspective projection:
*
* ````Javascript
* viewer.cameraFlight.flyTo({ projection: "perspective"}, () => {
* // Done
* });
* ````
*
* Fly Camera to a position, while transitioning to orthographic projection:
*
* ````Javascript
* viewer.cameraFlight.flyTo({
* eye: [-100,20,2],
* look: [0,0,-40],
* up: [0,1,0],
* projection: "ortho", () => {
* // Done
* });
* ````
*/
class CameraFlightAnimation extends Component {
/**
* @private
*/
get type() {
return "CameraFlightAnimation";
}
/**
@constructor
@private
*/
constructor(owner, cfg = {}) {
super(owner, cfg);
this._look1 = math.vec3();
this._eye1 = math.vec3();
this._up1 = math.vec3();
this._look2 = math.vec3();
this._eye2 = math.vec3();
this._up2 = math.vec3();
this._orthoScale1 = 1;
this._orthoScale2 = 1;
this._flying = false;
this._flyEyeLookUp = false;
this._flyingEye = false;
this._flyingLook = false;
this._callback = null;
this._callbackScope = null;
this._time1 = null;
this._time2 = null;
this.easing = cfg.easing !== false;
this.duration = cfg.duration;
this.fit = cfg.fit;
this.fitFOV = cfg.fitFOV;
this.trail = cfg.trail;
}
/**
* Flies the {@link Camera} to a target.
*
* * When the target is a boundary, the {@link Camera} will fly towards the target and stop when the target fills most of the canvas.
* * When the target is an explicit {@link Camera} position, given as ````eye````, ````look```` and ````up````, then CameraFlightAnimation will interpolate the {@link Camera} to that target and stop there.
*
* @param {Object|Component} [params=Scene] Either a parameters object or a {@link Component} subtype that has
* an AABB. Defaults to the {@link Scene}, which causes the {@link Camera} to fit the Scene in view.
* @param {Number} [params.arc=0] Factor in range ````[0..1]```` indicating how much the {@link Camera#eye} position
* will swing away from its {@link Camera#look} position as it flies to the target.
* @param {Number|String|Component} [params.component] ID or instance of a component to fly to. Defaults to the entire {@link Scene}.
* @param {Number[]} [params.aabb] World-space axis-aligned bounding box (AABB) target to fly to.
* @param {Number[]} [params.eye] Position to fly the eye position to.
* @param {Number[]} [params.look] Position to fly the look position to.
* @param {Number[]} [params.up] Position to fly the up vector to.
* @param {String} [params.projection] Projection type to transition into as we fly. Can be any of the values of {@link Camera.projection}.
* @param {Boolean} [params.fit=true] Whether to fit the target to the view volume. Overrides {@link CameraFlightAnimation#fit}.
* @param {Number} [params.fitFOV] How much of field-of-view, in degrees, that a target {@link Entity} or its AABB should
* fill the canvas on arrival. Overrides {@link CameraFlightAnimation#fitFOV}.
* @param {Number} [params.duration] Flight duration in seconds. Overrides {@link CameraFlightAnimation#duration}.
* @param {Number} [params.orthoScale] Animate the Camera's orthographic scale to this target value. See {@link Ortho#scale}.
* @param {Function} [callback] Callback fired on arrival.
* @param {Object} [scope] Optional scope for callback.
*/
flyTo(params, callback, scope) {
params = params || this.scene;
if (this._flying) {
this.stop();
}
this._flying = false;
this._flyingEye = false;
this._flyingLook = false;
this._flyingEyeLookUp = false;
this._callback = callback;
this._callbackScope = scope;
const camera = this.scene.camera;
const flyToProjection = (!!params.projection) && (params.projection !== camera.projection);
this._eye1[0] = camera.eye[0];
this._eye1[1] = camera.eye[1];
this._eye1[2] = camera.eye[2];
this._look1[0] = camera.look[0];
this._look1[1] = camera.look[1];
this._look1[2] = camera.look[2];
this._up1[0] = camera.up[0];
this._up1[1] = camera.up[1];
this._up1[2] = camera.up[2];
this._orthoScale1 = camera.ortho.scale;
this._orthoScale2 = params.orthoScale || this._orthoScale1;
let aabb;
let eye;
let look;
let up;
let componentId;
if (params.aabb) {
aabb = params.aabb;
} else if (params.length === 6) {
aabb = params;
} else if ((params.eye && params.look) || params.up) {
eye = params.eye;
look = params.look;
up = params.up;
} else if (params.eye) {
eye = params.eye;
} else if (params.look) {
look = params.look;
} else { // Argument must be an instance or ID of a Component (subtype)
let component = params;
if (utils.isNumeric(component) || utils.isString(component)) {
componentId = component;
component = this.scene.components[componentId];
if (!component) {
this.error("Component not found: " + utils.inQuotes(componentId));
if (callback) {
if (scope) {
callback.call(scope);
} else {
callback();
}
}
return;
}
}
if (!flyToProjection) {
aabb = component.aabb || this.scene.aabb;
}
}
const poi = params.poi;
if (aabb) {
if (aabb[3] < aabb[0] || aabb[4] < aabb[1] || aabb[5] < aabb[2]) { // Don't fly to an inverted boundary
return;
}
if (aabb[3] === aabb[0] && aabb[4] === aabb[1] && aabb[5] === aabb[2]) { // Don't fly to an empty boundary
return;
}
aabb = aabb.slice();
const aabbCenter = math.getAABB3Center(aabb);
this._look2 = poi || aabbCenter;
const eyeLookVec = math.subVec3(this._eye1, this._look1, tempVec3);
const eyeLookVecNorm = math.normalizeVec3(eyeLookVec);
const diag = poi ? math.getAABB3DiagPoint(aabb, poi) : math.getAABB3Diag(aabb);
const fitFOV = params.fitFOV || this._fitFOV;
const sca = Math.abs(diag / Math.tan(fitFOV * math.DEGTORAD));
this._orthoScale2 = diag * 1.1;
this._eye2[0] = this._look2[0] + (eyeLookVecNorm[0] * sca);
this._eye2[1] = this._look2[1] + (eyeLookVecNorm[1] * sca);
this._eye2[2] = this._look2[2] + (eyeLookVecNorm[2] * sca);
this._up2[0] = this._up1[0];
this._up2[1] = this._up1[1];
this._up2[2] = this._up1[2];
this._flyingEyeLookUp = true;
} else if (eye || look || up) {
this._flyingEyeLookUp = !!eye && !!look && !!up;
this._flyingEye = !!eye && !look;
this._flyingLook = !!look && !eye;
if (eye) {
this._eye2[0] = eye[0];
this._eye2[1] = eye[1];
this._eye2[2] = eye[2];
}
if (look) {
this._look2[0] = look[0];
this._look2[1] = look[1];
this._look2[2] = look[2];
}
if (up) {
this._up2[0] = up[0];
this._up2[1] = up[1];
this._up2[2] = up[2];
}
}
if (flyToProjection) {
if (params.projection === "ortho" && camera.projection !== "ortho") {
this._projection2 = "ortho";
this._projMatrix1 = camera.projMatrix.slice();
this._projMatrix2 = camera.ortho.matrix.slice();
camera.projection = "customProjection";
}
if (params.projection === "perspective" && camera.projection !== "perspective") {
this._projection2 = "perspective";
this._projMatrix1 = camera.projMatrix.slice();
this._projMatrix2 = camera.perspective.matrix.slice();
camera.projection = "customProjection";
}
} else {
this._projection2 = null;
}
this.fire("started", params, true);
this._time1 = Date.now();
this._time2 = this._time1 + (params.duration ? params.duration * 1000 : this._duration);
this._flying = true; // False as soon as we stop
core.scheduleTask(this._update, this);
}
/**
* Jumps the {@link Scene}'s {@link Camera} to the given target.
*
* * When the target is a boundary, this CameraFlightAnimation will position the {@link Camera} at where the target fills most of the canvas.
* * When the target is an explicit {@link Camera} position, given as ````eye````, ````look```` and ````up```` vectors, then this CameraFlightAnimation will jump the {@link Camera} to that target.
*
* @param {*|Component} params Either a parameters object or a {@link Component} subtype that has a World-space AABB.
* @param {Number} [params.arc=0] Factor in range [0..1] indicating how much the {@link Camera#eye} will swing away from its {@link Camera#look} as it flies to the target.
* @param {Number|String|Component} [params.component] ID or instance of a component to fly to.
* @param {Number[]} [params.aabb] World-space axis-aligned bounding box (AABB) target to fly to.
* @param {Number[]} [params.eye] Position to fly the eye position to.
* @param {Number[]} [params.look] Position to fly the look position to.
* @param {Number[]} [params.up] Position to fly the up vector to.
* @param {String} [params.projection] Projection type to transition into. Can be any of the values of {@link Camera.projection}.
* @param {Number} [params.fitFOV] How much of field-of-view, in degrees, that a target {@link Entity} or its AABB should fill the canvas on arrival. Overrides {@link CameraFlightAnimation#fitFOV}.
* @param {Boolean} [params.fit] Whether to fit the target to the view volume. Overrides {@link CameraFlightAnimation#fit}.
*/
jumpTo(params) {
this._jumpTo(params);
}
_jumpTo(params) {
if (this._flying) {
this.stop();
}
const camera = this.scene.camera;
var aabb;
var componentId;
var newEye;
var newLook;
var newUp;
if (params.aabb) { // Boundary3D
aabb = params.aabb;
} else if (params.length === 6) { // AABB
aabb = params;
} else if (params.eye || params.look || params.up) { // Camera pose
newEye = params.eye;
newLook = params.look;
newUp = params.up;
} else { // Argument must be an instance or ID of a Component (subtype)
let component = params;
if (utils.isNumeric(component) || utils.isString(component)) {
componentId = component;
component = this.scene.components[componentId];
if (!component) {
this.error("Component not found: " + utils.inQuotes(componentId));
return;
}
}
aabb = component.aabb || this.scene.aabb;
}
const poi = params.poi;
if (aabb) {
if (aabb[3] <= aabb[0] || aabb[4] <= aabb[1] || aabb[5] <= aabb[2]) { // Don't fly to an empty boundary
return;
}
var diag = poi ? math.getAABB3DiagPoint(aabb, poi) : math.getAABB3Diag(aabb);
newLook = poi || math.getAABB3Center(aabb, newLook);
if (this._trail) {
math.subVec3(camera.look, newLook, newLookEyeVec);
} else {
math.subVec3(camera.eye, camera.look, newLookEyeVec);
}
math.normalizeVec3(newLookEyeVec);
let dist;
const fit = (params.fit !== undefined) ? params.fit : this._fit;
if (fit) {
dist = Math.abs((diag) / Math.tan((params.fitFOV || this._fitFOV) * math.DEGTORAD));
} else {
dist = math.lenVec3(math.subVec3(camera.eye, camera.look, tempVec3));
}
math.mulVec3Scalar(newLookEyeVec, dist);
camera.eye = math.addVec3(newLook, newLookEyeVec, tempVec3);
camera.look = newLook;
this.scene.camera.ortho.scale = diag * 1.1;
} else if (newEye || newLook || newUp) {
if (newEye) {
camera.eye = newEye;
}
if (newLook) {
camera.look = newLook;
}
if (newUp) {
camera.up = newUp;
}
}
if (params.projection) {
camera.projection = params.projection;
}
}
_update() {
if (!this._flying) {
return;
}
const time = Date.now();
let t = (time - this._time1) / (this._time2 - this._time1);
const stopping = (t >= 1);
if (t > 1) {
t = 1;
}
const tFlight = this.easing ? CameraFlightAnimation._ease(t, 0, 1, 1) : t;
const camera = this.scene.camera;
if (this._flyingEye || this._flyingLook) {
if (this._flyingEye) {
math.subVec3(camera.eye, camera.look, newLookEyeVec);
camera.eye = math.lerpVec3(tFlight, 0, 1, this._eye1, this._eye2, newEye);
camera.look = math.subVec3(newEye, newLookEyeVec, newLook);
} else if (this._flyingLook) {
camera.look = math.lerpVec3(tFlight, 0, 1, this._look1, this._look2, newLook);
camera.up = math.lerpVec3(tFlight, 0, 1, this._up1, this._up2, newUp);
}
} else if (this._flyingEyeLookUp) {
camera.eye = math.lerpVec3(tFlight, 0, 1, this._eye1, this._eye2, newEye);
camera.look = math.lerpVec3(tFlight, 0, 1, this._look1, this._look2, newLook);
camera.up = math.lerpVec3(tFlight, 0, 1, this._up1, this._up2, newUp);
}
if (this._projection2) {
const tProj = (this._projection2 === "ortho") ? CameraFlightAnimation._easeOutExpo(t, 0, 1, 1) : CameraFlightAnimation._easeInCubic(t, 0, 1, 1);
camera.customProjection.matrix = math.lerpMat4(tProj, 0, 1, this._projMatrix1, this._projMatrix2);
} else {
camera.ortho.scale = this._orthoScale1 + (t * (this._orthoScale2 - this._orthoScale1));
}
if (stopping) {
camera.ortho.scale = this._orthoScale2;
this.stop();
return;
}
core.scheduleTask(this._update, this); // Keep flying
}
static _ease(t, b, c, d) { // Quadratic easing out - decelerating to zero velocity http://gizma.com/easing
t /= d;
return -c * t * (t - 2) + b;
}
static _easeInCubic(t, b, c, d) {
t /= d;
return c * t * t * t + b;
}
static _easeOutExpo(t, b, c, d) {
return c * (-Math.pow(2, -10 * t / d) + 1) + b;
}
/**
* Stops an earlier flyTo, fires arrival callback.
*/
stop() {
if (!this._flying) {
return;
}
this._flying = false;
this._time1 = null;
this._time2 = null;
if (this._projection2) {
this.scene.camera.projection = this._projection2;
}
const callback = this._callback;
if (callback) {
this._callback = null;
if (this._callbackScope) {
callback.call(this._callbackScope);
} else {
callback();
}
}
this.fire("stopped", true, true);
}
/**
* Cancels an earlier flyTo without calling the arrival callback.
*/
cancel() {
if (!this._flying) {
return;
}
this._flying = false;
this._time1 = null;
this._time2 = null;
if (this._callback) {
this._callback = null;
}
this.fire("canceled", true, true);
}
/**
* Sets the flight duration, in seconds, when calling {@link CameraFlightAnimation#flyTo}.
*
* Stops any flight currently in progress.
*
* default value is ````0.5````.
*
* @param {Number} value New duration value.
*/
set duration(value) {
this._duration = value ? (value * 1000.0) : 500;
this.stop();
}
/**
* Gets the flight duration, in seconds, when calling {@link CameraFlightAnimation#flyTo}.
*
* default value is ````0.5````.
*
* @returns {Number} New duration value.
*/
get duration() {
return this._duration / 1000.0;
}
/**
* Sets if, when CameraFlightAnimation is flying to a boundary, it will always adjust the distance between the
* {@link Camera#eye} and {@link Camera#look} so as to ensure that the target boundary is always filling the view volume.
*
* When false, the eye will remain at its current distance from the look position.
*
* Default value is ````true````.
*
* @param {Boolean} value Set ````true```` to activate this behaviour.
*/
set fit(value) {
this._fit = value !== false;
}
/**
* Gets if, when CameraFlightAnimation is flying to a boundary, it will always adjust the distance between the
* {@link Camera#eye} and {@link Camera#look} so as to ensure that the target boundary is always filling the view volume.
*
* When false, the eye will remain at its current distance from the look position.
*
* Default value is ````true````.
*
* @returns {Boolean} value Set ````true```` to activate this behaviour.
*/
get fit() {
return this._fit;
}
/**
* Sets how much of the perspective field-of-view, in degrees, that a target {@link Entity#aabb} should
* fill the canvas when calling {@link CameraFlightAnimation#flyTo} or {@link CameraFlightAnimation#jumpTo}.
*
* Default value is ````45````.
*
* @param {Number} value New FOV value.
*/
set fitFOV(value) {
this._fitFOV = value || 45;
}
/**
* Gets how much of the perspective field-of-view, in degrees, that a target {@link Entity#aabb} should
* fill the canvas when calling {@link CameraFlightAnimation#flyTo} or {@link CameraFlightAnimation#jumpTo}.
*
* Default value is ````45````.
*
* @returns {Number} Current FOV value.
*/
get fitFOV() {
return this._fitFOV;
}
/**
* Sets if this CameraFlightAnimation to point the {@link Camera}
* in the direction that it is travelling when flying to a target after calling {@link CameraFlightAnimation#flyTo}.
*
* Default value is ````true````.
*
* @param {Boolean} value Set ````true```` to activate trailing behaviour.
*/
set trail(value) {
this._trail = !!value;
}
/**
* Gets if this CameraFlightAnimation points the {@link Camera}
* in the direction that it is travelling when flying to a target after calling {@link CameraFlightAnimation#flyTo}.
*
* Default value is ````true````.
*
* @returns {Boolean} True if trailing behaviour is active.
*/
get trail() {
return this._trail;
}
/**
* @private
*/
destroy() {
this.stop();
super.destroy();
}
}
export {CameraFlightAnimation};