Skip to content

Commit d19d86f

Browse files
markuswntrstephentyrone
authored andcommitted
Add transformation representations
1 parent 69554d0 commit d19d86f

File tree

3 files changed

+574
-0
lines changed

3 files changed

+574
-0
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Transformation
2+
3+
`Rotation.swift` encapsulates an API for working with other forms of rotation representations, such as *Angle/Axis*, *Polar* or *Rotation Vector*. The API provides conversion from these representations to `Quaternion` and vice versa. Additionally, the API provides a method to directly rotate an arbitrary vector by a quaternion and thus avoids the calculation of an intermediate representation to any other form in the process.
4+
5+
## Policies
6+
- zero and non-finite quaternions have an indeterminate angle and axis. Thus,
7+
the `angle` property of `.zero` or `.infinity` is `RealType.nan`, and the
8+
`axis` property of `.zero` or `.infinity` is `.nan` in all lanes.
9+
- Quaternions with `angle == .zero` have an indeterminate axis. Thus, the
10+
`axis` property is `.nan` in all lanes.
Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
//===--- Transformation.swift ---------------------------------*- swift -*-===//
2+
//
3+
// This source file is part of the Swift Numerics open source project
4+
//
5+
// Copyright (c) 2020 Apple Inc. and the Swift Numerics project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
extension Quaternion {
13+
/// The [rotation angle][wiki] of the Angle-Axis representation.
14+
///
15+
/// Returns the rotation angle about the rotation *axis* in radians
16+
/// within *[0, 2π]* range.
17+
///
18+
/// Edge cases:
19+
/// -
20+
/// - If the quaternion is zero or non-finite, angle is `nan`.
21+
///
22+
/// See also:
23+
/// -
24+
/// - `.axis`
25+
/// - `.angleAxis`
26+
/// - `.polar`
27+
/// - `.rotationVector`
28+
/// - `init(angle:axis:)`
29+
/// - `init(length:angle:axis)`
30+
/// - `init(rotation:)`
31+
///
32+
/// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Recovering_the_axis-angle_representation
33+
@inlinable
34+
public var angle: RealType {
35+
2 * halfAngle
36+
}
37+
38+
/// The [rotation axis][wiki] of the Angle-Axis representation.
39+
///
40+
/// Returns the *(x,y,z)* rotation axis encoded in the quaternion
41+
/// as SIMD3 vector of unit length.
42+
///
43+
/// Edge cases:
44+
/// -
45+
/// - If the quaternion is zero or non-finite, axis is `nan` in all lanes.
46+
/// - If the rotation angle is zero, axis is `nan` in all lanes.
47+
///
48+
/// See also:
49+
/// -
50+
/// - `.angle`
51+
/// - `.angleAxis`
52+
/// - `.polar`
53+
/// - `.rotationVector`
54+
/// - `init(angle:axis:)`
55+
/// - `init(length:angle:axis)`
56+
/// - `init(rotation:)`
57+
///
58+
/// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Recovering_the_axis-angle_representation
59+
@inlinable
60+
public var axis: SIMD3<RealType> {
61+
guard isFinite && imaginary != .zero && !real.isZero else {
62+
return SIMD3(repeating: .nan)
63+
}
64+
return imaginary / .sqrt(imaginary.lengthSquared)
65+
}
66+
67+
/// The [Angle-Axis][wiki] representation.
68+
///
69+
/// Returns the rotation angle in radians within *[0, 2π]* and the rotation
70+
/// axis as SIMD3 vector of unit length.
71+
///
72+
/// Edge cases:
73+
/// -
74+
/// - If the quaternion is zero or non-finite, angle and axis are `nan`.
75+
/// - If the angle is zero, axis is `nan` in all lanes.
76+
///
77+
/// See also:
78+
/// -
79+
/// - `.angle`
80+
/// - `.axis`
81+
/// - `.polar`
82+
/// - `.rotationVector`
83+
/// - `init(angle:axis:)`
84+
/// - `init(length:angle:axis)`
85+
/// - `init(rotation:)`
86+
///
87+
/// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Recovering_the_axis-angle_representation
88+
public var angleAxis: (angle: RealType, axis: SIMD3<RealType>) {
89+
(angle, axis)
90+
}
91+
92+
/// The [rotation vector][rotvector].
93+
///
94+
/// A rotation vector is a vector of same direction as the rotation axis,
95+
/// whose length is the rotation angle of an Angle-Axis representation. It
96+
/// is effectively the product of multiplying the rotation `axis` by the
97+
/// rotation `angle`. Rotation vectors are often called "scaled axis" — this
98+
/// is a different name for the same concept.
99+
///
100+
/// Edge cases:
101+
/// -
102+
/// - If the quaternion is zero or non-finite, the rotation vector is `nan`
103+
/// in all lanes.
104+
/// - If the rotation angle is zero, the rotation vector is `nan`
105+
/// in all lanes.
106+
///
107+
/// See also:
108+
/// -
109+
/// - `.angle`
110+
/// - `.axis`
111+
/// - `.angleAxis`
112+
/// - `init(angle:axis:)`
113+
/// - `init(length:angle:axis)`
114+
/// - `init(rotation:)`
115+
///
116+
/// [rotvector]: https://en.wikipedia.org/wiki/Axis–angle_representation#Rotation_vector
117+
@_transparent
118+
public var rotationVector: SIMD3<RealType> {
119+
axis * angle
120+
}
121+
122+
/// The [polar decomposition][wiki].
123+
///
124+
/// Returns the length of this quaternion, half rotation angle in radians of
125+
/// *[0, π]* range and the rotation axis as SIMD3 vector of unit length.
126+
///
127+
/// Edge cases:
128+
/// -
129+
/// - If the quaternion is zero, length is `.zero` and angle and axis
130+
/// are `nan`.
131+
/// - If the quaternion is non-finite, length is `.infinity` and angle and
132+
/// axis are `nan`.
133+
/// - For any length other than `.zero` or `.infinity`, if angle is zero, axis
134+
/// is `nan`.
135+
///
136+
/// See also:
137+
/// -
138+
/// - `.angle`
139+
/// - `.axis`
140+
/// - `.angleAxis`
141+
/// - `.rotationVector`
142+
/// - `init(angle:axis:)`
143+
/// - `init(length:angle:axis)`
144+
/// - `init(rotation:)`
145+
///
146+
/// [wiki]: https://en.wikipedia.org/wiki/Polar_decomposition#Quaternion_polar_decomposition
147+
public var polar: (length: RealType, halfAngle: RealType, axis: SIMD3<RealType>) {
148+
(length, halfAngle, axis)
149+
}
150+
151+
/// Creates a unit quaternion specified with [Angle-Axis][wiki] values.
152+
///
153+
/// Angle-Axis is a representation of a 3D rotation using two different
154+
/// quantities: an angle describing the magnitude of rotation, and a vector
155+
/// of unit length indicating the axis direction to rotate along.
156+
///
157+
/// This initializer reads given `angle` and `axis` values and creates a
158+
/// quaternion of equal rotation properties using the following equation:
159+
///
160+
/// Q = (cos(angle/2), axis * sin(angle/2))
161+
///
162+
/// Given `axis` gets normalized if it is not of unit length.
163+
///
164+
/// The final quaternion is of unit length.
165+
///
166+
/// Edge cases:
167+
/// -
168+
/// - For any `θ`, even `.infinity` or `.nan`:
169+
/// ```
170+
/// Quaternion(angle: θ, axis: .zero) == .zero
171+
/// ```
172+
/// - For any `θ`, even `.infinity` or `.nan`:
173+
/// ```
174+
/// Quaternion(angle: θ, axis: .infinity) == .ininfity
175+
/// ```
176+
/// - Otherwise, `θ` must be finite, or a precondition failure occurs.
177+
///
178+
/// See also:
179+
/// -
180+
/// - `.angle`
181+
/// - `.axis`
182+
/// - `.angleAxis`
183+
/// - `.rotationVector`
184+
/// - `.polar`
185+
/// - `init(rotation:)`
186+
/// - `init(length:angle:axis)`
187+
///
188+
/// - Parameter angle: The rotation angle about the rotation axis in radians
189+
/// - Parameter axis: The rotation axis
190+
///
191+
/// [wiki]: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Recovering_the_axis-angle_representation
192+
@inlinable
193+
public init(angle: RealType, axis: SIMD3<RealType>) {
194+
let length: RealType = .sqrt(axis.lengthSquared)
195+
if angle.isFinite && length.isNormal {
196+
self = Quaternion(halfAngle: angle/2, unitAxis: axis/length)
197+
} else {
198+
precondition(
199+
length.isZero || length.isInfinite,
200+
"Either angle must be finite, or axis length must be zero or infinite."
201+
)
202+
self = Quaternion(length)
203+
}
204+
}
205+
206+
/// Creates a unit quaternion specified with given [rotation vector][wiki].
207+
///
208+
/// A rotation vector is a vector of same direction as the rotation axis,
209+
/// whose length is the rotation angle of an Angle-Axis representation. It
210+
/// is effectively the product of multiplying the rotation `axis` by the
211+
/// rotation `angle`.
212+
///
213+
/// This initializer reads the angle and axis values of given rotation vector
214+
/// and creates a quaternion of equal rotation properties using the following
215+
/// equation:
216+
///
217+
/// Q = (cos(angle/2), axis * sin(angle/2))
218+
///
219+
/// Rotation vectors are sometimes referred to as *scaled axis* — this is a
220+
/// different name for the same concept.
221+
///
222+
/// The final quaternion is of unit length.
223+
///
224+
/// Edge cases:
225+
/// -
226+
/// - If `vector` is `.zero`, the quaternion is `.zero`:
227+
/// ```
228+
/// Quaternion(rotation: .zero) == .zero
229+
/// ```
230+
/// - If `vector` is `.infinity` or `-.infinity`, the quaternion is `.infinity`:
231+
/// ```
232+
/// Quaternion(rotation: -.infinity) == .infinity
233+
/// ```
234+
///
235+
/// See also:
236+
/// -
237+
/// - `.angle`
238+
/// - `.axis`
239+
/// - `.angleAxis`
240+
/// - `.polar`
241+
/// - `.rotationVector`
242+
/// - `init(angle:axis:)`
243+
/// - `init(length:angle:axis)`
244+
///
245+
/// - Parameter vector: The rotation vector.
246+
///
247+
/// [wiki]: https://en.wikipedia.org/wiki/Axis–angle_representation#Rotation_vector
248+
@inlinable
249+
public init(rotation vector: SIMD3<RealType>) {
250+
let angle: RealType = .sqrt(vector.lengthSquared)
251+
if !angle.isZero && angle.isFinite {
252+
self = Quaternion(halfAngle: angle/2, unitAxis: vector/angle)
253+
} else {
254+
self = Quaternion(angle)
255+
}
256+
}
257+
258+
/// Creates a quaternion specified with [polar coordinates][wiki].
259+
///
260+
/// This initializer reads given `length`, `halfAngle` and `axis` values and
261+
/// creates a quaternion of equal rotation properties and specified *length*
262+
/// using the following equation:
263+
///
264+
/// Q = (cos(halfAngle), axis * sin(halfAngle)) * length
265+
///
266+
/// Given `axis` gets normalized if it is not of unit length.
267+
///
268+
/// Edge cases:
269+
/// -
270+
/// - Negative lengths are interpreted as reflecting the point through the origin, i.e.:
271+
/// ```
272+
/// Quaternion(length: -r, angle: θ, axis: axis) == Quaternion(length: -r, angle: θ, axis: axis)
273+
/// ```
274+
/// - For any `θ` and any `axis`, even `.infinity` or `.nan`:
275+
/// ```
276+
/// Quaternion(length: .zero, angle: θ, axis: axis) == .zero
277+
/// ```
278+
/// - For any `θ` and any `axis`, even `.infinity` or `.nan`:
279+
/// ```
280+
/// Quaternion(length: .infinity, angle: θ, axis: axis) == .infinity
281+
/// ```
282+
/// - Otherwise, `θ` and `axis` must be finite, or a precondition failure occurs.
283+
///
284+
/// See also:
285+
/// -
286+
/// - `.angle`
287+
/// - `.axis`
288+
/// - `.angleAxis`
289+
/// - `.rotationVector`
290+
/// - `.polar`
291+
/// - `init(angle:axis)`
292+
/// - `init(rotation:)`
293+
///
294+
/// [wiki]: https://en.wikipedia.org/wiki/Polar_decomposition#Quaternion_polar_decomposition
295+
@inlinable
296+
public init(length: RealType, halfAngle: RealType, axis: SIMD3<RealType>) {
297+
let axisLength: RealType = .sqrt(axis.lengthSquared)
298+
if halfAngle.isFinite && axisLength.isNormal {
299+
self = Quaternion(
300+
halfAngle: halfAngle,
301+
unitAxis: axis/axisLength
302+
).multiplied(by: length)
303+
} else {
304+
precondition(
305+
length.isZero || length.isInfinite,
306+
"Either angle must be finite, or length must be zero or infinite."
307+
)
308+
self = Quaternion(length)
309+
}
310+
}
311+
}
312+
313+
// MARK: - Transformation Helper
314+
//
315+
// While Angle/Axis, Rotation Vector and Polar are different representations
316+
// of transformations, they have common properties such as being based on a
317+
// rotation *angle* about a rotation axis of unit length.
318+
//
319+
// The following extension provides these common operation internally.
320+
extension Quaternion {
321+
/// The half rotation angle in radians within *[0, π]* range.
322+
///
323+
/// Edge cases:
324+
/// -
325+
/// If the quaternion is zero or non-finite, halfAngle is `nan`.
326+
@usableFromInline @inline(__always)
327+
internal var halfAngle: RealType {
328+
guard !isZero && isFinite else { return .nan }
329+
return .atan2(y: .sqrt(imaginary.lengthSquared), x: real)
330+
}
331+
332+
/// Creates a new quaternion from given half rotation angle about given
333+
/// rotation axis.
334+
///
335+
/// The angle-axis values are transformed using the following equation:
336+
///
337+
/// Q = (cos(halfAngle), unitAxis * sin(halfAngle))
338+
///
339+
/// - Parameters:
340+
/// - halfAngle: The half rotation angle
341+
/// - unitAxis: The rotation axis of unit length
342+
@usableFromInline @inline(__always)
343+
internal init(halfAngle: RealType, unitAxis: SIMD3<RealType>) {
344+
self.init(.cos(halfAngle), unitAxis * .sin(halfAngle))
345+
}
346+
}
347+
348+
// MARK: - SIMD Helper
349+
//
350+
// Provides common vector operations on SIMD3 to ease the use of "imaginary"
351+
// and *(x,y,z)* axis representations internally to the module.
352+
extension SIMD3 where Scalar: FloatingPoint {
353+
354+
/// Returns the squared length of this SIMD3 instance.
355+
@usableFromInline @inline(__always)
356+
internal var lengthSquared: Scalar {
357+
(self * self).sum()
358+
}
359+
360+
/// Returns the vector/cross product of this quaternion with `other`.
361+
@usableFromInline @inline(__always)
362+
internal func vectorProduct(with other: SIMD3<Scalar>) -> SIMD3<Scalar> {
363+
let selfYZW = self[SIMD3<Int>(1,2,0)]
364+
let otherYZX = other[SIMD3<Int>(1,2,0)]
365+
let selfZXY = self[SIMD3<Int>(2,0,1)]
366+
let otherZXY = other[SIMD3<Int>(2,0,1)]
367+
return (selfYZW * otherZXY) - (selfZXY * otherYZX)
368+
}
369+
}

0 commit comments

Comments
 (0)