Skip to content

Commit c49ce13

Browse files
Add Quaternion Rotation (#33)
* Add Quaternion Rotation * Remove unnecessary Mat4 allocation * Move and rename quaternion Mat4 extraction * Documentation * Quaternion dedicated data class * Fix Quaternion operator wrong return type * Handle multiple Euler rotations orders
1 parent ec6e582 commit c49ce13

File tree

6 files changed

+601
-18
lines changed

6 files changed

+601
-18
lines changed

README.md

+23
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,29 @@ You can also use the invoke operator to access elements in row-major mode with
187187
v = myMat4(2, 3) // equivalent to myMat4[2, 1]
188188
```
189189

190+
## Rotation types
191+
192+
Construct a Euler Angle Rotation Matrix using per axis angles in degrees
193+
194+
```kotlin
195+
rotationMatrix = rotation(d = Float3(y = 90.0f)) // rotation of 90° around y axis
196+
```
197+
198+
Construct a Euler Angle Rotation Matrix using axis direction and angle in degree
199+
200+
```kotlin
201+
rotationMatrix = rotation(axis = Float3(y = 1.0f), angle = 90.0f) // rotation of 90° around y axis
202+
```
203+
204+
Construct a Quaternion Rotation Matrix following the Hamilton convention.
205+
Assume the destination and local coordinate spaces are initially aligned, and the local coordinate
206+
space is then rotated counter-clockwise about a unit-length axis, k, by an angle, theta.
207+
208+
```kotlin
209+
rotationMatrix = rotation(quaternion = Float4(y = 1.0f, w = 1.0f)) // rotation of 90° around y axis
210+
```
211+
212+
190213
## Scalar APIs
191214

192215
The file `Scalar.kt` contains various helper methods to use common math operations

src/commonMain/kotlin/dev/romainguy/kotlin/math/Matrix.kt

+168-15
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ enum class MatrixColumn {
2424
X, Y, Z, W
2525
}
2626

27+
enum class RotationsOrder {
28+
XYZ, XZY, YXZ, YZX, ZXY, ZYX
29+
}
30+
2731
data class Mat2(
2832
var x: Float2 = Float2(x = 1.0f),
2933
var y: Float2 = Float2(y = 1.0f)) {
@@ -41,14 +45,14 @@ data class Mat2(
4145
fun identity() = Mat2()
4246
}
4347

44-
operator fun get(column: Int) = when(column) {
48+
operator fun get(column: Int) = when (column) {
4549
0 -> x
4650
1 -> y
4751
else -> throw IllegalArgumentException("column must be in 0..1")
4852
}
4953
operator fun get(column: Int, row: Int) = get(column)[row]
5054

51-
operator fun get(column: MatrixColumn) = when(column) {
55+
operator fun get(column: MatrixColumn) = when (column) {
5256
MatrixColumn.X -> x
5357
MatrixColumn.Y -> y
5458
else -> throw IllegalArgumentException("column must be X or Y")
@@ -124,15 +128,15 @@ data class Mat3(
124128
fun identity() = Mat3()
125129
}
126130

127-
operator fun get(column: Int) = when(column) {
131+
operator fun get(column: Int) = when (column) {
128132
0 -> x
129133
1 -> y
130134
2 -> z
131135
else -> throw IllegalArgumentException("column must be in 0..2")
132136
}
133137
operator fun get(column: Int, row: Int) = get(column)[row]
134138

135-
operator fun get(column: MatrixColumn) = when(column) {
139+
operator fun get(column: MatrixColumn) = when (column) {
136140
MatrixColumn.X -> x
137141
MatrixColumn.Y -> y
138142
MatrixColumn.Z -> z
@@ -263,7 +267,7 @@ data class Mat4(
263267
inline val upperLeft: Mat3
264268
get() = Mat3(x.xyz, y.xyz, z.xyz)
265269

266-
operator fun get(column: Int) = when(column) {
270+
operator fun get(column: Int) = when (column) {
267271
0 -> x
268272
1 -> y
269273
2 -> z
@@ -272,7 +276,7 @@ data class Mat4(
272276
}
273277
operator fun get(column: Int, row: Int) = get(column)[row]
274278

275-
operator fun get(column: MatrixColumn) = when(column) {
279+
operator fun get(column: MatrixColumn) = when (column) {
276280
MatrixColumn.X -> x
277281
MatrixColumn.Y -> y
278282
MatrixColumn.Z -> z
@@ -333,6 +337,8 @@ data class Mat4(
333337
x.w * v.x + y.w * v.y + z.w * v.z+ w.w * v.w
334338
)
335339

340+
fun toQuaternion() = quaternion(this)
341+
336342
fun toFloatArray() = floatArrayOf(
337343
x.x, y.x, z.x, w.x,
338344
x.y, y.y, z.y, w.y,
@@ -468,18 +474,90 @@ fun translation(t: Float3) = Mat4(w = Float4(t, 1.0f))
468474
fun translation(m: Mat4) = translation(m.translation)
469475

470476
fun rotation(m: Mat4) = Mat4(normalize(m.right), normalize(m.up), normalize(m.forward))
471-
fun rotation(d: Float3): Mat4 {
477+
478+
/**
479+
* Construct a rotation matrix from Euler angles using YPR around a specified order
480+
*
481+
* Uses intrinsic Tait-Bryan angles. This means that rotations are performed with respect to the
482+
* local coordinate system.
483+
* That is, for order 'XYZ', the rotation is first around the X axis (which is the same as the
484+
* world-X axis), then around local-Y (which may now be different from the world Y-axis),
485+
* then local-Z (which may be different from the world Z-axis)
486+
*
487+
* @param d Per axis Euler angles in degrees
488+
* Yaw, pitch, roll (YPR) are taken accordingly to the rotations order input.
489+
* @param order The order in which to apply rotations.
490+
* Default is [RotationsOrder.ZYX] which means that the object will first be rotated around its Z
491+
* axis, then its Y axis and finally its X axis.
492+
*
493+
* @return The rotation matrix
494+
*/
495+
fun rotation(d: Float3, order: RotationsOrder = RotationsOrder.ZYX): Mat4 {
472496
val r = transform(d, ::radians)
473-
val c = transform(r) { x -> cos(x) }
474-
val s = transform(r) { x -> sin(x) }
497+
return when(order) {
498+
RotationsOrder.XZY -> rotation(r.x, r.z, r.y)
499+
RotationsOrder.XYZ -> rotation(r.x, r.y, r.z)
500+
RotationsOrder.YXZ -> rotation(r.y, r.x, r.z)
501+
RotationsOrder.YZX -> rotation(r.y, r.z, r.x)
502+
RotationsOrder.ZYX -> rotation(r.z, r.y, r.x)
503+
RotationsOrder.ZXY -> rotation(r.z, r.x, r.y)
504+
}
505+
}
475506

476-
return Mat4.of(
477-
c.y * c.z, -c.x * s.z + s.x * s.y * c.z, s.x * s.z + c.x * s.y * c.z, 0.0f,
478-
c.y * s.z, c.x * c.z + s.x * s.y * s.z, -s.x * c.z + c.x * s.y * s.z, 0.0f,
479-
-s.y, s.x * c.y, c.x * c.y, 0.0f,
480-
0.0f, 0.0f, 0.0f, 1.0f
481-
)
507+
/**
508+
* Construct a rotation matrix from Euler yaw, pitch, roll around a specified order.
509+
*
510+
* @param roll about 1st rotation axis in radians. Z in case of ZYX order
511+
* @param pitch about 2nd rotation axis in radians. Y in case of ZYX order
512+
* @param yaw about 3rd rotation axis in radians. X in case of ZYX order
513+
* @param order The order in which to apply rotations.
514+
* Default is [RotationsOrder.ZYX] which means that the object will first be rotated around its Z
515+
* axis, then its Y axis and finally its X axis.
516+
*
517+
* @return The rotation matrix
518+
*/
519+
fun rotation(yaw: Float = 0.0f, pitch: Float = 0.0f, roll: Float = 0.0f, order: RotationsOrder = RotationsOrder.ZYX): Mat4 {
520+
val c1 = cos(yaw)
521+
val s1 = sin(yaw)
522+
val c2 = cos(pitch)
523+
val s2 = sin(pitch)
524+
val c3 = cos(roll)
525+
val s3 = sin(roll)
526+
527+
return when (order) {
528+
RotationsOrder.XZY -> Mat4.of(
529+
c2 * c3, -s2, c2 * s3, 0.0f,
530+
s1 * s3 + c1 * c3 * s2, c1 * c2, c1 * s2 * s3 - c3 * s1, 0.0f,
531+
c3 * s1 * s2 - c1 * s3, c2 * s1, c1 * c3 + s1 * s2 * s3, 0.0f,
532+
0.0f, 0.0f, 0.0f, 1.0f)
533+
RotationsOrder.XYZ -> Mat4.of(
534+
c2 * c3, -c2 * s3, s2, 0.0f,
535+
c1 * s3 + c3 * s1 * s2, c1 * c3 - s1 * s2 * s3, -c2 * s1, 0.0f,
536+
s1 * s3 - c1 * c3 * s2, c3 * s1 + c1 * s2 * s3, c1 * c2, 0.0f,
537+
0.0f, 0.0f, 0.0f, 1.0f)
538+
RotationsOrder.YXZ -> Mat4.of(
539+
c1 * c3 + s1 * s2 * s3, c3 * s1 * s2 - c1 * s3, c2 * s1, 0.0f,
540+
c2 * s3, c2 * c3, -s2, 0.0f,
541+
c1 * s2 * s3 - c3 * s1, c1 * c3 * s2 + s1 * s3, c1 * c2, 0.0f,
542+
0.0f, 0.0f, 0.0f, 1.0f)
543+
RotationsOrder.YZX -> Mat4.of(
544+
c1 * c2, s1 * s3 - c1 * c3 * s2, c3 * s1 + c1 * s2 * s3, 0.0f,
545+
s2, c2 * c3, -c2 * s3, 0.0f,
546+
-c2 * s1, c1 * s3 + c3 * s1 * s2, c1 * c3 - s1 * s2 * s3, 0.0f,
547+
0.0f, 0.0f, 0.0f, 1.0f)
548+
RotationsOrder.ZYX -> Mat4.of(
549+
c1 * c2, c1 * s2 * s3 - c3 * s1, s1 * s3 + c1 * c3 * s2, 0.0f,
550+
c2 * s1, c1 * c3 + s1 * s2 * s3, c3 * s1 * s2 - c1 * s3, 0.0f,
551+
-s2, c2 * s3, c2 * c3, 0.0f,
552+
0.0f, 0.0f, 0.0f, 1.0f)
553+
RotationsOrder.ZXY -> Mat4.of(
554+
c1 * c3 - s1 * s2 * s3, -c2 * s1, c1 * s3 + c3 * s1 * s2, 0.0f,
555+
c3 * s1 + c1 * s2 * s3, c1 * c2, s1 * s3 - c1 * c3 * s2, 0.0f,
556+
-c2 * s3, s2, c2 * c3, 0.0f,
557+
0.0f, 0.0f, 0.0f, 1.0f)
558+
}
482559
}
560+
483561
fun rotation(axis: Float3, angle: Float): Mat4 {
484562
val x = axis.x
485563
val y = axis.y
@@ -498,6 +576,81 @@ fun rotation(axis: Float3, angle: Float): Mat4 {
498576
)
499577
}
500578

579+
/**
580+
* Construct a Quaternion Rotation Matrix following the Hamilton convention
581+
*
582+
* Assume the destination and local coordinate spaces are initially aligned, and the local
583+
* coordinate space is then rotated counter-clockwise about a unit-length axis, k, by an angle,
584+
* theta.
585+
*/
586+
fun rotation(quaternion: Quaternion): Mat4 {
587+
val n = normalize(quaternion)
588+
return Mat4(
589+
Float4(
590+
1.0f - 2.0f * (n.y * n.y + n.z * n.z),
591+
2.0f * (n.x * n.y - n.z * n.w),
592+
2.0f * (n.x * n.z + n.y * n.w)
593+
),
594+
Float4(
595+
2.0f * (n.x * n.y + n.z * n.w),
596+
1.0f - 2.0f * (n.x * n.x + n.z * n.z),
597+
2.0f * (n.y * n.z - n.x * n.w)
598+
),
599+
Float4(
600+
2.0f * (n.x * n.z - n.y * n.w),
601+
2.0f * (n.y * n.z + n.x * n.w),
602+
1.0f - 2.0f * (n.x * n.x + n.y * n.y)
603+
)
604+
)
605+
}
606+
607+
/**
608+
* Extract Quaternion rotation from a Matrix
609+
*/
610+
fun quaternion(m: Mat4): Quaternion {
611+
val trace = m.x.x + m.y.y + m.z.z
612+
return normalize(
613+
when {
614+
trace > 0 -> {
615+
val s = sqrt(trace + 1.0f) * 2.0f
616+
Quaternion(
617+
(m.z.y - m.y.z) / s,
618+
(m.x.z - m.z.x) / s,
619+
(m.y.x - m.x.y) / s,
620+
0.25f * s
621+
)
622+
}
623+
m.x.x > m.y.y && m.x.x > m.z.z -> {
624+
val s = sqrt(1.0f + m.x.x - m.y.y - m.z.z) * 2.0f
625+
Quaternion(
626+
0.25f * s,
627+
(m.x.y + m.y.x) / s,
628+
(m.x.z + m.z.x) / s,
629+
(m.z.y - m.y.z) / s
630+
)
631+
}
632+
m.y.y > m.z.z -> {
633+
val s = sqrt(1.0f + m.y.y - m.x.x - m.z.z) * 2.0f
634+
Quaternion(
635+
(m.x.y + m.y.x) / s,
636+
0.25f * s,
637+
(m.y.z + m.z.y) / s,
638+
(m.x.z - m.z.x) / s
639+
)
640+
}
641+
else -> {
642+
val s = sqrt(1.0f + m.z.z - m.x.x - m.y.y) * 2.0f
643+
Quaternion(
644+
(m.y.x - m.x.y) / s,
645+
(m.x.z + m.z.x) / s,
646+
(m.y.z + m.z.y) / s,
647+
0.25f * s
648+
)
649+
}
650+
}
651+
)
652+
}
653+
501654
fun normal(m: Mat4) = scale(1.0f / Float3(length2(m.right), length2(m.up), length2(m.forward))) * m
502655

503656
fun lookAt(eye: Float3, target: Float3, up: Float3 = Float3(z = 1.0f)): Mat4 {

0 commit comments

Comments
 (0)