Tutorial 2: Orientations and Rotations
In this tutorial you will learn to work with orientation quantities in 2D and 3D space, and to create and apply rotations — the angular counterpart of translations.
An orientation quantity represents the current angular state of a body: the direction it is facing. Like a position quantity, it has no associated coordinate system — it is raw angular data. A rotation is a transformation function that changes orientations and can also be applied to position quantities. It is composable and invertible, just like a translation.
This tutorial covers the R2 (2D) and R3 (3D) orientation types separately, because
the two cases differ significantly in their representations.
The Code
#include <print>
import ninbot;
void SO2()
{
using namespace nin;
using namespace nin::R2;
orientation_qty identity;
orientation_qty qty_from_angle { 90_deg };
orientation_qty qty_from_complex { std::complex<double>{ 0, 1 } };
units::angle qty_angle = qty_from_angle.to_angle();
std::complex<double> qty_complex = qty_from_angle.to_complex();
rotation rot { 45_deg };
rotation rot2 { π_rad/2 };
rotation rot12 = compose( rot, rot2 );
rotation rot12inv = inv( rot12 );
orientation_qty rotated_ori = rot12( identity );
R2::point_qty p { 1_m, 0_m };
R2::point_qty p_rotated = rot( p );
std::println( "SO2: 90° orientation as angle: {}", qty_angle );
std::println( "SO2: 45° rotation of (1, 0) m: {}", p_rotated );
std::println( "SO2: 135° rotation of identity: {}", rotated_ori.to_angle() );
}
void SO3()
{
using namespace nin;
using namespace nin::R3;
orientation_qty identity;
orientation_qty qty_quaternion { nin::quaternion{ 0.5, 0.5, 0.5, 0.5 } };
orientation_qty qty_matrix { std::array<float, 9>{
-1, 0, 0,
0, -1, 0,
0, 0, 1
}, nin::matrix_ordering::ROW_MAJOR };
orientation_qty qty_XYaxes {
std::array<float, 3>{ 0, 1, 0 },
std::array<float, 3>{ 0, 0, 1 } };
orientation_qty qty_axisangle {
std::array<float, 3>{ 1, 1, 1 },
120_deg };
orientation_qty qty_euler { 180_deg, 0_deg, 1_rad, euler_seq::RollPitchYaw };
orientation_qty::axis_angle aa = qty_axisangle.to_axis_angle();
nin::quaternion q = qty_axisangle.to_quaternion();
std::array<double, 9> m = qty_axisangle.to_matrix( nin::matrix_ordering::ROW_MAJOR );
std::array<units::angle, 3> ea = qty_axisangle.to_euler_angles( euler_seq::XYZ );
orientation_qty halfway = slerp( identity, qty_axisangle, 0.5 );
rotation rot { std::array<float, 3>{ 0, 0, 1 }, 90_deg };
rotation rot_inv = inv( rot );
orientation_qty rotated_ori = rot( identity );
R3::point_qty p { 1_m, 0_m, 0_m };
R3::point_qty p_rotated = rot( p );
std::println( "SO3: axis-angle angle: {}", aa.angle );
std::println( "SO3: 90° Z-rotation of (1, 0, 0) m: {}", p_rotated );
std::println( "SO3: slerp halfway angle: {}", halfway.to_axis_angle().angle );
}
int main()
{
SO2();
SO3();
return 0;
}
Output
SO2: 90° orientation as angle: 1.5707963267948966 SO2: 45° rotation of (1, 0) m: (0.7071067811865476, 0.7071067811865475) (m) SO2: 135° rotation of identity: 2.356194490192345 SO3: axis-angle angle: 2.0943951023931957 SO3: 90° Z-rotation of (1, 0, 0) m: (2.220446049250313e-16, 1, 0) (m) SO3: slerp halfway angle: 1.0471975511965979
Building the code
Save the example as orientations-rotations.c++ alongside this CMakeLists.txt:
cmake_minimum_required(VERSION 4.0)
project(orientations-rotations LANGUAGES CXX)
find_package(Ninbot REQUIRED)
add_executable(orientations-rotations orientations-rotations.c++)
target_link_libraries(orientations-rotations PRIVATE ninbot::ninbot)
Then configure and build:
cmake -B build -G Ninja -DCMAKE_PREFIX_PATH=/path/to/ninbot/build
cmake --build build
./build/orientations-rotations
Walkthrough
Imports and namespaces
#include <print>
import ninbot;
using namespace nin;
using namespace nin::R2; // or nin::R3
using namespace nin brings unit literals (1_m, 90_deg, …) and the WCS tag into scope.
The inner R2:: and R3:: namespaces contain the orientation and rotation types for the
respective dimensionalities.
R2 orientations
orientation_qty identity;
orientation_qty qty_from_angle { 90_deg };
orientation_qty qty_from_complex { std::complex<double>{ 0, 1 } };
An R2::orientation_qty is stored internally as a unit complex number on the unit circle.
It can be constructed from an angle or from an explicit unit complex number.
The default-constructed value corresponds to angle 0 (facing the positive X direction).
units::angle qty_angle = qty_from_angle.to_angle();
std::complex<double> qty_complex = qty_from_angle.to_complex();
to_angle() extracts the angle in the range (−π, π] by default.
to_complex() returns the underlying unit complex number.
R2 rotations
rotation rot { 45_deg };
rotation rot2 { 90_deg };
An R2::rotation is a transformation parameterised by an angle.
rotation rot12 = compose( rot, rot2 );
rotation rot12inv = inv( rot12 );
compose combines two rotations: rot (45°) followed by rot2 (90°) gives 135°.
The first argument to compose evaluates after the second.
inv returns the inverse rotation.
orientation_qty rotated_ori = rot12( identity );
R2::point_qty p_rotated = rot( p );
Rotations are callable: applying rot12 to the identity orientation yields an orientation
at 135°.
Applying rot (45°) to the point (1, 0) m rotates it to (√½, √½) m.
R3 orientations
orientation_qty identity;
orientation_qty qty_quaternion { nin::quaternion{ 0.5, 0.5, 0.5, 0.5 } };
orientation_qty qty_matrix { std::array<float, 9>{ -1, 0, 0, 0, -1, 0, 0, 0, 1 },
nin::matrix_ordering::ROW_MAJOR };
orientation_qty qty_XYaxes { std::array<float, 3>{ 0, 1, 0 },
std::array<float, 3>{ 0, 0, 1 } };
orientation_qty qty_axisangle { std::array<float, 3>{ 1, 1, 1 }, 120_deg };
orientation_qty qty_euler { 180_deg, 0_deg, 1_rad, euler_order::RollPitchYaw };
R3::orientation_qty is stored internally as a unit quaternion.
The library accepts many input representations:
-
A quaternion directly (
nin::quaternion). -
A 3×3 rotation matrix as a flat
std::array<float, 9>(row-major or column-major). -
Two orthogonal axis vectors that define the X and Y axes of the new frame.
-
An axis-angle pair: a 3-element array for the axis and an angle.
-
Three Euler angles with an
euler_orderconvention.
R3 orientation conversions
orientation_qty::axis_angle aa = qty_axisangle.to_axis_angle();
nin::quaternion q = qty_axisangle.to_quaternion();
std::array<double, 9> m = qty_axisangle.to_matrix( nin::matrix_ordering::ROW_MAJOR );
std::array<units::angle, 3> ea = qty_axisangle.to_euler_angles( euler_order::XYZ );
Every R3::orientation_qty can be converted to any of the supported representations.
to_axis_angle() returns an axis_angle struct with .axis (a nin::vector<double,3>) and
.angle (a units::angle).
120° around (1, 1, 1)/√3 gives an angle of ≈2.094 rad.
R3 orientation interpolation
orientation_qty halfway = slerp( identity, qty_axisangle, 0.5 );
slerp (spherical linear interpolation) interpolates between two orientations along the
shortest arc on the unit quaternion sphere.
A parameter of 0.5 gives the orientation exactly halfway between the two:
half of 120° (≈2.094 rad) is 60° (≈1.047 rad).
Orientations are quantities, not transformations, so they support slerp but have no compose
or inv.
R3 rotations
rotation rot { std::array<float, 3>{ 0, 0, 1 }, 90_deg };
rotation rot_inv = inv( rot );
An R3::rotation is most commonly constructed from an axis-angle pair.
Here a 90° rotation around the Z axis is created and then inverted.
orientation_qty rotated_ori = rot( identity );
R3::point_qty p_rotated = rot( p );
Applying the 90° Z-rotation to the identity orientation gives an orientation facing 90° around Z. Applying it to (1, 0, 0) m rotates the point to (0, 1, 0) m (up to floating-point rounding).