kopia lustrzana https://github.com/peterhinch/micropython-samples
470 wiersze
19 KiB
Markdown
470 wiersze
19 KiB
Markdown
# 1. 3D Rotation (quaternions without the maths)
|
|
|
|
This repo contains a `Quaternion` class and a rudimentary 3D vector graphics
|
|
module. The aim is to present quaternions as an efficient way to manipulate 3d
|
|
objects and data from inertial measurement units (IMUs). As such they have
|
|
applications in 3D graphics and robotics.
|
|
|
|
Quaternions have a reputation for being mathematically difficult. This repo
|
|
aims to make them usable with no mathematics beyond a basic grasp of Cartesian
|
|
(x, y, z) coordinates. They are actually easier to use than the traditional
|
|
Euler angles (heading, pitch and roll), and avoid the mathematical problems
|
|
associated with Euler angles.
|
|
|
|
The following fragment shows a classic demo where a wireframe cube rotates to
|
|
match rotations of an IMU:
|
|
|
|
```python
|
|
# dobj is a display list, imu is a BNo055
|
|
# Create a unit cube, scale to 0.8 and move to centre on origin
|
|
cube = Cube(RED, BLUE, GREEN) * ( 0.8, 0.8, 0.8) - (0.4, 0.4, 0.4)
|
|
rot = Rotator() # Rotation quaternion
|
|
while True:
|
|
rot[:] = imu.quaternion() # Read IMU and assign to Rotator
|
|
dobj['cube'] = cube @ rot # Rotate cube to match IMU
|
|
dobj.show() # Display on screen
|
|
time.sleep(0.1)
|
|
```
|
|
In this instance the IMU (a Bosch BNO055) produces quaternion data. Similar
|
|
data may be computed from other IMUs by means of the
|
|
[sensor fusion module](https://github.com/micropython-IMU/micropython-fusion).
|
|
|
|
Section 2 of this doc aims to describe basic usage of the repo with an absolute
|
|
minimum of mathematics. Section 3 provides more detail for those wishing to
|
|
expand the usage of quaternions beyond basic 3D transformations.
|
|
|
|
The 3D graphics is of demo quality: it is not a serious attempt at a graphics
|
|
library. Its speed demonstrates that a 3D graphics library should be written in
|
|
C for adequate performance. It also has known limitations and shortcomings. Its
|
|
purpose is to prove and demonstrate the quaternion handling: all graphics
|
|
elements use quaternions in their definitions.
|
|
|
|
# 2 Introduction
|
|
|
|
The graphics module enables various wireframe objects to be rendered from a
|
|
"camera" view with an illusion of perspective. Objects may be moved, scaled,
|
|
distorted and rotated using arithmetic operators. Thus if `cube` is an instance
|
|
of a `Cube`,
|
|
`cube + (0, 0.2, 0)` will move it along the Y axis.
|
|
`cube * (1, 0.2, 0.2)` will turn it into a cuboid.
|
|
`cube @ Rotator(0.1, 1, 0, 0)` will rotate it around the X axis by 0.1 radians.
|
|
Any axis may be specified.
|
|
|
|
I wrote this to experiment with and test `quat.py` rather than as a serious
|
|
attempt at 3D graphics. It is written in Python so speed is an issue. Realtime
|
|
display of orientation is feasible. Playable games aren't. A C port would be
|
|
awesome.
|
|
|
|
## 2.1 Files
|
|
|
|
The library provides the following files which should be copied to the target's
|
|
filesystem.
|
|
Core files:
|
|
1. `quat.py` Provides classes for creating and rotating points.
|
|
2. `graph3d.py` Provides wireframe 3D graphics classes.
|
|
|
|
Setup files. There should be a file `setup3d.py` defining graphics and
|
|
(optionally) IMU hardware:
|
|
1. `setup3d.py` Assumes an SSD1351 OLED display and BNo055 IMU.
|
|
2. `setup3d_lcd160cr` Setup file for the official LCD. On the target rename to
|
|
`setup3d.py` to use. Uncomment the BNo055 code if required.
|
|
|
|
Demo scripts:
|
|
1. `test3d.py` Test script displays various moving objects.
|
|
2. `test_imu.py` Wireframe cube moves to match IMU data.
|
|
|
|
Test:
|
|
1. `quat_test.py` Unit test for `quat.py`. Run under Unix build.
|
|
|
|
## 2.2 Display hardware
|
|
|
|
The `setup3d.py` script assumes an SSD1351 128*128 bit display such as
|
|
[this one](https://www.adafruit.com/product/1431) - it may be adapted for other
|
|
displays. The driver is [here](https://github.com/peterhinch/micropython-nano-gui/blob/master/drivers/ssd1351/ssd1351_16bit.py)
|
|
|
|
The setup file must define the following data values:
|
|
1. Color values in a format suitable for its `line` and `fill` functions (see
|
|
below).
|
|
2. `DIMENSION = 64` This defines the number of pixels moved by a value of 1.0.
|
|
By default a 128*128 pixel space represents values defined by -1.0 <= v <= 1.0.
|
|
3. `imu` Required for `test_imu`. An inertial measuring unit. It must have a
|
|
`quaternion` method returning `w`, `x`, `y`, `z`. It may also have an `euler`
|
|
method returning `heading`, `roll`, `pitch`. If `euler` is unsupported comment
|
|
out the line in the test script that reports on those values.
|
|
|
|
The setup file must define these functions:
|
|
1. `fill(color)` Clear the display when called with `BLACK`.
|
|
2. `line(xs, ys, xe, ye, color)` Draw a line with args specified in absolute
|
|
pixels.
|
|
3. `show()` Display all lines.
|
|
|
|
By default the `imu` object is a BNo055 however it can be created for other IMU
|
|
types by means of the
|
|
[sensor fusion module](https://github.com/micropython-IMU/micropython-fusion)
|
|
which provides access to quaternion, heading, pitch and roll.
|
|
|
|
The `test_imu` rotating cube demo uses the
|
|
[Adafruit BNO055 breakout](https://www.adafruit.com/product/2472). The driver
|
|
comprises these files:
|
|
[bno055.py](https://github.com/micropython-IMU/micropython-bno055/blob/master/bno055.py)
|
|
and [bno055_base.py](https://github.com/micropython-IMU/micropython-bno055/blob/master/bno055_base.py).
|
|
|
|
The `test3d.py` script ends by drawing a moving sphere. This is RAM-intensive
|
|
and will cause a memory error unless run on a Pyboard D.
|
|
|
|
## 2.3 Getting started
|
|
|
|
The module `quat.py` provides functions producing two types of object:
|
|
1. A `Point` representing a point (x, y, z) in 3D space.
|
|
2. A `Rotator` representing an axis and an angle of rotation.
|
|
|
|
The following fragment instantiates a point and rotates it 45° around the X
|
|
axis:
|
|
```python
|
|
from quat import Point, Rotator
|
|
from math import pi
|
|
point = Point(0, 0, 1)
|
|
rot = Rotator(pi/4, 1, 0, 0) # Rotate 45° around x axis
|
|
point @= rot # perform the rotation in-place
|
|
```
|
|
This can be extended to wireframe shapes, which can also be moved and scaled.
|
|
Moving is done by adding a 3-tuple representing `(dx, dy, dz)` distances to
|
|
move. Scaling may be done by multiplying by a value to make the object larger
|
|
or smaller while preserving its shape. Alternatively the object can be
|
|
multiplied by a 3-tuple to scale it differently along each axis:
|
|
```python
|
|
from quat import Rotator
|
|
from math import pi
|
|
from graph3d import Cube, DisplayDict, Axes
|
|
from setup3d import * # Hardware setup and colors
|
|
|
|
# Objects for display are put in a `DisplayDict` which specifies the camera
|
|
# location and distance for the perspective view
|
|
dobj = DisplayDict(setup(), pi/6, 5)
|
|
dobj['axes'] = Axes(WHITE) # Show x, y, z axes
|
|
# By default the cube is from the origin to (1.0, 1.0, 1.0). Scale it
|
|
# to length of side 0.8 and centre it at origin
|
|
cube = Cube(RED, BLUE, GREEN) * (0.8, 0.8, 0.8) - (0.4, 0.4, 0.4)
|
|
# Rotate 45° around an axis from (0, 0, 0) to (1, 1, 1)
|
|
rot = Rotator(pi/4, 1, 1, 1)
|
|
dobj['cube'] = cube @ rot
|
|
dobj.show() # Update the physical display
|
|
```
|
|
|
|
## 2.4 The quat module
|
|
|
|
This provides the following:
|
|
1. `Point` Represents a point in space. Constructor takes `x`, `y` and `z`
|
|
floating point args where `-1.0 <= arg <= 1.0`.
|
|
2. `Rotator` An object which can rotate `Point` instances around an axis. The
|
|
axis is defined by a line from the origin to a point. Constructor args:
|
|
`theta=0, x=0, y=0, z=0` where `theta` is the amount of rotation in radians.
|
|
3. `Euler` Returns a `Rotator` defined by Euler angles. Constructor args:
|
|
`heading, pitch, roll` specified in radians.
|
|
|
|
## 2.5 The 3D graphics module
|
|
|
|
Code in this module uses `Point` and `Rotator` instances to create graphics
|
|
primitives. Constructors create unit sized objects at a fixed location: objects
|
|
may be scaled, distorted, moved or rotated prior to display.
|
|
|
|
In all cases `color` is a color value defined in `setup3d.py`.
|
|
|
|
The module provides the following objects:
|
|
1. `Line` Constructor args: `p0, p1, color` where `p0` and `p1` are `Point`
|
|
instances.
|
|
2. `Axes` Draws the three axes. Constructor arg: `color`.
|
|
3. `Square` Draws a unit square located in the +ve quadrant. Arg: `color`.
|
|
4. `Circle` Unit circle centred on origin. Args: `color`, `segments=12`.
|
|
5. `Sphere` Unit sphere centred on origin. Args: `color`, `segments=12`.
|
|
6. `Cone` Unit cone with apex at origin. Args: `color`, `segments=12`.
|
|
7. `Cube` Unit cube in +ve quadrant. Args: `color, front=None, sides=None`.
|
|
These represent color values. If color values are passed it allows front, back
|
|
and sides to have different colors enabling orientation to readily be seen.
|
|
8. `DisplayDict` A dictionary of objects for display.
|
|
|
|
The `Sphere` object is demanding of RAM. I have only succeeded in displaying it
|
|
on Pyboard D hosts (SF2 and SF6).
|
|
|
|
### 2.5.1 The DisplayDict class
|
|
|
|
This enables objects to be transformed or deleted at run time, and provides for
|
|
display refresh.
|
|
|
|
Constructor:
|
|
This takes the following args: `ssd, angle, distance`. `ssd` is the display
|
|
instance. The other args specify the camera angle and distance. Typical values
|
|
are pi/6 and 5.
|
|
|
|
Method:
|
|
1. `show` Clears and refreshes the display.
|
|
|
|
# 3. Quaternions
|
|
|
|
The mathematics of quaternions are interesting. I don't propose to elaborate on
|
|
it here as there are many excellent online texts and demonstrations. The ones I
|
|
found helpful are listed [below](./quaternions.md#Appendix 1: references). The
|
|
following summarises the maths necessary to read the rest of this section; it
|
|
uses Python notation rather than trying to represent mathematical equations.
|
|
|
|
A quaternion comprises four numbers `w, x, y, z`. These define an instance
|
|
`q = w + x*i + y*j + z*k` where `i, j, k` are square roots of -1 as per
|
|
Hamilton's equation:
|
|
`i**2 == j**2 == k**2 == i*j*k == -1`
|
|
|
|
A quaternion's `w` value is its scalar part, with `x`, `y` and `z` defining its
|
|
vector part. A quaternion with `w == 0` is a vector quaternion and is created
|
|
by the `Vector` or `Point` constructors in the code. It may be regarded as a
|
|
point in space or a vector between the origin and that point, depending on
|
|
context.
|
|
|
|
Quaternions have a magnitude represented by
|
|
`magnitude = sqrt(sum((w*w + x*x + y*y + z*z)))` This may be accessed by
|
|
issuing `magnitude = abs(q)`.
|
|
|
|
A quaternion whose vector part is 0 (i.e. x, y, z are 0) is a scalar. If w is 1
|
|
it is known as a unit quaternion.
|
|
|
|
A quaternion with a nonzero vector part and a magnitude of 1 is a rotation
|
|
quaternion. These may be produced by the `Rotator` constructor. If its `x y z`
|
|
values are considered as a vector starting at the origin, `w` represents the
|
|
amount of rotation about that axis.
|
|
|
|
Multiplication of quaternions is not commutative: in general
|
|
`q1 * q2 != q2 * q1`
|
|
Multiplication of quaternions produces a Hamilton product.
|
|
|
|
Quaternions have a conjugate analogous to the conjugate of a complex. If `q` is
|
|
a quaternion its conjugate may be found with:
|
|
`conj = Quaternion(q.w, -q.x, -q.y, -q.z)` In code `conj = q.conjugate()`.
|
|
|
|
A `Point` (vector quaternion) `P` may be rotated around an arbitrary axis by
|
|
defining a rotation quaternion `R` and performing the following
|
|
`P = R * P * conjugate(P)`
|
|
This is performed using the matmul `@` operator, e.g.:
|
|
`P @= R`
|
|
The use of this operator allows rotations to be used in arithmetic expressions.
|
|
|
|
Division of quaternions is covered in [section 3.9](./QUATERNION.md#39-division).
|
|
|
|
## 3.1 The Quaternion class
|
|
|
|
This uses Python special ("dunder" or magic) methods to enable arithmetic and
|
|
comparison operators and to support the iterator protocol.
|
|
|
|
```python
|
|
q = Quaternion(5, 6, 7, 8)
|
|
q2 = q * 2
|
|
q2 = 2 * q # See NOTE on operand ordering
|
|
print(q * 2 + (20, 30, 40))
|
|
```
|
|
Special constructors are provided to produce vector and rotation quaternions,
|
|
with the class constructor enabling arbitrary quaternions to be created.
|
|
|
|
NOTE: Unless firmware is compiled with `MICROPY_PY_REVERSE_SPECIAL_METHODS`
|
|
arithmetic expressions should have the quaternion on the left hand side. Thus
|
|
`q * 2` will work, but `2 * q` will throw an exception on such builds. See
|
|
[build notes](./QUATERNION.md#313-build-notes).
|
|
|
|
## 3.2 Quaternion constructors
|
|
|
|
There are constructors producing vector and rotation quaternions. In addition
|
|
the actual class constructor enables any quaternion to be instantiated.
|
|
|
|
### 3.2.1 Vector or Point
|
|
|
|
These take 3 args `x, y, z`. The constructor returns a vector quaternion with
|
|
`w == 0`.
|
|
|
|
### 3.2.2 Rotator
|
|
|
|
This takes 4 args `theta=0, x=0, y=0, z=0`. It returns a rotation quaternion
|
|
with magnitude 1. The vector from the origin to `x, y, z` is the axis of
|
|
rotation and `theta` is the angle in radians.
|
|
|
|
### 3.2.3 Euler
|
|
|
|
This takes 3 args `heading, pitch, roll` and returns a rotation quaternion. It
|
|
aims to be based on Tait-Bryan angles, however this implementation assumes that
|
|
positive `z` is upwards: some assume it is downwards. Euler angles have more
|
|
standards than you can shake a stick at:
|
|
[Euler angles are horrible](https://github.com/moble/quaternion/wiki/Euler-angles-are-horrible).
|
|
|
|
### 3.2.4 The Quaternion constructor
|
|
|
|
This takes four args `w=1, x=0, y=0, z=0`. The default is the unit quaternion.
|
|
|
|
## 3.3 Quaternion properties
|
|
|
|
Quaternion instances support `w, x, y, z` read/write properties.
|
|
|
|
## 3.4 Iterator protocol
|
|
|
|
Quaternion instances support this. Thus
|
|
```python
|
|
q = Quaternion(5, 6, 7, 8)
|
|
print(q[1:]) # Prints the vector part array('f', [6.0, 7.0, 8.0])
|
|
q[1:] = (9, 10, 11)
|
|
print(q[1:]) # array('f', [9.0, 10.0, 11.0])
|
|
for x in q:
|
|
print(x) # It is possible to iterate over the elements
|
|
```
|
|
|
|
## 3.5 abs() len() and str()
|
|
|
|
```python
|
|
q = Quaternion(5, 6, 7, 8)
|
|
abs(q) # Returns its magnitude 13.19090595827292
|
|
len(q) # Always 4
|
|
str(q) # 'w = 5.00 x = 6.00 y = 7.00 z = 8.00'
|
|
```
|
|
|
|
## 3.6 Comparison operators
|
|
|
|
Two `Quaternion` instances are equal if all their `w, x, y, z` values match.
|
|
Otherwise their maginitudes are compared. Thus `q1 == q2` tests for exact
|
|
equality. `q1 >= q2` will return `True` if they are exactly equal or if
|
|
`abs(q1) > abs(q2)`.
|
|
|
|
The test for "exact" equality is subject to the issues around comparison of
|
|
floating point numbers. The `math.isclose` method is used with a value of 0.001
|
|
(0.1%). This may be adjusted (`mdelta` and `adelta` variables).
|
|
|
|
## 3.7 Addition and subtraction
|
|
|
|
A `Quaternion` instance may have the following types added to it or subtracted
|
|
from it, resulting in a new `Quaternion` instance being returned.
|
|
1. Another Quaternion.
|
|
2. A scalar. This modifies the `w` value.
|
|
3. A 4-tuple or list: modifies `w, x, y, z`.
|
|
4. A 3-tuple or list: modifies the vector part `x, y, z` and sets the scalar
|
|
part `w` to zero returning a vector quaternion. Intended for moving `Point`
|
|
instances.
|
|
|
|
The typical application of addition of a 3-tuple is the movement of a `Point`.
|
|
|
|
## 3.8 Multiplication
|
|
|
|
A `Quaternion` instance may be mutliplied by the following types resulting in a
|
|
new `Quaternion` instance being returned.
|
|
1. Another Quaternion. Returns the Hamilton product. Multiplication is not
|
|
commutative.
|
|
2. A scalar. This multiplies `w, x, y, z`.
|
|
3. A 4-tuple or list: multiplies `w, x, y, z` by each element.
|
|
4. A 3-tuple or list: multiplies the vector part `x, y, z` by each element and
|
|
sets the scalar part `w` to zero returning a vector quaternion. Intended for
|
|
scaling `Point` instances.
|
|
|
|
Multiplication by a scalar or a tuple is commutative. If a list of points
|
|
comprises a shape, multiplication by a scalar can be used to resize the shape.
|
|
Multiplication by a 3-tuple can transform a shape, for example changing a cube
|
|
to a cuboid.
|
|
|
|
## 3.9 Division
|
|
|
|
It is possible to divide a scalar by a quaternion: `1/q` produces the inverse
|
|
of quaternion `q`. This may also be acquired by issuing `q.inverse()`. The
|
|
following statements are equivalent:
|
|
```python
|
|
q1 = (1, 2, 3) / Quaternion(4, 5, 6, 7)
|
|
q2 = Quaternion(4, 5, 6, 7).inverse() * (1, 2, 3)
|
|
```
|
|
Note that multiplication of a quaternion by a scalar or by a tuple is
|
|
commutative, while multiplication of quaternions is not. Consequently the
|
|
notation `p/q` where `p` and `q` are quaternions is ambiguous because it is
|
|
unclear whether it corresponds to `p*q.inverse()` or `q.inverse()*p`. The
|
|
`Quaternion` class disallows this, raising a `ValueError`.
|
|
|
|
See [build notes](./QUATERNION.md#313-build-notes).
|
|
|
|
## 3.10 Rotation
|
|
|
|
Rotations are not commutative. Place two books side by side. Rotate one 90°
|
|
clockwise, then flip it 180° about an axis facing away from you. Repeat with
|
|
the other book in the opposite order.
|
|
|
|
Rotation is implemented using the matmul `@` operator. This is normally applied
|
|
with a `Point` or `Vector` quaternion on the LHS and a `Rotator` (rotation
|
|
quaternion) on the RHS.
|
|
```python
|
|
point = Point(1, 0, 0)
|
|
rot = Rotator(pi/6, 0, 1, 0)
|
|
new_point = point @ rot
|
|
point @= rot # Rotate in place
|
|
```
|
|
There is an `rrot` method performing order-reversed rotation:
|
|
```
|
|
def __matmul__(self, rot):
|
|
return rot * self * rot.conjugate()
|
|
|
|
def rrot(self, rot):
|
|
return rot.conjugate() * self * rot
|
|
```
|
|
|
|
### 3.10.1 Rotation direction
|
|
|
|
Consider these `Rotator` instances:
|
|
```python
|
|
rotx = Rotator(0.1, 1, 0, 0) # About X
|
|
roty = Rotator(0.1, 0, 1, 0) # Y
|
|
rotz = Rotator(0.1, 0, 0, 1) # Z
|
|
point = Point(1, 1, 1)
|
|
```
|
|
Applying a rotator with a positive angle to a point will cause anticlockwise
|
|
rotation as seen from a positive location on the relevant axis looking towards
|
|
the origin. This is analogous to 2d rotation on the complex plane where
|
|
multiplying by (say) 1 + 1j will produce an anticlockwise rotation.
|
|
|
|
## 3.11 Other methods
|
|
|
|
1. `conjugate` No args, returns a new `Quaternion` being its conjugate.
|
|
2. `inverse` No args, returns a new `Quaternion` being its inverse.
|
|
3. `to_angle_axis` No args. Intended for rotation quaternions. Returns
|
|
`[theta, x, y, z]` where `theta` is the rotation angle and the vector from the
|
|
origin to `x, y, z` is the axis.
|
|
4. `copy` No args. Returns a copy of the instance.
|
|
5. `normalise` Aims to produce a rotation quaternion: returns a new quaternion
|
|
instance whose maginitude is 1. Intended to compensate for any accumulation of
|
|
errors in a rotation quaternion.
|
|
6. `isvec` No args. Returns `True` if it is a vector quaternion i.e. `w == 0`.
|
|
7. `isrot` No args. Returns `True` if it is a rotation quaternion i.e.
|
|
`abs(q) == 1`.
|
|
|
|
## 3.12 The euler function
|
|
|
|
This takes a `Quaternion` instance as an arg. Returns `heading, pitch, roll`.
|
|
See [section 3.2.3](./quaternions.md#323-euler).
|
|
|
|
## 3.13 Build notes
|
|
|
|
Not all ports provide `math.isclose` and the reverse special methods. To some
|
|
extent the lack of reverse special methods can be worked around by changing the
|
|
operand order in expressions to ensure that the left hand side of a binary
|
|
operator is a `Quaternion` instance. This cannot be applied to division and the
|
|
`.inverse()` method must be used.
|
|
|
|
Bettrr, edit `ports/port/mpconfigport.h` to ensure these lines are included:
|
|
```C
|
|
#define MICROPY_PY_MATH_ISCLOSE (1)
|
|
|
|
#define MICROPY_PY_ALL_SPECIAL_METHODS (1) // May already be present
|
|
#define MICROPY_PY_REVERSE_SPECIAL_METHODS (1) // Add this line
|
|
```
|
|
and rebuild the firmware.
|
|
|
|
# Appendix 1: references
|
|
|
|
[Visalising quaternions](https://www.youtube.com/watch?v=d4EgbgTm0Bg)
|
|
[Visalising quaternions - interactive](https://eater.net/quaternions/video/doublecover)
|
|
[Using Quaternion to Perform 3D rotations](https://www.cprogramming.com/tutorial/3d/uses.html)
|
|
The Wikipedia article is maths-heavy.
|
|
[Wikpedia](https://en.wikipedia.org/wiki/Quaternion)
|
|
The following ref is good, but its formula for multiplication reverses the
|
|
order of its arguments compared to the other sources. The q1*q2 from
|
|
other sources corresponds to q2*q1 from this paper.
|
|
[Rotation Quaternions, and How to Use Them](http://danceswithcode.net/engineeringnotes/quaternions/quaternions.html)
|
|
|
|
From the youtube video: p -> (q2(q1pq1**-1)q2**-1)
|
|
|
|
Why to use quaternions rather than [Euler angles](https://github.com/moble/quaternion/wiki/Euler-angles-are-horrible).
|