diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8b94016..7faddc1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,5 +1,6 @@
cmake_minimum_required(VERSION 3.7)
+
option(VERSION_STRING "Project version string" "0.0.0")
project(antkeeper VERSION ${VERSION_STRING} LANGUAGES CXX)
diff --git a/src/animation/bone.hpp b/src/animation/bone.hpp
new file mode 100644
index 0000000..4b66f54
--- /dev/null
+++ b/src/animation/bone.hpp
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2021 Christopher J. Howard
+ *
+ * This file is part of Antkeeper source code.
+ *
+ * Antkeeper source code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Antkeeper source code is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Antkeeper source code. If not, see .
+ */
+
+#ifndef ANTKEEPER_ANIMATION_BONE_HPP
+#define ANTKEEPER_ANIMATION_BONE_HPP
+
+#include
+
+/// Mask to extract the index of a bone.
+constexpr std::uint16_t bone_index_mask = 0xFF;
+
+/**
+ * Skeletal animation bone identifier, consisting of a bone index in the lower half, and a parent bone index in the upper half.
+ */
+typedef std::uint16_t bone;
+
+/**
+ * Bone index comparison function object.
+ */
+struct bone_index_compare
+{
+ /**
+ * Compares the indices of two bones.
+ *
+ * @param lhs First bone.
+ * @param rhs Second bone.
+ * @return Comparison result.
+ */
+ bool operator()(const bone& lhs, const bone& rhs) const;
+};
+
+/**
+ * Constructs a bone identifier.
+ *
+ * @param index Index of the bone.
+ * @param parent_index Index of the parent bone.
+ * @return Bone identifier.
+ */
+bone make_bone(std::uint8_t index, std::uint8_t parent_index);
+
+/**
+ * Constructs an orphan bone identifier.
+ *
+ * @param index Index of the orphan bone.
+ * @return Orphan bone identifier.
+ */
+bone make_bone(std::uint8_t index);
+
+/**
+ * Returns the index of a bone.
+ *
+ * @param x Bone identifier.
+ * @return Index of the bone.
+ */
+std::uint8_t bone_index(bone x);
+
+/**
+ * Returns the parent index of a bone.
+ *
+ * @param x Bone identifier.
+ * @return Index of the parent bone.
+ */
+std::uint8_t bone_parent_index(bone x);
+
+/**
+ * Returns `true` if a bone has a parent, `false` otherwise.
+ *
+ * @param x Bone identifier.
+ * @return Bone parent status.
+ */
+bool bone_has_parent(bone x);
+
+inline bool bone_index_compare::operator()(const bone& lhs, const bone& rhs) const
+{
+ return (lhs & bone_index_mask) < (rhs & bone_index_mask);
+}
+
+inline bone make_bone(std::uint8_t index, std::uint8_t parent_index)
+{
+ return (static_cast(parent_index) << 8) | index;
+}
+
+inline bone make_bone(std::uint8_t index)
+{
+ return make_bone(index, index);
+}
+
+inline std::uint8_t bone_index(bone x)
+{
+ return static_cast(x & bone_index_mask);
+}
+
+inline std::uint8_t bone_parent_index(bone x)
+{
+ return static_cast(x >> 8);
+}
+
+inline bool bone_has_parent(bone x)
+{
+ return (x & bone_index_mask) != (x >> 8);
+}
+
+#endif // ANTKEEPER_ANIMATION_BONE_HPP
diff --git a/src/animation/pose.cpp b/src/animation/pose.cpp
new file mode 100644
index 0000000..bcce652
--- /dev/null
+++ b/src/animation/pose.cpp
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 Christopher J. Howard
+ *
+ * This file is part of Antkeeper source code.
+ *
+ * Antkeeper source code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Antkeeper source code is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Antkeeper source code. If not, see .
+ */
+
+#include "animation/pose.hpp"
+#include "math/transform-operators.hpp"
+
+void concatenate(const pose& bone_space, pose& skeleton_space)
+{
+ for (auto&& [bone, transform]: bone_space)
+ {
+ auto parent_index = bone_parent_index(bone);
+
+ if (parent_index != bone_index(bone))
+ {
+ auto parent = skeleton_space.find(parent_index);
+ skeleton_space[bone] = (parent != skeleton_space.end()) ? parent->second * transform : transform;
+ }
+ else
+ {
+ skeleton_space[bone] = transform;
+ }
+ }
+}
diff --git a/src/render/skeleton.hpp b/src/animation/pose.hpp
similarity index 54%
rename from src/render/skeleton.hpp
rename to src/animation/pose.hpp
index f6cd1f3..3ffd714 100644
--- a/src/render/skeleton.hpp
+++ b/src/animation/pose.hpp
@@ -17,37 +17,26 @@
* along with Antkeeper source code. If not, see .
*/
-#ifndef ANTKEEPER_RENDER_SKELETON_HPP
-#define ANTKEEPER_RENDER_SKELETON_HPP
+#ifndef ANTKEEPER_ANIMATION_POSE_HPP
+#define ANTKEEPER_ANIMATION_POSE_HPP
-#include "render/bone.hpp"
-#include
-#include
-#include
-#include
-
-namespace render {
+#include "animation/bone.hpp"
+#include "math/transform-type.hpp"
+#include