diff --git a/src/core/include/pw/core/axisangle.hpp b/src/core/include/pw/core/axisangle.hpp
index 719cbb9..4bfe7a4 100644
--- a/src/core/include/pw/core/axisangle.hpp
+++ b/src/core/include/pw/core/axisangle.hpp
@@ -28,7 +28,7 @@
 
 namespace pw {
 
-template <typename T> struct axisangle {
+template <typename T> struct axisangle final {
 
     using value_type = T;
     using axis_type  = vector3<T>;
diff --git a/src/core/include/pw/core/frustum.hpp b/src/core/include/pw/core/frustum.hpp
new file mode 100644
index 0000000..58c3922
--- /dev/null
+++ b/src/core/include/pw/core/frustum.hpp
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 1999-2021 Hartmut Seichter
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+#ifndef PW_CORE_FRUSTUM_HPP
+#define PW_CORE_FRUSTUM_HPP
+
+#include <pw/core/globals.hpp>
+
+namespace pw {
+
+template <typename Scalar> struct frustum final {
+    Scalar left{-1}, right{1}, bottom{-1}, top{1}, z_near{-1}, z_far{1};
+
+    static constexpr auto make_perspective_symmetric(Scalar fov_h_deg,
+                                                     Scalar aspect_ratio,
+                                                     Scalar z_near,
+                                                     Scalar z_far) -> frustum {
+        const auto tangent_half{std::tan(pw::deg_to_rad(fov_h_deg / 2))};
+        const auto top{tangent_half * z_near};
+        const auto right{top * aspect_ratio};
+
+        return {.left   = -right,
+                .right  = right,
+                .bottom = -top,
+                .top    = top,
+                .z_near = z_near,
+                .z_far  = z_far};
+    }
+
+    static constexpr auto make_orthographic_symmetric(Scalar scale,
+                                                      Scalar aspect_ratio,
+                                                      Scalar z_near,
+                                                      Scalar z_far) -> frustum {
+        return {.left   = -scale,
+                .right  = scale,
+                .bottom = -scale * aspect_ratio,
+                .top    = scale * aspect_ratio,
+                .z_near = z_near,
+                .z_far  = z_far};
+    }
+};
+
+} // namespace pw
+
+#endif
diff --git a/src/core/include/pw/core/matrix.hpp b/src/core/include/pw/core/matrix.hpp
index 0a305e2..923aec1 100644
--- a/src/core/include/pw/core/matrix.hpp
+++ b/src/core/include/pw/core/matrix.hpp
@@ -23,7 +23,6 @@
 #ifndef PW_CORE_MATRIX_HPP
 #define PW_CORE_MATRIX_HPP
 
-#include <cstddef>
 #include <pw/core/globals.hpp>
 #include <pw/core/math.hpp>
 #include <pw/core/vector.hpp>
diff --git a/src/core/include/pw/core/matrix_transform.hpp b/src/core/include/pw/core/matrix_transform.hpp
index 5616ddf..cf170f2 100644
--- a/src/core/include/pw/core/matrix_transform.hpp
+++ b/src/core/include/pw/core/matrix_transform.hpp
@@ -26,41 +26,10 @@
 #include <pw/core/math.hpp>
 #include <pw/core/matrix.hpp>
 #include <pw/core/vector.hpp>
+#include <pw/core/frustum.hpp>
 
 namespace pw {
 
-template <typename Scalar> struct frustum final {
-    Scalar left{-1}, right{1}, bottom{-1}, top{1}, z_near{-1}, z_far{1};
-
-    static constexpr auto make_perspective_symmetric(Scalar fov_h_deg,
-                                                     Scalar aspect_ratio,
-                                                     Scalar z_near,
-                                                     Scalar z_far) -> frustum {
-        const auto tangent_half{std::tan(pw::deg_to_rad(fov_h_deg / 2))};
-        const auto top{tangent_half * z_near};
-        const auto right{top * aspect_ratio};
-
-        return {.left   = -right,
-                .right  = right,
-                .bottom = -top,
-                .top    = top,
-                .z_near = z_near,
-                .z_far  = z_far};
-    }
-
-    static constexpr auto make_orthographic_symmetric(Scalar scale,
-                                                      Scalar aspect_ratio,
-                                                      Scalar z_near,
-                                                      Scalar z_far) -> frustum {
-        return {.left   = -scale,
-                .right  = scale,
-                .bottom = -scale * aspect_ratio,
-                .top    = scale * aspect_ratio,
-                .z_near = z_near,
-                .z_far  = z_far};
-    }
-};
-
 struct matrix_transform {
 
     template <typename Scalar>
diff --git a/src/core/include/pw/core/quaternion.hpp b/src/core/include/pw/core/quaternion.hpp
index 80254ff..1e5893b 100644
--- a/src/core/include/pw/core/quaternion.hpp
+++ b/src/core/include/pw/core/quaternion.hpp
@@ -23,8 +23,8 @@
 #ifndef PW_CORE_QUATERNION_HPP
 #define PW_CORE_QUATERNION_HPP
 
-#include <pw/core/matrix.hpp>
 #include <pw/core/axisangle.hpp>
+#include <pw/core/matrix.hpp>
 #include <pw/core/vector.hpp>
 
 #include <concepts>
@@ -83,6 +83,10 @@ template <std::floating_point Scalar> struct quaternion final {
         return conjugate() / this->norm();
     }
 
+    constexpr auto dot(const quaternion& b) const noexcept -> Scalar {
+        return q_.dot(b.q_);
+    }
+
     constexpr auto to_matrix() const noexcept -> matrix<Scalar, 3, 3> {
 
         const Scalar xx = x() * x();
@@ -119,9 +123,61 @@ template <std::floating_point Scalar> struct quaternion final {
     }
 
     static constexpr auto lerp(const quaternion& a, const quaternion& b,
-                     const Scalar& t) -> quaternion {
+                               const Scalar& t) -> quaternion {
         return {value_type::lerp(a.q_, b.q_, t).normalized()};
     }
+
+    /**
+     * @note: a and b need to be normalized
+     */
+    static constexpr auto
+    slerp(const quaternion& a, const quaternion& b, const Scalar& t,
+          const Scalar& eps = Scalar{0.001}) -> quaternion {
+
+        // Calculate angle between them.
+        const Scalar cos_half_theta{a.dot(b)};
+
+        // if qa=qb or qa=-qb then theta = 0 and we can return a
+        if (std::fabs(cos_half_theta) >= Scalar{1}) {
+            return a;
+        }
+
+        // Calculate temporary values.
+        const Scalar half_theta{std::acos(cos_half_theta)};
+        const Scalar sin_half_theta{
+            std::sqrt(Scalar{1} - cos_half_theta * cos_half_theta)};
+
+        // if theta = 180 degrees then result is not fully defined
+        // we could rotate around any axis normal to a or b
+        const auto is_pi = std::fabs(sin_half_theta) < eps;
+
+        // now do the lerp either halfway or across unit sphere
+        const Scalar ratio_a{is_pi ? Scalar{0.5}
+                                   : std::sin((Scalar{1} - t) * half_theta) /
+                                         sin_half_theta};
+
+        const Scalar ratio_b{is_pi ? Scalar{0.5}
+                                   : std::sin(t * half_theta) / sin_half_theta};
+        return {
+            (a.x() * ratio_a + b.x() * ratio_b), // x
+            (a.y() * ratio_a + b.y() * ratio_b), // y
+            (a.z() * ratio_a + b.z() * ratio_b), // z
+            (a.w() * ratio_a + b.w() * ratio_b)  // w
+        };
+    }
+
+    static constexpr auto
+    from_axisangle(const axisangle<Scalar>& aa) -> quaternion {
+
+        const auto sin_half_angle{std::sin(aa.angle / Scalar{2})};
+
+        return {
+            aa.axis.x() * sin_half_angle,  // x
+            aa.axis.y() * sin_half_angle,  // y
+            aa.axis.z() * sin_half_angle,  // z
+            std::cos(aa.angle / Scalar{2}) // w
+        };
+    }
 };
 
 // deduction guide for quaternion
diff --git a/src/core/tests/pwcore_test_quaternion.cpp b/src/core/tests/pwcore_test_quaternion.cpp
index 8492549..588e0d8 100644
--- a/src/core/tests/pwcore_test_quaternion.cpp
+++ b/src/core/tests/pwcore_test_quaternion.cpp
@@ -13,6 +13,10 @@ auto main() -> int {
     pw::debug::d() << "q1 = quaternion{1,2,3,4} -> \n"
                    << pw::serialize::to_string(q1);
 
+    q1 = q1.normalized();
+    pw::debug::d() << "q1 = quaternion{1,2,3,4}.normalized() -> \n"
+                   << pw::serialize::to_string(q1);
+
     auto q0_x_q1 = q0 * q1;
     pw::debug::d() << "q0 * q1 -> \n" << pw::serialize::to_string(q0_x_q1);
 
@@ -28,10 +32,15 @@ auto main() -> int {
     pw::debug::d() << "q1.conjugate().inverse() -> \n"
                    << pw::serialize::to_string(q1_conj_inv);
 
-    auto lerp_q0_q1_half = decltype(q0)::lerp(q0, q1, 0.5f);
-    pw::debug::d() << "quaternion::lerp(q0,q1,0.5) -> \n"
+    pw::debug::d() << "quaternion::dot(q0,q1) -> " << q0.dot(q1);
+
+    auto lerp_q0_q1_half = decltype(q0)::lerp(q0, q1, 0.3f);
+    pw::debug::d() << "quaternion::lerp(q0,q1,0.3) -> \n"
                    << pw::serialize::to_string(lerp_q0_q1_half);
 
+    auto slerp_q0_q1_half = decltype(q0)::slerp(q0, q1, 0.3f);
+    pw::debug::d() << "quaternion::slerp(q0,q1,0.3) -> \n"
+                   << pw::serialize::to_string(slerp_q0_q1_half);
 
     return 0;
 }