@woosh/meep-engine 2.33.70 → 2.33.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/geom/3d/eulerAnglesFromMatrix.js +115 -0
- package/core/geom/3d/topology/TopoMesh.js +1 -1
- package/core/geom/Vector3.js +2 -2
- package/core/geom/normalize_angle_rad.js +18 -0
- package/core/geom/normalize_angle_rad.spec.js +45 -0
- package/core/geom/v3_angle_between.js +22 -0
- package/engine/graphics/ecs/camera/topdown/TopDownCameraController.js +14 -24
- package/engine/graphics/ecs/camera/topdown/TopDownCameraController.spec.js +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
//
|
|
2
|
+
|
|
3
|
+
import { v2_magnitude } from "../Vector2.js";
|
|
4
|
+
|
|
5
|
+
function atan2(a, b) {
|
|
6
|
+
return Math.atan2(a, b);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
*
|
|
11
|
+
* @param {number[]} mat4
|
|
12
|
+
* @param {number} row_index
|
|
13
|
+
* @param {number} column_index
|
|
14
|
+
* @return {number}
|
|
15
|
+
*/
|
|
16
|
+
function coeff(mat4, row_index, column_index) {
|
|
17
|
+
return mat4[row_index + column_index * 4];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sin(a) {
|
|
21
|
+
return Math.sin(a);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function cos(a) {
|
|
25
|
+
return Math.cos(a);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const EIGEN_PI = Math.PI;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
*
|
|
32
|
+
* \returns the Euler-angles of the rotation matrix \c *this using the convention defined by the triplet (\a a0,\a a1,\a a2)
|
|
33
|
+
*
|
|
34
|
+
* Each of the three parameters \a a0,\a a1,\a a2 represents the respective rotation axis as an integer in {0,1,2}.
|
|
35
|
+
* For instance, in:
|
|
36
|
+
* \code Vector3f ea = mat.eulerAngles(2, 0, 2); \endcode
|
|
37
|
+
* "2" represents the z axis and "0" the x axis, etc. The returned angles are such that
|
|
38
|
+
* we have the following equality:
|
|
39
|
+
* \code
|
|
40
|
+
* mat == AngleAxisf(ea[0], Vector3f::UnitZ())
|
|
41
|
+
* * AngleAxisf(ea[1], Vector3f::UnitX())
|
|
42
|
+
* * AngleAxisf(ea[2], Vector3f::UnitZ()); \endcode
|
|
43
|
+
* This corresponds to the right-multiply conventions (with right hand side frames).
|
|
44
|
+
*
|
|
45
|
+
* The returned angles are in the ranges [0:pi]x[-pi:pi]x[-pi:pi].
|
|
46
|
+
|
|
47
|
+
* NOTE: ported from Eigen C++ library
|
|
48
|
+
* @see https://gitlab.com/libeigen/eigen/-/blob/master/Eigen/src/Geometry/EulerAngles.h
|
|
49
|
+
* @see https://stackoverflow.com/questions/11514063/extract-yaw-pitch-and-roll-from-a-rotationmatrix
|
|
50
|
+
* @param {number[]} res
|
|
51
|
+
* @param {number[]|Float32Array|mat4} m4
|
|
52
|
+
* @param {number} a0 axis index
|
|
53
|
+
* @param {number} a1
|
|
54
|
+
* @param {number} a2
|
|
55
|
+
*/
|
|
56
|
+
export function eulerAnglesFromMatrix(res, m4, a0, a1, a2) {
|
|
57
|
+
const odd = ((a0 + 1) % 3 === a1) ? 0 : 1;
|
|
58
|
+
|
|
59
|
+
const i = a0;
|
|
60
|
+
const j = (a0 + 1 + odd) % 3;
|
|
61
|
+
const k = (a0 + 2 - odd) % 3;
|
|
62
|
+
|
|
63
|
+
if (a0 === a2) {
|
|
64
|
+
res[0] = atan2(coeff(m4, j, i), coeff(m4, k, i));
|
|
65
|
+
if ((odd && res[0] < 0) || ((!odd) && res[0] > 0)) {
|
|
66
|
+
if (res[0] > 0) {
|
|
67
|
+
res[0] -= EIGEN_PI;
|
|
68
|
+
} else {
|
|
69
|
+
res[0] += EIGEN_PI;
|
|
70
|
+
}
|
|
71
|
+
const s2 = v2_magnitude(coeff(m4, j, i), coeff(m4, k, i));
|
|
72
|
+
res[1] = -atan2(s2, coeff(m4, i, i));
|
|
73
|
+
} else {
|
|
74
|
+
const s2 = v2_magnitude(coeff(m4, j, i), coeff(m4, k, i));
|
|
75
|
+
res[1] = atan2(s2, coeff(m4, i, i));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// With a=(0,1,0), we have i=0; j=1; k=2, and after computing the first two angles,
|
|
79
|
+
// we can compute their respective rotation, and apply its inverse to M. Since the result must
|
|
80
|
+
// be a rotation around x, we have:
|
|
81
|
+
//
|
|
82
|
+
// c2 s1.s2 c1.s2 1 0 0
|
|
83
|
+
// 0 c1 -s1 * M = 0 c3 s3
|
|
84
|
+
// -s2 s1.c2 c1.c2 0 -s3 c3
|
|
85
|
+
//
|
|
86
|
+
// Thus: m11.c1 - m21.s1 = c3 & m12.c1 - m22.s1 = s3
|
|
87
|
+
|
|
88
|
+
const s1 = sin(res[0]);
|
|
89
|
+
const c1 = cos(res[0]);
|
|
90
|
+
res[2] = atan2(c1 * coeff(m4, j, k) - s1 * coeff(m4, k, k), c1 * coeff(m4, j, j) - s1 * coeff(m4, k, j));
|
|
91
|
+
} else {
|
|
92
|
+
res[0] = atan2(coeff(m4, j, k), coeff(m4, k, k));
|
|
93
|
+
const c2 = v2_magnitude(coeff(m4, i, i), coeff(m4, i, j));
|
|
94
|
+
if ((odd && res[0] < 0) || ((!odd) && res[0] > 0)) {
|
|
95
|
+
if (res[0] > 0) {
|
|
96
|
+
res[0] -= EIGEN_PI;
|
|
97
|
+
} else {
|
|
98
|
+
res[0] += EIGEN_PI;
|
|
99
|
+
}
|
|
100
|
+
res[1] = atan2(-coeff(m4, i, k), -c2);
|
|
101
|
+
} else
|
|
102
|
+
res[1] = atan2(-coeff(m4, i, k), c2);
|
|
103
|
+
const s1 = sin(res[0]);
|
|
104
|
+
const c1 = cos(res[0]);
|
|
105
|
+
res[2] = atan2(s1 * coeff(m4, k, i) - c1 * coeff(m4, j, i), c1 * coeff(m4, j, j) - s1 * coeff(m4, k, j));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!odd) {
|
|
109
|
+
// invert result
|
|
110
|
+
res[0] = -res[0];
|
|
111
|
+
res[1] = -res[1];
|
|
112
|
+
res[2] = -res[2];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
}
|
|
@@ -482,7 +482,7 @@ export class TopoMesh {
|
|
|
482
482
|
assert.equal(a.isTopoVertex, true, 'a.isTopoVertex !== true');
|
|
483
483
|
assert.equal(b.isTopoVertex, true, 'b.isTopoVertex !== true');
|
|
484
484
|
|
|
485
|
-
assert.notEqual(a, b, 'a === b, attempting to create an edge from vertex to itself')
|
|
485
|
+
assert.notEqual(a, b, 'a === b, attempting to create an edge from vertex to itself');
|
|
486
486
|
|
|
487
487
|
const aEdges = a.edges;
|
|
488
488
|
const n = aEdges.length;
|
package/core/geom/Vector3.js
CHANGED
|
@@ -11,7 +11,7 @@ import { sign } from "../math/sign.js";
|
|
|
11
11
|
import { v3_dot } from "./v3_dot.js";
|
|
12
12
|
import { v3_length_sqr } from "./v3_length_sqr.js";
|
|
13
13
|
import { v3_length } from "./v3_length.js";
|
|
14
|
-
import { v3_angle_between } from "./v3_angle_between.js";
|
|
14
|
+
import { v3_angle_between, v3_angle_between_atan2 } from "./v3_angle_between.js";
|
|
15
15
|
import { v3_lerp } from "./v3_lerp.js";
|
|
16
16
|
import { v3_slerp } from "./v3_slerp.js";
|
|
17
17
|
import { computeHashFloat } from "../math/hash/computeHashFloat.js";
|
|
@@ -448,7 +448,7 @@ class Vector3 {
|
|
|
448
448
|
* @returns {number}
|
|
449
449
|
*/
|
|
450
450
|
angleTo(other) {
|
|
451
|
-
return
|
|
451
|
+
return v3_angle_between_atan2(this.x, this.y, this.z, other.x, other.y, other.z);
|
|
452
452
|
}
|
|
453
453
|
|
|
454
454
|
/**
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { PI2 } from "../math/MathUtils.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Force angle to [-PI, +PI] range
|
|
5
|
+
* based on answer https://stackoverflow.com/a/2323034/3973586
|
|
6
|
+
* @param {number} angle in radians
|
|
7
|
+
* @return {number} in radians
|
|
8
|
+
*/
|
|
9
|
+
export function normalize_angle_rad(angle) {
|
|
10
|
+
let r = angle % PI2;
|
|
11
|
+
r = (r + PI2) % PI2;
|
|
12
|
+
|
|
13
|
+
if (r > Math.PI) {
|
|
14
|
+
r -= PI2;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return r;
|
|
18
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { normalize_angle_rad } from "./normalize_angle_rad.js";
|
|
2
|
+
|
|
3
|
+
test('0', () => {
|
|
4
|
+
expect(normalize_angle_rad(0)).toBe(0);
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
test('PI', () => {
|
|
8
|
+
expect(normalize_angle_rad(Math.PI)).toBe(Math.PI);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('-PI', () => {
|
|
12
|
+
expect(normalize_angle_rad(-Math.PI)).toBeCloseTo(Math.PI);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('2*PI', () => {
|
|
16
|
+
expect(normalize_angle_rad(Math.PI * 2)).toBeCloseTo(0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('-2*PI', () => {
|
|
20
|
+
expect(normalize_angle_rad(-Math.PI * 2)).toBeCloseTo(0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('-1', () => {
|
|
24
|
+
expect(normalize_angle_rad(-1)).toBeCloseTo(-1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('-3', () => {
|
|
28
|
+
expect(normalize_angle_rad(-3)).toBeCloseTo(-3);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('1', () => {
|
|
32
|
+
expect(normalize_angle_rad(1)).toBeCloseTo(1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('3', () => {
|
|
36
|
+
expect(normalize_angle_rad(3)).toBeCloseTo(3);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('2*PI + 1', () => {
|
|
40
|
+
expect(normalize_angle_rad(Math.PI * 2 + 1)).toBeCloseTo(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('- ( 2*PI + 1)', () => {
|
|
44
|
+
expect(normalize_angle_rad(-(Math.PI * 2 + 1))).toBeCloseTo(-1);
|
|
45
|
+
});
|
|
@@ -29,3 +29,25 @@ export function v3_angle_between(x0, y0, z0, x1, y1, z1) {
|
|
|
29
29
|
|
|
30
30
|
return Math.acos(theta);
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
*
|
|
35
|
+
* @param {number} x0
|
|
36
|
+
* @param {number} y0
|
|
37
|
+
* @param {number} z0
|
|
38
|
+
* @param {number} x1
|
|
39
|
+
* @param {number} y1
|
|
40
|
+
* @param {number} z1
|
|
41
|
+
* @returns {number}
|
|
42
|
+
*/
|
|
43
|
+
export function v3_angle_between_atan2(x0, y0, z0, x1, y1, z1) {
|
|
44
|
+
const cx = y0 * z1 - z0 * y1;
|
|
45
|
+
const cy = z0 * x1 - x0 * z1;
|
|
46
|
+
const cz = x0 * y1 - y0 * x1;
|
|
47
|
+
|
|
48
|
+
const cross_length = v3_length(cx, cy, cz);
|
|
49
|
+
|
|
50
|
+
const d = v3_dot(x0, y0, z0, x1, y1, z1);
|
|
51
|
+
|
|
52
|
+
return Math.atan2(cross_length, d);
|
|
53
|
+
}
|
|
@@ -7,29 +7,15 @@ import Vector3 from '../../../../../core/geom/Vector3.js';
|
|
|
7
7
|
import { assert } from "../../../../../core/assert.js";
|
|
8
8
|
import { lerp } from "../../../../../core/math/lerp.js";
|
|
9
9
|
import { Euler, Matrix4 } from "three";
|
|
10
|
-
import {
|
|
10
|
+
import { normalize_angle_rad } from "../../../../../core/geom/normalize_angle_rad.js";
|
|
11
|
+
import { eulerAnglesFromMatrix } from "../../../../../core/geom/3d/eulerAnglesFromMatrix.js";
|
|
11
12
|
|
|
12
13
|
const m4 = new Matrix4();
|
|
13
14
|
const euler = new Euler();
|
|
15
|
+
const angles = [];
|
|
14
16
|
|
|
15
17
|
const panOffset = new Vector3();
|
|
16
18
|
|
|
17
|
-
/**
|
|
18
|
-
* Force angle to [-PI, +PI] range
|
|
19
|
-
* based on answer https://stackoverflow.com/a/2323034/3973586
|
|
20
|
-
* @param {number} angle in radians
|
|
21
|
-
* @return {number} in radians
|
|
22
|
-
*/
|
|
23
|
-
function normalize_angle(angle) {
|
|
24
|
-
let r = angle % PI2;
|
|
25
|
-
r = (r + PI2) % PI2;
|
|
26
|
-
|
|
27
|
-
if (r > Math.PI) {
|
|
28
|
-
r -= PI2;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return r;
|
|
32
|
-
}
|
|
33
19
|
|
|
34
20
|
/**
|
|
35
21
|
*
|
|
@@ -40,11 +26,14 @@ function normalize_angle(angle) {
|
|
|
40
26
|
*/
|
|
41
27
|
export function rotate_from_view(displacement_x, displacement_y, input, output) {
|
|
42
28
|
// crop yaw and pitch to safe range of -PI to PI
|
|
43
|
-
const i_pitch =
|
|
44
|
-
const i_yaw =
|
|
29
|
+
const i_pitch = normalize_angle_rad(input.pitch);
|
|
30
|
+
const i_yaw = normalize_angle_rad(input.yaw);
|
|
31
|
+
const i_roll = normalize_angle_rad(input.roll);
|
|
32
|
+
|
|
33
|
+
const d_roll = (i_roll >= Math.PI || i_roll <= -Math.PI) ? -1 : 1;
|
|
45
34
|
|
|
46
35
|
// flip rotation axis when pitch is over 90deg in either direction, that is UP vector is pointing down
|
|
47
|
-
const d_yaw = ((i_pitch > Math.PI || i_pitch < -Math.PI) ? -1 : 1) * displacement_x;
|
|
36
|
+
const d_yaw = d_roll * ((i_pitch > Math.PI || i_pitch < -Math.PI) ? -1 : 1) * displacement_x;
|
|
48
37
|
|
|
49
38
|
output.yaw = i_yaw + d_yaw;
|
|
50
39
|
output.pitch = i_pitch - displacement_y;
|
|
@@ -138,17 +127,18 @@ class TopDownCameraController {
|
|
|
138
127
|
* @param {Vector3} [up]
|
|
139
128
|
*/
|
|
140
129
|
setFromEyeAndTarget(eye, target, up = Vector3.up) {
|
|
130
|
+
|
|
141
131
|
/*
|
|
142
132
|
NOTE: I'm not sure why, but three.js camera points in the opposite direction to normal objects
|
|
143
133
|
See: https://github.com/mrdoob/three.js/blob/412b99a7f26e117ea97f40eb53d010ab81aa3279/src/core/Object3D.js#L282
|
|
144
134
|
*/
|
|
145
135
|
m4.lookAt(target, eye, up);
|
|
146
136
|
|
|
147
|
-
|
|
137
|
+
eulerAnglesFromMatrix(angles, m4.elements, 2, 1, 0)
|
|
148
138
|
|
|
149
|
-
this.pitch =
|
|
150
|
-
this.yaw =
|
|
151
|
-
this.roll =
|
|
139
|
+
this.pitch = angles[2];
|
|
140
|
+
this.yaw = angles[1];
|
|
141
|
+
this.roll = angles[0];
|
|
152
142
|
|
|
153
143
|
this.distance = eye.distanceTo(target);
|
|
154
144
|
|
|
@@ -11,6 +11,15 @@ test('setFromEyeAndTarget does not introduce roll when UP is aligned with +Y', (
|
|
|
11
11
|
expect(ut.roll).toBe(0);
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
+
test('setFromEyeAndTarget, with different directional axial combinations', () => {
|
|
15
|
+
const ut = new TopDownCameraController();
|
|
16
|
+
|
|
17
|
+
ut.setFromEyeAndTarget(new Vector3(0, 1, -1), new Vector3(0, 0, 0), new Vector3(0, 1, 0));
|
|
18
|
+
ut.setFromEyeAndTarget(new Vector3(0, 1, -1), new Vector3(0, 0, 0), new Vector3(0, 1, 0));
|
|
19
|
+
|
|
20
|
+
expect(ut.roll).toBe(0);
|
|
21
|
+
});
|
|
22
|
+
|
|
14
23
|
test('setFromEyeAndTarget results in roll respecting UP vector', () => {
|
|
15
24
|
const eye = new Vector3();
|
|
16
25
|
const target = new Vector3();
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"productName": "Meep",
|
|
6
6
|
"description": "production-ready JavaScript game engine based on Entity Component System Architecture",
|
|
7
7
|
"author": "Alexander Goldring",
|
|
8
|
-
"version": "2.33.
|
|
8
|
+
"version": "2.33.74",
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"gl-matrix": "3.4.3",
|
|
11
11
|
"fast-levenshtein": "2.0.6",
|