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_order convention.

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).