@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
268 lines (209 loc) • 7.57 kB
JavaScript
import { Matrix4 } from "three";
import { assert } from "../../../../../core/assert.js";
import { eulerAnglesFromMatrix } from "../../../../../core/geom/3d/mat4/eulerAnglesFromMatrix.js";
import { normalize_angle_rad } from "../../../../../core/geom/normalize_angle_rad.js";
import Quaternion from "../../../../../core/geom/Quaternion.js";
import Vector3 from '../../../../../core/geom/Vector3.js';
import { lerp } from "../../../../../core/math/lerp.js";
import { max2 } from "../../../../../core/math/max2.js";
import { sign_not_zero } from "../../../../../core/math/sign_not_zero.js";
const m4 = new Matrix4();
const angles = [];
const panOffset = new Vector3();
/**
*
* @param {number} displacement_x
* @param {number} displacement_y
* @param {TopDownCameraController} input source thing being rotated
* @param {TopDownCameraController} output target thing where the result is written to
*/
export function rotate_from_view(displacement_x, displacement_y, input, output) {
// crop yaw and pitch to safe range of -PI to PI
const i_pitch = normalize_angle_rad(input.pitch);
const i_yaw = normalize_angle_rad(input.yaw);
const i_roll = normalize_angle_rad(input.roll);
const quaternion = Quaternion.fromEulerAngles(i_pitch, i_yaw, i_roll);
const forward = Vector3.up.clone();
forward.applyQuaternion(quaternion);
// flip rotation axis when pitch is over 90deg in either direction, that is UP vector is pointing down
const d_yaw = sign_not_zero(forward.y) * displacement_x;
output.yaw = i_yaw + d_yaw;
output.pitch = i_pitch - displacement_y;
}
/**
* pass in distance in world space to move left
* @param {number} distance
* @param {Object3D} object
* @param {Vector3} result
* @param {Vector3} multiplier
*/
function panLeft(distance, object, result, multiplier) {
const te = object.matrix.elements;
// get X column of matrix
panOffset.set(te[0], te[1], te[2]);
panOffset.multiplyScalar(-distance);
//result.add(panOffset);
result._sub(panOffset.x * multiplier.x, panOffset.y * multiplier.y, panOffset.z * multiplier.z);
}
/**
* pass in distance in world space to move up
* @param {number} distance
* @param {Object3D} object
* @param {Vector3} result
* @param {Vector3} multiplier
*/
function panUp(distance, object, result, multiplier) {
const te = object.matrix.elements;
// get Y column of matrix
panOffset.set(te[4], te[5], te[6]);
panOffset.multiplyScalar(distance);
//result.add(panOffset);
result._sub(panOffset.x * multiplier.x, panOffset.y * multiplier.y, panOffset.z * multiplier.z);
}
const deg2_in_rad = Math.PI / 360;
/**
* main entry point; pass in Vector2 of change desired in pixel space, right and down are positive
* @param {Vector2} delta
* @param {Object3D} object
* @param {Element} element
* @param {number} targetDistance
* @param {number} fov
* @param {Vector3} result
* @param {Vector3} multiplier
*/
function pan(delta, object, element, targetDistance, fov, result, multiplier = Vector3.one) {
assert.isNumber(fov, 'fov');
// half of the fov is center to top of screen
const d = targetDistance * Math.tan(fov * deg2_in_rad);
// NOTE: we actually don't use screenWidth, since perspective camera is fixed to screen height
const m = 2 * d / element.clientHeight;
panLeft(delta.x * m, object, result, multiplier);
panUp(delta.y * m, object, result, multiplier);
}
class TopDownCameraController {
constructor(options) {
this.target = new Vector3();
this.pitch = 0;
this.yaw = 0;
this.roll = 0;
this.distance = 0;
this.distanceMin = 1;
this.distanceMax = 200;
//
if (options !== void 0) {
this.fromJSON(options);
}
}
/**
* Set target, rotation and distance from 2 vectors
* @param {Vector3} eye
* @param {Vector3} target
* @param {Vector3} [up]
*/
setFromEyeAndTarget(eye, target, up = Vector3.up) {
/*
NOTE: I'm not sure why, but three.js camera points in the opposite direction to normal objects
See: https://github.com/mrdoob/three.js/blob/412b99a7f26e117ea97f40eb53d010ab81aa3279/src/core/Object3D.js#L282
*/
m4.lookAt(target, eye, up);
eulerAnglesFromMatrix(angles, m4.elements, 2, 1, 0)
this.pitch = angles[2];
this.yaw = angles[1];
this.roll = angles[0];
this.distance = eye.distanceTo(target);
// ensure we can maintain this distance
this.distanceMax = max2(this.distance, this.distanceMax);
this.target.copy(target);
}
/**
*
* @param {TopDownCameraController} other
* @returns {boolean}
*/
equals(other) {
return this.target.equals(other.target)
&& this.pitch === other.pitch
&& this.yaw === other.yaw
&& this.roll === other.roll
&& this.distance === other.distance
&& this.distanceMin === other.distanceMin
&& this.distanceMax === other.distanceMax
;
}
/**
*
* @param {TopDownCameraController} other
*/
copy(other) {
this.target.copy(other.target);
this.pitch = other.pitch;
this.yaw = other.yaw;
this.roll = other.roll;
this.distance = other.distance;
this.distanceMax = other.distanceMax;
this.distanceMin = other.distanceMin;
}
/**
*
* @returns {TopDownCameraController}
*/
clone() {
const r = new TopDownCameraController();
r.copy(this);
return r;
}
/**
*
* @param {TopDownCameraController} a
* @param {TopDownCameraController} b
* @param {number} t
*/
lerp(a, b, t) {
this.target.lerpVectors(a.target, b.target, t);
this.pitch = lerp(a.pitch, b.pitch, t);
this.yaw = lerp(a.yaw, b.yaw, t);
this.roll = lerp(a.roll, b.roll, t);
this.distance = lerp(a.distance, b.distance, t);
this.distanceMin = lerp(a.distanceMin, b.distanceMin, t);
this.distanceMax = lerp(a.distanceMax, b.distanceMax, t);
}
toJSON() {
return {
target: this.target.toJSON(),
pitch: this.pitch,
yaw: this.yaw,
roll: this.roll,
distance: this.distance,
distanceMin: this.distanceMin,
distanceMax: this.distanceMax
};
}
fromJSON(
{
distance = 0,
distanceMin = 0,
distanceMax = 0,
pitch = 0,
yaw = 0,
roll = 0,
target = Vector3.zero,
}
) {
this.distance = distance;
this.distanceMin = distanceMin;
this.distanceMax = distanceMax;
this.pitch = pitch;
this.roll = roll;
this.yaw = yaw;
this.target.fromJSON(target);
}
static fromJSON(j) {
const r = new TopDownCameraController();
r.fromJSON(j);
return r;
}
}
TopDownCameraController.typeName = "TopDownCameraController";
TopDownCameraController.pan = pan;
TopDownCameraController.rotate = rotate_from_view;
export default TopDownCameraController;