Skip to content

Commit

Permalink
feature: add eulerFromMat3
Browse files Browse the repository at this point in the history
  • Loading branch information
0b5vr committed Feb 5, 2022
1 parent ab1ae7e commit 06e7265
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 0 deletions.
11 changes: 11 additions & 0 deletions src/math/euler/EulerOrder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Note that this is **extrinsic** rotations (which is same as Blender, Maya, and Unity).
* Three.js uses intrinsic rotations so you have to reverse the order if you want to match the behavior with Three.js.
*/
export type EulerOrder =
| 'XYZ'
| 'XZY'
| 'YXZ'
| 'YZX'
| 'ZXY'
| 'ZYX';
42 changes: 42 additions & 0 deletions src/math/euler/eulerFromMat3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { clamp } from '../utils';
import { sanitizeAngle } from '../sanitizeAngle';
import { vecManhattanLength } from '../vec/vecManhattanLength';
import type { EulerOrder } from './EulerOrder';
import type { RawMatrix3 } from '../mat3/RawMatrix3';
import type { RawVector3 } from '../vec3/RawVector3';

/**
* Return a euler angles out of a matrix3.
* Make sure the input matrix is normalized.
*/
export function eulerFromMat3( m: RawMatrix3, order: EulerOrder ): RawVector3 {
const [ i, j, k, sign ] =
order === 'XYZ' ? [ 0, 1, 2, 1 ] :
order === 'XZY' ? [ 0, 2, 1, -1 ] :
order === 'YXZ' ? [ 1, 0, 2, -1 ] :
order === 'YZX' ? [ 1, 2, 0, 1 ] :
order === 'ZXY' ? [ 2, 0, 1, 1 ] :
[ 2, 1, 0, -1 ];

const result: RawVector3 = [ 0.0, 0.0, 0.0 ];

const c = m[ k + i * 3 ];
result[ j ] = -sign * Math.asin( clamp( c, -1.0, 1.0 ) );

if ( Math.abs( c ) < 0.999999 ) {
result[ i ] = sign * Math.atan2( m[ k + j * 3 ], m[ k * 4 ] );
result[ k ] = sign * Math.atan2( m[ j + i * 3 ], m[ i * 4 ] );
} else {
// "y is 90deg" cases
result[ i ] = sign * Math.atan2( -m[ j + k * 3 ], m[ j * 4 ] );
}

if ( vecManhattanLength( result ) > 1.5 * Math.PI ) {
// "two big revolutions" cases
result[ i ] = sanitizeAngle( result[ i ] + Math.PI );
result[ j ] = sanitizeAngle( Math.PI - result[ j ] );
result[ k ] = sanitizeAngle( result[ k ] + Math.PI );
}

return result;
}
2 changes: 2 additions & 0 deletions src/math/euler/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './eulerFromMat3';
export * from './EulerOrder';
85 changes: 85 additions & 0 deletions src/math/euler/tests/eulerFromMat3.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import '../../../tests/matchers/toBeCloseToArray';
import { RawMatrix3 } from '../../mat3/RawMatrix3';
import { eulerFromMat3 } from '../eulerFromMat3';

const mat3X45Y45Z45: RawMatrix3 = [
0.500, 0.500, -0.707,
-0.146, 0.854, 0.500,
0.854, -0.146, 0.500,
];

const mat3X30YN50Z40: RawMatrix3 = [
0.492, 0.413, 0.766,
-0.850, 0.417, 0.321,
-0.187, -0.809, 0.557,
];

const mat3XN150YN15Z170: RawMatrix3 = [
-0.951, 0.168, 0.259,
0.023, 0.875, -0.483,
-0.308, -0.453, -0.837,
];

const mat3X30Y90Z70: RawMatrix3 = [
0.000, 0.000, -1.000,
-0.643, 0.766, 0.000,
0.766, 0.643, 0.000,
];

const mat3X120YN90Z20: RawMatrix3 = [
0.000, 0.000, 1.000,
-0.643, -0.766, 0.000,
0.766, -0.643, 0.000,
];

describe( 'eulerFromMat3', () => {
it( 'returns an euler angles out of a matrix3 (XYZ)', () => {
const subject = eulerFromMat3( mat3X45Y45Z45, 'XYZ' );
expect( subject ).toBeCloseToArray( [ 0.785, 0.785, 0.785 ] );
} );

it( 'returns an euler angles out of a matrix3 (XZY)', () => {
const subject = eulerFromMat3( mat3X45Y45Z45, 'XZY' );
expect( subject ).toBeCloseToArray( [ 0.170, 0.955, 0.524 ] );
} );

it( 'returns an euler angles out of a matrix3 (YXZ)', () => {
const subject = eulerFromMat3( mat3X45Y45Z45, 'YXZ' );
expect( subject ).toBeCloseToArray( [ 0.524, 0.955, 0.170 ] );
} );

it( 'returns an euler angles out of a matrix3 (YZX)', () => {
const subject = eulerFromMat3( mat3X45Y45Z45, 'YZX' );
expect( subject ).toBeCloseToArray( [ 0.530, 1.041, 0.147 ] );
} );

it( 'returns an euler angles out of a matrix3 (ZXY)', () => {
const subject = eulerFromMat3( mat3X45Y45Z45, 'ZXY' );
expect( subject ).toBeCloseToArray( [ 0.147, 1.041, 0.530 ] );
} );

it( 'returns an euler angles out of a matrix3 (ZYX)', () => {
const subject = eulerFromMat3( mat3X45Y45Z45, 'ZYX' );
expect( subject ).toBeCloseToArray( [ 0.284, 1.023, 0.285 ] );
} );

it( 'performs a "y is negative" case', () => {
const subject = eulerFromMat3( mat3X30YN50Z40, 'XYZ' );
expect( subject ).toBeCloseToArray( [ 0.524, -0.873, 0.698 ] );
} );

it( 'performs a "two big revolutions" case (X and Z are big)', () => {
const subject = eulerFromMat3( mat3XN150YN15Z170, 'XYZ' );
expect( subject ).toBeCloseToArray( [ 0.524, -2.880, -0.174 ] );
} );

it( 'performs an edge case properly (y is 90deg)', () => {
const subject = eulerFromMat3( mat3X30Y90Z70, 'XYZ' );
expect( subject ).toBeCloseToArray( [ -0.698, 1.571, 0.0 ] );
} );

it( 'performs an edge case properly (y is -90deg)', () => {
const subject = eulerFromMat3( mat3X120YN90Z20, 'XYZ' );
expect( subject ).toBeCloseToArray( [ 2.443, -1.571, 0.0 ] );
} );
} );
2 changes: 2 additions & 0 deletions src/math/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './euler';
export * from './mat3';
export * from './mat4';
export * from './quat';
Expand All @@ -8,6 +9,7 @@ export * from './vec4';
export * from './Matrix4';
export * from './mod';
export * from './Quaternion';
export * from './sanitizeAngle';
export * from './utils';
export * from './Vector';
export * from './Vector3';
Expand Down
8 changes: 8 additions & 0 deletions src/math/sanitizeAngle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { mod } from './mod';

/**
* Enclose arbitrary angle (in radian) into [-π, π)
*/
export function sanitizeAngle( angle: number ): number {
return mod( angle + Math.PI, 2.0 * Math.PI ) - Math.PI;
}
19 changes: 19 additions & 0 deletions src/math/tests/sanitizeAngle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { sanitizeAngle } from '../sanitizeAngle';

describe( 'sanitizeAngle', () => {
it( 'should do nothing to 0deg', () => {
expect( sanitizeAngle( 0.0 ) ).toBeCloseTo( 0.0 );
} );

it( 'should do nothing to -90deg', () => {
expect( sanitizeAngle( -Math.PI / 2.0 ) ).toBeCloseTo( -Math.PI / 2.0 );
} );

it( 'should sanitize 210deg to -150deg', () => {
expect( sanitizeAngle( 3.665 ) ).toBeCloseTo( -2.618 );
} );

it( 'should sanitize 540deg to 0deg', () => {
expect( sanitizeAngle( 12.5664 ) ).toBeCloseTo( 0.0 );
} );
} );

0 comments on commit 06e7265

Please sign in to comment.