diff --git a/demos/combustion_demo/src/combustion_demo.cpp b/demos/combustion_demo/src/combustion_demo.cpp
index 4aaf9f7a6ef0fe802a4e99c67e717eaa47b429de..ab7bedcef95122ba366b7797bb0f60e947c649c9 100644
--- a/demos/combustion_demo/src/combustion_demo.cpp
+++ b/demos/combustion_demo/src/combustion_demo.cpp
@@ -33,6 +33,7 @@
 #include "phx/core/runtime_component.hpp"
 #include "phx/display/display_system_openvr.hpp"
 #include "phx/display/display_system_window.hpp"
+#include "phx/input/device_system.hpp"
 #include "phx/input/input_system.hpp"
 #include "phx/rendering/auxiliary/splash_screen.hpp"
 #include "phx/rendering/components/mesh_handle.hpp"
@@ -47,11 +48,12 @@
 #endif
 
 int main(int, char**) {
-  std::unique_ptr<phx::Engine> engine = phx::Setup::CreateDefaultEngine(false);
+  std::unique_ptr<phx::Engine> engine = phx::Setup::CreateDefaultEngine(true);
   auto scene = engine->GetScene();
   auto rendering_system = engine->GetSystem<phx::RenderingSystem>();
   rendering_system->SetEnabled(false);
   auto openvr_system = engine->GetSystem<phx::DisplaySystemOpenVR>();
+  auto device_system = engine->GetSystem<phx::DeviceSystem>();
 
   phx::SplashScreen* splash = engine->CreateSystem<phx::SplashScreen>(
       engine->GetSystem<phx::DisplaySystemWindow>()->GetWindow());
@@ -70,15 +72,17 @@ int main(int, char**) {
     if (key == 'q') engine->Stop();
   });
 
-  auto virtual_platform = scene->GetEntitiesWithComponents<
-      phx::RuntimeComponent<phx::USER_PLATFORM>>()[0];
-  // virtual_platform->AddComponent<DesktopNavigationBehavior>(
-  //    engine->GetSystem<phx::InputSystem>());
-  auto controller_navigation_behavior =
-      virtual_platform->AddComponent<VRControllerInteractionBehavior>(
-          openvr_system);
+  auto right_controller_entities = scene->GetEntitiesWithComponents<
+      phx::RuntimeComponent<phx::RIGHT_CONTROLLER>>();
+  VRControllerInteractionBehavior* right_interaction_behavior = nullptr;
+  if (right_controller_entities.size() >= 1) {
+    auto right_controller_entity = right_controller_entities[0];
+    right_interaction_behavior =
+        right_controller_entity->AddComponent<VRControllerInteractionBehavior>(
+            device_system);
+  }
 
-  auto handle = std::async([&controller_navigation_behavior, &scene,
+  auto handle = std::async([&right_interaction_behavior, &scene,
                             rendering_system, splash, input_system,
                             openvr_system]() {
     auto model_surface_entity = phx::SceneLoader::InsertModelIntoScene(
@@ -133,7 +137,8 @@ int main(int, char**) {
         glm::vec3(1.0f, 1.0f, 1.0f));
     floor_material->GetMaterial()->SetAmbientColor(glm::vec3(0.1f, 0.1f, 0.1f));
 
-    controller_navigation_behavior->SetTarget(vis_root_transform);
+    if (right_interaction_behavior)
+      right_interaction_behavior->SetTarget(vis_root_transform);
 
     auto desk_entity = phx::SceneLoader::InsertModelIntoScene(
         "models/cube/cube2.obj", scene.get());
@@ -199,6 +204,8 @@ int main(int, char**) {
 
   if (!openvr_system) {
     camera->AddComponent<DesktopNavigationBehavior>(input_system);
+    camera->GetFirstComponent<phx::Transform>()->Translate(
+        glm::vec3(0.0f, 1.0f, 1.0f));
   }
 
   engine->Run();
diff --git a/demos/combustion_demo/src/vr_controller_interaction_behavior.cpp b/demos/combustion_demo/src/vr_controller_interaction_behavior.cpp
index a7d7e2b0fe6357b0c3db50b1d45a35ab56143723..6d113a15013a3ef318979fae5e1fbf739d8451ae 100644
--- a/demos/combustion_demo/src/vr_controller_interaction_behavior.cpp
+++ b/demos/combustion_demo/src/vr_controller_interaction_behavior.cpp
@@ -41,47 +41,31 @@ SUPPRESS_WARNINGS_END
 #include "phx/rendering/components/transform.hpp"
 
 VRControllerInteractionBehavior::VRControllerInteractionBehavior(
-    phx::DisplaySystemOpenVR* display_system_openvr)
-    : display_system_openvr_(display_system_openvr) {}
+    phx::DeviceSystem* device_system)
+    : device_system_(device_system) {}
 
-void VRControllerInteractionBehavior::OnUpdate() {
-  // If there exists an HMD and a transform.
-  phx::HMD* hmd = nullptr;
-  if (display_system_openvr_ != nullptr) {
-    hmd = display_system_openvr_->GetHMD();
-  }
+void VRControllerInteractionBehavior::OnButtonSignal(
+    phx::VRController::ButtonId id, phx::VRController::ButtonEvent event) {
   const auto transform = GetEntity()->GetFirstComponent<phx::Transform>();
-  if (hmd == nullptr || transform == nullptr) return;
-
-  auto indices = hmd->GetControllerIndices();
-
-  for (auto i = 0u; i < indices.size(); ++i) {
-    vr::VRControllerState_t controller_state;
-    vr::VRSystem()->GetControllerState(indices[i], &controller_state,
-                                       sizeof controller_state);
-
-    if (target_) {
-      if (controller_state.ulButtonPressed &
-          vr::ButtonMaskFromId(vr::EVRButtonId::k_EButton_SteamVR_Trigger)) {
-        target_->SetParent(
-            GetEntity()
-                ->GetScene()
-                ->GetEntitiesWithComponents<
-                    phx::RuntimeComponent<phx::RIGHT_CONTROLLER>>()[0]
-                ->GetFirstComponent<phx::Transform>());
-        is_grabbed_ = true;
-        release_delay_ = 100;
-      } else if (is_grabbed_) {
-        release_delay_--;
-      }
-      if (release_delay_ == 0) {
-        target_->SetParent(nullptr);
-        is_grabbed_ = false;
-      }
+  if (target_ != nullptr) {
+    if (event == phx::VRController::BUTTON_PRESSED &&
+        id == vr::EVRButtonId::k_EButton_SteamVR_Trigger) {
+      if (transform != nullptr)
+        target_->SetParent(GetEntity()->GetFirstComponent<phx::Transform>());
+    }
+    if (event == phx::VRController::BUTTON_RELEASED &&
+        id == vr::EVRButtonId::k_EButton_SteamVR_Trigger) {
+      target_->SetParent(nullptr);
     }
   }
 }
 
+void VRControllerInteractionBehavior::OnUpdate() {
+  if (side_ == phx::VRController::INVALID_CONTROLLER) {
+    RegisterOnDeviceSignal();
+  }
+}
+
 void VRControllerInteractionBehavior::SetTarget(phx::Transform* target) {
   target_ = target;
 }
@@ -89,3 +73,33 @@ void VRControllerInteractionBehavior::SetTarget(phx::Transform* target) {
 phx::Transform* VRControllerInteractionBehavior::GetTarget() const {
   return target_;
 }
+
+void VRControllerInteractionBehavior::RegisterOnDeviceSignal() {
+  if (GetEntity()
+          ->GetFirstComponent<phx::RuntimeComponent<phx::LEFT_CONTROLLER>>() !=
+      nullptr)
+    side_ = phx::VRController::LEFT_CONTROLLER;
+  else if (GetEntity()
+               ->GetFirstComponent<
+                   phx::RuntimeComponent<phx::RIGHT_CONTROLLER>>() != nullptr)
+    side_ = phx::VRController::RIGHT_CONTROLLER;
+  else
+    phx::warn(
+        "Added VRControllerInteractionBehavior to a non-controller entity");
+
+  phx::VRController* controller = nullptr;
+  for (auto cont : device_system_->GetDevices<phx::VRController>()) {
+    if (cont->GetSide() == side_) {
+      controller = cont;
+      break;
+    }
+  }
+
+  if (controller != nullptr) {
+    controller->RegisterButtonSignal(
+        [this](phx::VRController::ButtonId id,
+               phx::VRController::ButtonEvent event) {
+          this->OnButtonSignal(id, event);
+        });
+  }
+}
diff --git a/demos/combustion_demo/src/vr_controller_interaction_behavior.hpp b/demos/combustion_demo/src/vr_controller_interaction_behavior.hpp
index 6ed6f309f1cb452e1e330c06ea634ae5ed5c516b..c66c364edaf9e77ec3f5dd6938facee53789c175 100644
--- a/demos/combustion_demo/src/vr_controller_interaction_behavior.hpp
+++ b/demos/combustion_demo/src/vr_controller_interaction_behavior.hpp
@@ -23,14 +23,14 @@
 #ifndef DEMOS_COMBUSTION_DEMO_SRC_VR_CONTROLLER_INTERACTION_BEHAVIOR_HPP_
 #define DEMOS_COMBUSTION_DEMO_SRC_VR_CONTROLLER_INTERACTION_BEHAVIOR_HPP_
 
-#include "phx/display/display_system_openvr.hpp"
+#include "phx/input/device_system.hpp"
+#include "phx/input/vr_controller.hpp"
 #include "phx/rendering/components/transform.hpp"
 #include "phx/scripting/behavior.hpp"
 
 class VRControllerInteractionBehavior : public phx::Behavior {
  public:
-  explicit VRControllerInteractionBehavior(
-      phx::DisplaySystemOpenVR* display_system_openvr);
+  explicit VRControllerInteractionBehavior(phx::DeviceSystem* device_system);
   VRControllerInteractionBehavior(const VRControllerInteractionBehavior& that) =
       default;
   VRControllerInteractionBehavior(VRControllerInteractionBehavior&& temp) =
@@ -42,17 +42,21 @@ class VRControllerInteractionBehavior : public phx::Behavior {
       VRControllerInteractionBehavior&& temp) = default;
 
   void OnUpdate() override;
+  void OnButtonSignal(phx::VRController::ButtonId id,
+                      phx::VRController::ButtonEvent event);
 
   void SetTarget(phx::Transform* target);
   phx::Transform* GetTarget() const;
 
  protected:
-  phx::DisplaySystemOpenVR* display_system_openvr_;
+  phx::DeviceSystem* device_system_;
 
  private:
+  void RegisterOnDeviceSignal();
+
   phx::Transform* target_ = nullptr;
-  bool is_grabbed_ = false;
-  int release_delay_ = 0;
+  phx::VRController::ControllerSide side_ =
+      phx::VRController::INVALID_CONTROLLER;
 };
 
 #endif  // DEMOS_COMBUSTION_DEMO_SRC_VR_CONTROLLER_INTERACTION_BEHAVIOR_HPP_
diff --git a/demos/viewer/src/navigation_behavior.cpp b/demos/viewer/src/navigation_behavior.cpp
deleted file mode 100644
index e9c914fd4d8658c03d65d1362203764739d3e2ed..0000000000000000000000000000000000000000
--- a/demos/viewer/src/navigation_behavior.cpp
+++ /dev/null
@@ -1,72 +0,0 @@
-//------------------------------------------------------------------------------
-// Project Phoenix
-//
-// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
-// Virtual Reality & Immersive Visualization Group.
-//------------------------------------------------------------------------------
-//                                 License
-//
-// Licensed under the 3-Clause BSD License (the "License");
-// you may not use this file except in compliance with the License.
-// See the file LICENSE for the full text.
-// You may obtain a copy of the License at
-//
-//     https://opensource.org/licenses/BSD-3-Clause
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//------------------------------------------------------------------------------
-
-#include "navigation_behavior.hpp"
-
-#include <vector>
-
-#include "phx/suppress_warnings.hpp"
-
-SUPPRESS_WARNINGS_BEGIN
-#include "glm/detail/type_vec3.hpp"
-#include "glm/glm.hpp"
-#include "glm/gtc/matrix_access.hpp"
-SUPPRESS_WARNINGS_END
-
-#include "phx/core/engine.hpp"
-#include "phx/core/entity.hpp"
-#include "phx/core/scene.hpp"
-#include "phx/display/display_system_openvr.hpp"
-#include "phx/display/hmd.hpp"
-#include "phx/rendering/components/transform.hpp"
-
-NavigationBehavior::NavigationBehavior(
-    phx::DisplaySystemOpenVR* display_system_openvr)
-    : display_system_openvr_(display_system_openvr) {}
-
-void NavigationBehavior::OnUpdate() {
-  // If there exists an HMD and a transform.
-  phx::HMD* hmd = nullptr;
-  if (display_system_openvr_ != nullptr) {
-    hmd = display_system_openvr_->GetHMD();
-  }
-  const auto transform = GetEntity()->GetFirstComponent<phx::Transform>();
-  if (hmd == nullptr || transform == nullptr) return;
-
-  auto indices = hmd->GetControllerIndices();
-
-  for (auto i = 0u; i < indices.size(); ++i) {
-    vr::VRControllerState_t controller_state;
-    vr::VRSystem()->GetControllerState(indices[i], &controller_state,
-                                       sizeof controller_state);
-
-    // Set the transform based on whether the trigger is pressed.
-    if (controller_state.ulButtonTouched &
-        vr::ButtonMaskFromId(vr::EVRButtonId::k_EButton_SteamVR_Trigger))
-      transform->Translate(
-          -0.05F * glm::column(hmd->GetRightControllerTransformation(), 2));
-    if (controller_state.ulButtonTouched &
-        vr::ButtonMaskFromId(vr::EVRButtonId::k_EButton_SteamVR_Touchpad))
-      transform->Translate(
-          0.05F * glm::column(hmd->GetRightControllerTransformation(), 2));
-  }
-}
diff --git a/demos/viewer/src/viewer.cpp b/demos/viewer/src/viewer.cpp
index d8d6fe5a575496de37b140e615c349bb0ee62fa0..a43834aba9cadeb24c0425e9c2373a7b4026f78d 100644
--- a/demos/viewer/src/viewer.cpp
+++ b/demos/viewer/src/viewer.cpp
@@ -22,10 +22,7 @@
 
 #include <chrono>
 #include <future>
-#include <iostream>
 #include <memory>
-#include <string>
-#include <utility>
 #include <vector>
 
 #include "phx/core/engine.hpp"
@@ -36,47 +33,40 @@
 #include "phx/display/display_system_openvr.hpp"
 #include "phx/display/display_system_window.hpp"
 #include "phx/display/window.hpp"
+#include "phx/input/device_system.hpp"
 #include "phx/input/input_system.hpp"
 #include "phx/rendering/auxiliary/splash_screen.hpp"
 #include "phx/rendering/components/light.hpp"
-#include "phx/rendering/components/material_handle.hpp"
-#include "phx/rendering/components/mesh_handle.hpp"
 #include "phx/rendering/components/transform.hpp"
 #include "phx/rendering/rendering_system.hpp"
 #include "phx/resources/loaders/assimp_model_loader.hpp"
-#include "phx/resources/loaders/openvr_resource_loader.hpp"
 #include "phx/resources/loaders/scene_loader.hpp"
-#include "phx/resources/types/mesh.hpp"
-#include "phx/resources/resource_declaration.hpp"
 #include "phx/resources/resource_manager.hpp"
-#include "phx/resources/resource_pointer.hpp"
-#include "phx/phoenix.hpp"
 #include "phx/setup.hpp"
 
-#include "navigation_behavior.hpp"
-#include "rotation_behavior.hpp"
 #include "viewer_system.hpp"
+#include "vrcontroller_navigation_behavior.hpp"
 
 #if defined __clang__
 #pragma clang diagnostic ignored "-Wmissing-prototypes"
 #endif
 
 int main(int, char**) {
-  std::unique_ptr<phx::Engine> engine = phx::Setup::CreateDefaultEngine(false);
+  std::unique_ptr<phx::Engine> engine = phx::Setup::CreateDefaultEngine(true);
   auto scene = engine->GetScene();
   auto rendering_system = engine->GetSystem<phx::RenderingSystem>();
   rendering_system->SetEnabled(false);
 
   phx::SplashScreen* splash = engine->CreateSystem<phx::SplashScreen>(
-    engine->GetSystem<phx::DisplaySystemWindow>()->GetWindow());
+      engine->GetSystem<phx::DisplaySystemWindow>()->GetWindow());
   engine->MoveSystemBefore(splash,
-    engine->GetSystem<phx::DisplaySystemWindow>());
+                           engine->GetSystem<phx::DisplaySystemWindow>());
 
   auto assimp_loader = static_cast<phx::AssimpModelLoader*>(
-    phx::ResourceManager::instance().GetLoaderForType(".obj"));
+      phx::ResourceManager::instance().GetLoaderForType(".obj"));
 
   assimp_loader->SetProgressUpdateCallback(
-    [splash](float progress) { splash->SetLoadProgress(progress); });
+      [splash](float progress) { splash->SetLoadProgress(progress); });
 
   phx::InputSystem* input_system = engine->GetSystem<phx::InputSystem>();
   ViewerSystem* viewer_system = engine->CreateSystem<ViewerSystem>();
@@ -88,8 +78,8 @@ int main(int, char**) {
   });
 
   auto handle = std::async([&scene, rendering_system, splash]() {
-    phx::SceneLoader::InsertModelIntoScene(
-      "models/UniversityScene/Univers20171013.obj", scene.get());
+    phx::SceneLoader::InsertModelIntoScene("models/bunny.obj", scene.get());
+    // "models/UniversityScene/Univers20171013.obj"
 
     rendering_system->SetEnabled(true);
     splash->SetEnabled(false);
@@ -124,8 +114,9 @@ int main(int, char**) {
   virtual_platform_transform->SetLocalTranslation(start_position);
   phx::info("The  virtual platform's start position is: {}", start_position);
 
-  virtual_platform->AddComponent<NavigationBehavior>(
-      engine->GetSystem<phx::DisplaySystemOpenVR>());
+  virtual_platform->AddComponent<VRControllerNavigationBehavior>(
+      engine->GetSystem<phx::DeviceSystem>(),
+      phx::VRController::RIGHT_CONTROLLER);
 
   engine->Run();
 
diff --git a/demos/viewer/src/vrcontroller_navigation_behavior.cpp b/demos/viewer/src/vrcontroller_navigation_behavior.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9acee895c5d9784aa53d6c017de0fd72a8e5b7ba
--- /dev/null
+++ b/demos/viewer/src/vrcontroller_navigation_behavior.cpp
@@ -0,0 +1,99 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#include "vrcontroller_navigation_behavior.hpp"
+
+#include "phx/suppress_warnings.hpp"
+
+SUPPRESS_WARNINGS_BEGIN
+#include "glm/detail/type_vec3.hpp"
+#include "glm/glm.hpp"
+#include "glm/gtc/matrix_access.hpp"
+SUPPRESS_WARNINGS_END
+
+#include "phx/core/entity.hpp"
+#include "phx/core/logger.hpp"
+#include "phx/core/runtime_component.hpp"
+#include "phx/display/hmd.hpp"
+#include "phx/rendering/components/transform.hpp"
+
+VRControllerNavigationBehavior::VRControllerNavigationBehavior(
+    phx::DeviceSystem* device_system, phx::VRController::ControllerSide side)
+    : device_system_(device_system), side_(side) {
+  phx::VRController* controller = GetController();
+
+  if (controller != nullptr) {
+    controller->RegisterButtonSignal(
+        [this](phx::VRController::ButtonId id,
+               phx::VRController::ButtonEvent event) {
+          this->OnButtonSignal(id, event);
+        });
+  }
+}
+
+void VRControllerNavigationBehavior::OnUpdate() {
+  const auto transform = GetEntity()->GetFirstComponent<phx::Transform>();
+  phx::VRController* controller = GetController();
+  if (transform != nullptr && controller != nullptr && button_pressed_) {
+    glm::mat4 controller_pos = controller->GetPose();
+    glm::vec3 controller_forward = glm::column(controller_pos, 2);
+    float speed =
+        speed_ * static_cast<float>(time_info_->time_since_last_frame.count());
+    speed *=
+        -1.0f * controller->GetAxesValue(phx::VRController::AXES_TRACKPAD)[1];
+    transform->Translate(speed * controller_forward);
+  }
+
+  phx::info("speed_: {}, axes: {}, time: {}, button_pressed: {}", speed_,
+            controller->GetAxesValue(phx::VRController::AXES_TRACKPAD),
+            time_info_->time_since_last_frame.count(), button_pressed_);
+}
+
+void VRControllerNavigationBehavior::OnButtonSignal(
+    phx::VRController::ButtonId id, phx::VRController::ButtonEvent event) {
+  if (event == phx::VRController::BUTTON_TOUCH) {
+    if (id == vr::EVRButtonId::k_EButton_SteamVR_Touchpad) {
+      button_pressed_ = true;
+    }
+  }
+  if (event == phx::VRController::BUTTON_UNTOUCH &&
+      id == vr::EVRButtonId::k_EButton_SteamVR_Touchpad) {
+    button_pressed_ = false;
+  }
+}
+
+float VRControllerNavigationBehavior::GetNavigationSpeed() const {
+  return speed_;
+}
+
+void VRControllerNavigationBehavior::SetNavigationSpeed(float speed) {
+  speed_ = speed;
+}
+
+phx::VRController* VRControllerNavigationBehavior::GetController() {
+  for (auto controller : device_system_->GetDevices<phx::VRController>()) {
+    if (controller->GetSide() == side_) {
+      return controller;
+    }
+  }
+  return nullptr;
+}
diff --git a/demos/viewer/src/vrcontroller_navigation_behavior.hpp b/demos/viewer/src/vrcontroller_navigation_behavior.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..2423ca508a6ea343fc986a77e7deb9613ef4afd5
--- /dev/null
+++ b/demos/viewer/src/vrcontroller_navigation_behavior.hpp
@@ -0,0 +1,62 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#ifndef DEMOS_VIEWER_SRC_VRCONTROLLER_NAVIGATION_BEHAVIOR_HPP_
+#define DEMOS_VIEWER_SRC_VRCONTROLLER_NAVIGATION_BEHAVIOR_HPP_
+
+#include "phx/input/device_system.hpp"
+#include "phx/input/vr_controller.hpp"
+#include "phx/rendering/components/transform.hpp"
+#include "phx/scripting/behavior.hpp"
+
+class VRControllerNavigationBehavior : public phx::Behavior {
+ public:
+  explicit VRControllerNavigationBehavior(
+      phx::DeviceSystem* device_system, phx::VRController::ControllerSide side);
+  VRControllerNavigationBehavior(const VRControllerNavigationBehavior& that) =
+      default;
+  VRControllerNavigationBehavior(VRControllerNavigationBehavior&& temp) =
+      default;
+  virtual ~VRControllerNavigationBehavior() = default;
+  VRControllerNavigationBehavior& operator=(
+      const VRControllerNavigationBehavior& that) = default;
+  VRControllerNavigationBehavior& operator=(
+      VRControllerNavigationBehavior&& temp) = default;
+
+  void OnUpdate() override;
+  void OnButtonSignal(phx::VRController::ButtonId id,
+                      phx::VRController::ButtonEvent event);
+
+  float GetNavigationSpeed() const;
+  void SetNavigationSpeed(float speed);
+
+ private:
+  phx::DeviceSystem* device_system_;
+  phx::VRController* GetController();
+
+  float speed_ = 0.5f;  // m/s
+  bool button_pressed_ = false;
+  phx::VRController::ControllerSide side_ =
+      phx::VRController::INVALID_CONTROLLER;
+};
+
+#endif  // DEMOS_VIEWER_SRC_VRCONTROLLER_NAVIGATION_BEHAVIOR_HPP_
diff --git a/library/phx/display/display_system_openvr.cpp b/library/phx/display/display_system_openvr.cpp
index a455ed5a07f6b738c4fd939f5402f22b04e55a8c..be1642e41657adba5f027f9e02f125b0956be080 100644
--- a/library/phx/display/display_system_openvr.cpp
+++ b/library/phx/display/display_system_openvr.cpp
@@ -33,6 +33,7 @@
 #include "phx/core/logger.hpp"
 #include "phx/core/runtime_component.hpp"
 #include "phx/core/scene.hpp"
+#include "phx/input/device_system.hpp"
 #include "phx/rendering/rendering_system.hpp"
 
 #undef CreateWindow
@@ -50,28 +51,20 @@ DisplaySystemOpenVR::DisplaySystemOpenVR(Engine* engine)
 }
 DisplaySystemOpenVR::~DisplaySystemOpenVR() {}
 
-phx::HMD* DisplaySystemOpenVR::CreateHMD() {
-  if (hmd_ == nullptr) {
-    hmd_ = std::make_unique<HMD>();
-  } else {
-    warn(
-        "[DisplaySystemOpenVR] HMD already created, so far only one is "
-        "supported.");
-  }
-  return hmd_.get();
+phx::HMD* DisplaySystemOpenVR::GetHMD() {
+  auto hmds = engine_->GetSystem<DeviceSystem>()->GetDevices<HMD>();
+  if (hmds.size() == 0) return nullptr;
+  return hmds[0];
 }
 
-void DisplaySystemOpenVR::DestroyHMD() { hmd_.reset(); }
-
-phx::HMD* DisplaySystemOpenVR::GetHMD() { return hmd_.get(); }
-
 void DisplaySystemOpenVR::Update(const FrameTimer::TimeInfo&) {
-  if (hmd_ != nullptr) {
+  HMD* hmd = GetHMD();
+  if (hmd != nullptr) {
     if (left_render_target_ != nullptr && right_render_target_ != nullptr) {
       auto right_texture = right_render_target_->GetColorTexture();
-      hmd_->Submit(HMD::RIGHT_EYE, right_texture);
+      hmd->Submit(HMD::RIGHT_EYE, right_texture);
       auto left_texture = left_render_target_->GetColorTexture();
-      hmd_->Submit(HMD::LEFT_EYE, left_texture);
+      hmd->Submit(HMD::LEFT_EYE, left_texture);
     }
   }
 }
diff --git a/library/phx/display/display_system_openvr.hpp b/library/phx/display/display_system_openvr.hpp
index eb1c50dace48a0a776c12e78e6e823f779393237..0e195ed9ba54aa8682d84d4853a14074fcee5245 100644
--- a/library/phx/display/display_system_openvr.hpp
+++ b/library/phx/display/display_system_openvr.hpp
@@ -30,8 +30,8 @@
 #include "phx/display/display_system.hpp"
 #include "phx/display/hmd.hpp"
 #include "phx/display/window.hpp"
-#include "phx/rendering/backend/render_target.hpp"
 #include "phx/export.hpp"
+#include "phx/rendering/backend/render_target.hpp"
 
 namespace phx {
 
@@ -45,17 +45,12 @@ class PHOENIX_EXPORT DisplaySystemOpenVR : public DisplaySystem {
   DisplaySystemOpenVR& operator=(const DisplaySystemOpenVR&) = delete;
   DisplaySystemOpenVR& operator=(DisplaySystemOpenVR&&) = default;
 
-  HMD* CreateHMD();
-  void DestroyHMD();
   HMD* GetHMD();
 
   void Update(const FrameTimer::TimeInfo&) override;
 
   void CreateRenderTargets(Scene* scene);
 
- protected:
-  std::unique_ptr<HMD> hmd_;
-
  private:
   void RemoveRenderTargets();
   void SetEyeProjections(Scene* scene);
diff --git a/library/phx/display/hmd.cpp b/library/phx/display/hmd.cpp
index 437a090372151dc070cfd88c423cd44d4fd8407c..f8d72a6ffaf9c5b2cff31fb25c1dad7c02abc725 100644
--- a/library/phx/display/hmd.cpp
+++ b/library/phx/display/hmd.cpp
@@ -25,7 +25,6 @@
 #include <algorithm>
 #include <array>
 #include <memory>
-#include <stdexcept>
 #include <string>
 #include <utility>
 #include <vector>
@@ -42,16 +41,10 @@ SUPPRESS_WARNINGS_END
 
 #include "phx/core/logger.hpp"
 #include "phx/resources/resource_manager.hpp"
-#include "phx/resources/resource_pointer.hpp"
 
 namespace phx {
-HMD::HMD() {
-  vr::HmdError hmd_error = vr::VRInitError_None;
-  vr_system_ = vr::VR_Init(&hmd_error, vr::VRApplication_Scene);
-  if (vr_system_ == nullptr || hmd_error != vr::VRInitError_None) {
-    error("HMD cannot be initialized with error-code: {}", hmd_error);
-    throw std::runtime_error("OpenVR cannot be initialized!");
-  }
+HMD::HMD() : TrackedDevice() {
+  id_ = vr::k_unTrackedDeviceIndex_Hmd;
 
   uint32_t x, y;
   vr_system_->GetRecommendedRenderTargetSize(&x, &y);
@@ -60,12 +53,9 @@ HMD::HMD() {
   projection_right_ = GetProjectionMatrixFromOpenVR(vr::Hmd_Eye::Eye_Right);
   projection_left_ = GetProjectionMatrixFromOpenVR(vr::Hmd_Eye::Eye_Left);
 
-  eye_to_head_right_ = GetEyeToHeadMatrixFromOpenVR(vr::Hmd_Eye::Eye_Right);
-  eye_to_head_left_ = GetEyeToHeadMatrixFromOpenVR(vr::Hmd_Eye::Eye_Left);
+  UpdateEyeToHeadMatrices();
 }
 
-HMD::~HMD() { vr::VR_Shutdown(); }
-
 bool HMD::IsHMDPresent() { return vr::VR_IsHmdPresent(); }
 
 void HMD::Submit(Side side, gl::texture_2d* texture) {
@@ -75,18 +65,7 @@ void HMD::Submit(Side side, gl::texture_2d* texture) {
   vr::VRCompositor()->Submit(static_cast<vr::EVREye>(side), &vr_texture);
 }
 
-std::vector<vr::TrackedDeviceIndex_t> HMD::GetControllerIndices() {
-  std::vector<std::uint32_t> indices(vr::k_unMaxTrackedDeviceCount);
-  vr::VRSystem()->GetSortedTrackedDeviceIndicesOfClass(
-      vr::TrackedDeviceClass_Controller, indices.data(),
-      static_cast<std::uint32_t>(indices.size()));
-  return indices;
-}
-
-vr::ETrackedControllerRole HMD::GetControllerRoleForTrackedDeviceIndex(
-    vr::TrackedDeviceIndex_t device_index) const {
-  return vr_system_->GetControllerRoleForTrackedDeviceIndex(device_index);
-}
+void HMD::UpdateDeviceIndex() { id_ = vr::k_unTrackedDeviceIndex_Hmd; }
 
 const glm::uvec2& HMD::GetViewportSize() const { return viewport_size_; }
 
@@ -104,145 +83,26 @@ const glm::mat4& HMD::GetEyeToHeadMatrix(Side side) const {
     return eye_to_head_left_;
 }
 
-void HMD::UpdateTrackedDevices() {
-  vr::VRCompositor()->WaitGetPoses(tracked_device_poses_,
-                                   vr::k_unMaxTrackedDeviceCount, nullptr, 0);
-}
+void HMD::Update() {
+  last_wait_get_poses_.resize(vr::k_unMaxTrackedDeviceCount);
 
-glm::mat4 HMD::GetHeadTransformation() {
-  if (tracked_device_poses_[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid) {
-    glm::mat4 head = TransformToGlmMatrix(
-        tracked_device_poses_[vr::k_unTrackedDeviceIndex_Hmd]
+  vr::VRCompositor()->WaitGetPoses(&last_wait_get_poses_[0],
+                                   vr::k_unMaxTrackedDeviceCount, NULL, 0);
+  if (last_wait_get_poses_[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid) {
+    pose_ = TransformToGlmMatrix(
+        last_wait_get_poses_[vr::k_unTrackedDeviceIndex_Hmd]
             .mDeviceToAbsoluteTracking);
-    last_head_transformation_ = head;
-    return head;
+  } else {
+    debug("[HMD] HMD pose is invalid, use the last valid one");
   }
-  debug("[HMD] HMD pose is invalid, use the last valid one");
-  return last_head_transformation_;
-}
 
-glm::mat4 HMD::GetTransformationForRole(vr::ETrackedControllerRole role) {
-  auto controller_indices = GetControllerIndices();
-  for (auto i = 0u; i < controller_indices.size(); ++i)
-    if (vr_system_->GetControllerRoleForTrackedDeviceIndex(
-            controller_indices[i]) == role)
-      return TransformToGlmMatrix(tracked_device_poses_[controller_indices[i]]
-                                      .mDeviceToAbsoluteTracking);
-  debug(
-      "[HMD::GetTransformationForRole] Unable to find tranformation for role "
-      "{}",
-      role);
-  return glm::mat4();
+  TrackedDevice::Update();
 }
 
-glm::mat4 HMD::GetLeftControllerTransformation() {
-  return GetTransformationForRole(vr::TrackedControllerRole_LeftHand);
-}
-
-glm::mat4 HMD::GetRightControllerTransformation() {
-  return GetTransformationForRole(vr::TrackedControllerRole_RightHand);
-}
-
-std::unique_ptr<phx::Mesh> HMD::GetControllerMesh(Controller controller) {
-  auto model = GetControllerModel(controller);
-  if (model == nullptr) {
-    return nullptr;
-  }
-  auto mesh = std::make_unique<phx::Mesh>();
-  std::vector<glm::vec3> vertices;
-  std::vector<glm::vec3> normals;
-  std::vector<glm::vec2> texcoords;
-  for (std::size_t i = 0; i < model->unVertexCount; i++) {
-    vertices.push_back(glm::vec3(model->rVertexData[i].vPosition.v[0],
-                                 model->rVertexData[i].vPosition.v[1],
-                                 model->rVertexData[i].vPosition.v[2]));
-    normals.push_back(glm::vec3(model->rVertexData[i].vNormal.v[0],
-                                model->rVertexData[i].vNormal.v[1],
-                                model->rVertexData[i].vNormal.v[2]));
-    texcoords.push_back(glm::vec2(model->rVertexData[i].rfTextureCoord[0],
-                                  model->rVertexData[i].rfTextureCoord[1]));
-  }
-  std::vector<unsigned int> indices;
-  for (std::size_t i = 0; i < model->unTriangleCount * 3; i++) {
-    indices.push_back(model->rIndexData[i]);
+void HMD::OnOpenVREvent(const vr::VREvent_t& event) {
+  if (event.eventType == vr::VREvent_IpdChanged) {
+    UpdateEyeToHeadMatrices();
   }
-  mesh->SetVertices(std::move(vertices));
-  mesh->SetNormals(std::move(normals));
-  mesh->SetTextureCoords(std::move(texcoords));
-  mesh->SetIndices(std::move(indices));
-  return mesh;
-}
-
-vr::RenderModel_t* HMD::GetControllerModel(Controller controller) {
-  std::string rendermodel_name;
-  rendermodel_name.resize(1024);
-  auto controller_indices = GetControllerIndices();
-  for (auto i = 0u; i < controller_indices.size(); ++i) {
-    if (vr_system_->GetControllerRoleForTrackedDeviceIndex(
-            controller_indices[i]) ==
-        static_cast<vr::ETrackedControllerRole>(controller)) {
-      vr::VRSystem()->GetStringTrackedDeviceProperty(
-          controller_indices[i], vr::Prop_RenderModelName_String,
-          &rendermodel_name[0], static_cast<uint32_t>(rendermodel_name.size()));
-    }
-  }
-
-  vr::RenderModel_t* model = nullptr;
-  while (vr::VRRenderModels()->LoadRenderModel_Async(
-             &rendermodel_name[0], &model) == vr::VRRenderModelError_Loading) {
-    std::this_thread::sleep_for(std::chrono::milliseconds(50));
-  }
-  if (model == nullptr) {
-    return nullptr;
-  }
-
-  return model;
-}
-
-std::unique_ptr<Material> HMD::GetControllerMaterial(Controller controller) {
-  auto model = GetControllerModel(controller);
-  if (model == nullptr) return nullptr;
-
-  auto material = std::make_unique<phx::Material>();
-  material->SetAmbientColor(glm::vec3(0.1, 0.1, 0.1));
-  material->SetSpecularColor(glm::vec3(0.3, 0.3, 0.3));
-  material->SetShininess(1.0f);
-
-  auto texture = ResourceManager::instance().DeclareResource<Image>(
-      {{"TYPE", "openVR"},
-       {"OpenVR_type", "texture"},
-       {"texture_id", model->diffuseTextureId},
-       {"side",
-        controller == HMD::Controller::LEFT_CONTROLLER ? "left" : "right"}});
-  texture.Load();
-  material->SetDiffuseImage(texture);
-
-  return material;
-}
-
-std::unique_ptr<Image> HMD::GetControllerTexture(int id) {
-  vr::RenderModel_TextureMap_t* texture_map;
-  while (vr::VRRenderModels()->LoadTexture_Async(id, &texture_map) ==
-         vr::VRRenderModelError_Loading) {
-    std::this_thread::sleep_for(std::chrono::milliseconds(50));
-  }
-  if (texture_map == nullptr) {
-    return nullptr;
-  }
-
-  std::vector<unsigned char> image_data(texture_map->unWidth *
-                                        texture_map->unHeight * 4);
-  std::copy(texture_map->rubTextureMapData,
-            texture_map->rubTextureMapData + image_data.size(),
-            image_data.begin());
-  auto image = std::make_unique<phx::Image>(
-      &image_data[0],
-      std::array<std::size_t, 2>{
-          {static_cast<std::size_t>(texture_map->unWidth),
-           static_cast<std::size_t>(texture_map->unHeight)}},
-      32);
-
-  return image;
 }
 
 glm::mat4 HMD::GetProjectionMatrixFromOpenVR(const vr::Hmd_Eye eye) {
@@ -259,18 +119,9 @@ glm::mat4 HMD::GetEyeToHeadMatrixFromOpenVR(const vr::Hmd_Eye eye) {
   return TransformToGlmMatrix(steamvr_eye_head_matrix);
 }
 
-glm::mat4 HMD::TransformToGlmMatrix(const vr::HmdMatrix34_t& mat) {
-  return glm::mat4(mat.m[0][0], mat.m[1][0], mat.m[2][0], 0.0f, mat.m[0][1],
-                   mat.m[1][1], mat.m[2][1], 0.0f, mat.m[0][2], mat.m[1][2],
-                   mat.m[2][2], 0.0f, mat.m[0][3], mat.m[1][3], mat.m[2][3],
-                   1.0f);
-}
-
-glm::mat4 HMD::TransformToGlmMatrix(const vr::HmdMatrix44_t& mat) {
-  return glm::mat4(mat.m[0][0], mat.m[1][0], mat.m[2][0], mat.m[3][0],
-                   mat.m[0][1], mat.m[1][1], mat.m[2][1], mat.m[3][1],
-                   mat.m[0][2], mat.m[1][2], mat.m[2][2], mat.m[3][2],
-                   mat.m[0][3], mat.m[1][3], mat.m[2][3], mat.m[3][3]);
+void HMD::UpdateEyeToHeadMatrices() {
+  eye_to_head_right_ = GetEyeToHeadMatrixFromOpenVR(vr::Hmd_Eye::Eye_Right);
+  eye_to_head_left_ = GetEyeToHeadMatrixFromOpenVR(vr::Hmd_Eye::Eye_Left);
 }
 
 }  // namespace phx
diff --git a/library/phx/display/hmd.hpp b/library/phx/display/hmd.hpp
index 4bf398e9009ead18a97c8aec08fbdce9345be680..be7f5f0f6398c3a2d03ad65af177873e5e9c9628 100644
--- a/library/phx/display/hmd.hpp
+++ b/library/phx/display/hmd.hpp
@@ -35,20 +35,20 @@ SUPPRESS_WARNINGS_BEGIN
 #include "openvr.h"  //NOLINT
 SUPPRESS_WARNINGS_END
 
+#include "phx/export.hpp"
+#include "phx/input/tracked_device.hpp"
 #include "phx/resources/types/material.hpp"
 #include "phx/resources/types/mesh.hpp"
-#include "phx/export.hpp"
 
 #include "gl/texture.hpp"
 
 namespace phx {
 
-class OpenVRResourceLoader;
-
-class PHOENIX_EXPORT HMD {
+class PHOENIX_EXPORT HMD : public TrackedDevice {
  public:
   HMD();
-  ~HMD();
+  ~HMD() = default;
+
   HMD(const HMD&) = delete;
   HMD(HMD&&) = default;
 
@@ -60,45 +60,25 @@ class PHOENIX_EXPORT HMD {
     LEFT_EYE = vr::EVREye::Eye_Left
   };
 
-  enum Controller {
-    RIGHT_CONTROLLER = vr::TrackedControllerRole_RightHand,
-    LEFT_CONTROLLER = vr::TrackedControllerRole_LeftHand
-  };
+  void Update() override;
+  void OnOpenVREvent(const vr::VREvent_t& event) override;
 
   const glm::uvec2& GetViewportSize() const;
 
   const glm::mat4& GetProjectionMatrix(Side side) const;
   const glm::mat4& GetEyeToHeadMatrix(Side side) const;
 
-  void UpdateTrackedDevices();
-  glm::mat4 GetHeadTransformation();
-  glm::mat4 GetLeftControllerTransformation();
-  glm::mat4 GetRightControllerTransformation();
-
   static bool IsHMDPresent();
 
   void Submit(Side side, gl::texture_2d* texture);
 
-  std::vector<vr::TrackedDeviceIndex_t> GetControllerIndices();
-  vr::ETrackedControllerRole GetControllerRoleForTrackedDeviceIndex(
-      vr::TrackedDeviceIndex_t device_index) const;
+  void UpdateDeviceIndex() override;
 
  private:
-  glm::mat4 GetTransformationForRole(vr::ETrackedControllerRole role);
-
   glm::mat4 GetProjectionMatrixFromOpenVR(const vr::Hmd_Eye eye);
   glm::mat4 GetEyeToHeadMatrixFromOpenVR(const vr::Hmd_Eye eye);
+  void UpdateEyeToHeadMatrices();
 
-  friend OpenVRResourceLoader;
-  vr::RenderModel_t* GetControllerModel(Controller controller);
-  std::unique_ptr<Mesh> GetControllerMesh(Controller controller);
-  std::unique_ptr<Material> GetControllerMaterial(Controller controller);
-  std::unique_ptr<Image> GetControllerTexture(int id);
-
-  static glm::mat4 TransformToGlmMatrix(const vr::HmdMatrix34_t& mat);
-  static glm::mat4 TransformToGlmMatrix(const vr::HmdMatrix44_t& mat);
-
-  vr::IVRSystem* vr_system_;
   glm::uvec2 viewport_size_;
 
   glm::mat4 projection_right_;
@@ -106,9 +86,6 @@ class PHOENIX_EXPORT HMD {
   glm::mat4 eye_to_head_right_;
   glm::mat4 eye_to_head_left_;
 
-  vr::TrackedDevicePose_t tracked_device_poses_
-      [vr::k_unMaxTrackedDeviceCount];  // NOLINT(runtime/arrays)
-
   glm::mat4 last_head_transformation_;
 };
 
diff --git a/library/phx/input/device.cpp b/library/phx/input/device.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5c68859d70073a2d10a384ed647c75afdd66c9b0
--- /dev/null
+++ b/library/phx/input/device.cpp
@@ -0,0 +1,25 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#include "phx/input/device.hpp"
+
+namespace phx {}  // namespace phx
diff --git a/demos/viewer/src/navigation_behavior.hpp b/library/phx/input/device.hpp
similarity index 57%
rename from demos/viewer/src/navigation_behavior.hpp
rename to library/phx/input/device.hpp
index aee9accca37a44597cd536ae96edbcb5d6471ab4..c477639764a905efdc712809d5a74034be7aaf98 100644
--- a/demos/viewer/src/navigation_behavior.hpp
+++ b/library/phx/input/device.hpp
@@ -20,25 +20,19 @@
 // limitations under the License.
 //------------------------------------------------------------------------------
 
-#ifndef DEMOS_VIEWER_SRC_NAVIGATION_BEHAVIOR_HPP_
-#define DEMOS_VIEWER_SRC_NAVIGATION_BEHAVIOR_HPP_
+#ifndef LIBRARY_PHX_INPUT_DEVICE_HPP_
+#define LIBRARY_PHX_INPUT_DEVICE_HPP_
 
-#include "phx/display/display_system_openvr.hpp"
-#include "phx/scripting/behavior.hpp"
+#include "phx/export.hpp"
 
-class NavigationBehavior : public phx::Behavior {
- public:
-  explicit NavigationBehavior(phx::DisplaySystemOpenVR* display_system_openvr);
-  NavigationBehavior(const NavigationBehavior& that) = default;
-  NavigationBehavior(NavigationBehavior&& temp) = default;
-  ~NavigationBehavior() override = default;
-  NavigationBehavior& operator=(const NavigationBehavior& that) = default;
-  NavigationBehavior& operator=(NavigationBehavior&& temp) = default;
-
-  void OnUpdate() override;
+namespace phx {
 
- protected:
-  phx::DisplaySystemOpenVR* display_system_openvr_;
+class PHOENIX_EXPORT Device {
+ public:
+  virtual ~Device() = default;
+  virtual void Update() = 0;
 };
 
-#endif  // DEMOS_VIEWER_SRC_NAVIGATION_BEHAVIOR_HPP_
+}  // namespace phx
+
+#endif  // LIBRARY_PHX_INPUT_DEVICE_HPP_
diff --git a/library/phx/input/device_system.cpp b/library/phx/input/device_system.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c19d7947ef0a3d69cfecb728c33ddf9de591405a
--- /dev/null
+++ b/library/phx/input/device_system.cpp
@@ -0,0 +1,53 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#include "phx/input/device_system.hpp"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "phx/core/logger.hpp"
+
+namespace phx {
+DeviceSystem::DeviceSystem(Engine* engine) : System(engine) {}
+
+DeviceSystem::~DeviceSystem() {}
+
+void DeviceSystem::Update(const FrameTimer::TimeInfo&) {
+  for (auto& device : devices_) {
+    device->Update();
+  }
+}
+
+void DeviceSystem::RemoveDevice(Device* device) {
+  auto iterator =
+      std::remove_if(devices_.begin(), devices_.end(),
+                     [device](const std::unique_ptr<Device>& iteratee) {
+                       return device == iteratee.get();
+                     });
+  devices_.erase(iterator, devices_.end());
+}
+
+std::string DeviceSystem::ToString() const { return "DeviceSystem"; }
+
+}  // namespace phx
diff --git a/library/phx/input/device_system.hpp b/library/phx/input/device_system.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..644b8b471f4a3ed5b0ce43f5628dc33c7128ff6d
--- /dev/null
+++ b/library/phx/input/device_system.hpp
@@ -0,0 +1,84 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#ifndef LIBRARY_PHX_INPUT_DEVICE_SYSTEM_HPP_
+#define LIBRARY_PHX_INPUT_DEVICE_SYSTEM_HPP_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "phx/core/engine.hpp"
+#include "phx/core/system.hpp"
+#include "phx/export.hpp"
+#include "phx/input/device.hpp"
+
+namespace phx {
+class InputSystem;
+
+class PHOENIX_EXPORT DeviceSystem : public System {
+ public:
+  DeviceSystem() = delete;
+  DeviceSystem(const DeviceSystem&) = delete;
+  DeviceSystem(DeviceSystem&&) = default;
+  virtual ~DeviceSystem();
+
+  DeviceSystem& operator=(const DeviceSystem&) = delete;
+  DeviceSystem& operator=(DeviceSystem&&) = default;
+
+  void Update(const FrameTimer::TimeInfo& time_info) override;
+
+  template <typename DeviceType>
+  std::vector<DeviceType*> GetDevices() {
+    static_assert(std::is_base_of<Device, DeviceType>::value,
+                  "The type does not inherit from Device.");
+
+    std::vector<DeviceType*> devices;
+    for (const auto& device : devices_) {
+      auto casted_device = dynamic_cast<DeviceType*>(device.get());
+      if (casted_device != nullptr) {
+        devices.push_back(casted_device);
+      }
+    }
+    return devices;
+  }
+
+  template <typename DeviceType, typename... ComponentArguments>
+  DeviceType* AddDevice(ComponentArguments&&... arguments) {
+    devices_.push_back(std::make_unique<DeviceType>(arguments...));
+    return dynamic_cast<DeviceType*>(devices_.back().get());
+  }
+
+  void RemoveDevice(Device* device);
+
+  std::string ToString() const override;
+
+ private:
+  friend DeviceSystem* Engine::CreateSystem<DeviceSystem>();
+  explicit DeviceSystem(Engine* engine);
+
+  std::vector<std::unique_ptr<Device>> devices_;
+};
+
+}  // namespace phx
+
+#endif  // LIBRARY_PHX_INPUT_DEVICE_SYSTEM_HPP_
diff --git a/library/phx/input/openvr_controller_behavior.cpp b/library/phx/input/openvr_controller_behavior.cpp
index cd229828f4a407039f843dc5219e07d7d5c6aaca..fefc8a7c802f0c27dc679072401cffb559017d98 100644
--- a/library/phx/input/openvr_controller_behavior.cpp
+++ b/library/phx/input/openvr_controller_behavior.cpp
@@ -31,14 +31,14 @@ namespace phx {
 void OpenVRControllerBehavior::OnUpdate() {
   phx::Scene* scene = GetEntity()->GetScene();
   phx::Entity* runtime_entity = nullptr;
-  if (side_ == Side::LEFT) {
+  if (side_ == VRController::LEFT_CONTROLLER) {
     auto runtime_entities = scene->GetEntitiesWithComponents<
         phx::RuntimeComponent<phx::LEFT_CONTROLLER>>();
     if (!runtime_entities.empty()) {
       runtime_entity = runtime_entities[0];
     }
   }
-  if (side_ == Side::RIGHT) {
+  if (side_ == VRController::RIGHT_CONTROLLER) {
     auto runtime_entities = scene->GetEntitiesWithComponents<
         phx::RuntimeComponent<phx::RIGHT_CONTROLLER>>();
     if (!runtime_entities.empty()) {
@@ -53,9 +53,11 @@ void OpenVRControllerBehavior::OnUpdate() {
   }
 }
 
-void OpenVRControllerBehavior::SetSide(Side side) { side_ = side; }
+void OpenVRControllerBehavior::SetSide(VRController::ControllerSide side) {
+  side_ = side;
+}
 
-OpenVRControllerBehavior::Side OpenVRControllerBehavior::GetSide() const {
+VRController::ControllerSide OpenVRControllerBehavior::GetSide() const {
   return side_;
 }
 
diff --git a/library/phx/input/openvr_controller_behavior.hpp b/library/phx/input/openvr_controller_behavior.hpp
index 99483e41811c55be517c7eb80fde13b3d20d5153..9f8ce400b7a42d04937543ca2daf4df505361814 100644
--- a/library/phx/input/openvr_controller_behavior.hpp
+++ b/library/phx/input/openvr_controller_behavior.hpp
@@ -26,20 +26,19 @@
 #include "phx/suppress_warnings.hpp"
 
 #include "phx/export.hpp"
+#include "phx/input/vr_controller.hpp"
 #include "phx/scripting/behavior.hpp"
 
 namespace phx {
 class PHOENIX_EXPORT OpenVRControllerBehavior : public Behavior {
  public:
-  enum Side { LEFT, RIGHT };
-
   void OnUpdate() override;
 
-  void SetSide(Side side);
-  Side GetSide() const;
+  void SetSide(VRController::ControllerSide side);
+  VRController::ControllerSide GetSide() const;
 
  private:
-  Side side_ = Side::LEFT;
+  VRController::ControllerSide side_ = VRController::LEFT_CONTROLLER;
 };
 }  // namespace phx
 
diff --git a/library/phx/input/openvr_controller_system.cpp b/library/phx/input/openvr_controller_model_system.cpp
similarity index 75%
rename from library/phx/input/openvr_controller_system.cpp
rename to library/phx/input/openvr_controller_model_system.cpp
index eecf2be7c87c8437f68888ac9670a81bb7123009..643328fc5d8d7169f2b312a6ea9573d3dda354b2 100644
--- a/library/phx/input/openvr_controller_system.cpp
+++ b/library/phx/input/openvr_controller_model_system.cpp
@@ -20,7 +20,7 @@
 // limitations under the License.
 //------------------------------------------------------------------------------
 
-#include "phx/input/openvr_controller_system.hpp"
+#include "phx/input/openvr_controller_model_system.hpp"
 
 #include <memory>
 #include <string>
@@ -29,6 +29,7 @@
 #include "phx/core/logger.hpp"
 #include "phx/display/display_system_openvr.hpp"
 #include "phx/input/openvr_controller_behavior.hpp"
+#include "phx/input/vr_controller.hpp"
 #include "phx/rendering/components/material_handle.hpp"
 #include "phx/rendering/components/mesh_handle.hpp"
 #include "phx/rendering/components/transform.hpp"
@@ -38,18 +39,11 @@
 
 namespace phx {
 
-OpenVRControllerSystem::OpenVRControllerSystem(Engine* engine,
-                                               DisplaySystem* display_system)
-    : System(engine) {
-  // find HMD
-  hmd_ = dynamic_cast<DisplaySystemOpenVR*>(display_system)->GetHMD();
-  if (hmd_ == nullptr) {
-    error("Cannot use OpenVRControllerSystem without an HMD!");
-    return;
-  }
-
+OpenVRControllerModelSystem::OpenVRControllerModelSystem(
+    Engine* engine, DeviceSystem* device_system)
+    : System(engine), device_system_(device_system) {
   auto& resource_manager = ResourceManager::instance();
-  auto openvr_loader = std::make_unique<OpenVRResourceLoader>(hmd_);
+  auto openvr_loader = std::make_unique<OpenVRResourceLoader>(device_system);
   resource_manager.RegisterResourceType("openVR", std::move(openvr_loader));
 }
 
@@ -58,11 +52,11 @@ OpenVRControllerSystem::OpenVRControllerSystem(Engine* engine,
 #pragma clang diagnostic ignored "-Wmissing-prototypes"
 #endif
 
-Entity* OpenVRControllerSystem::AddController(
-    phx::Scene* scene, OpenVRControllerBehavior::Side side) {
+Entity* OpenVRControllerModelSystem::AddController(
+    phx::Scene* scene, VRController::ControllerSide side) {
   auto& resource_manager = ResourceManager::instance();
   std::string side_string =
-      side == OpenVRControllerBehavior::LEFT ? "left" : "right";
+      side == VRController::LEFT_CONTROLLER ? "left" : "right";
 
   ResourceDeclaration mesh_declaration{
       {"TYPE", "openVR"}, {"OpenVR_type", "mesh"}, {"side", side_string}};
@@ -82,9 +76,9 @@ Entity* OpenVRControllerSystem::AddController(
   return nullptr;
 }
 
-Entity* OpenVRControllerSystem::AddControllerEntity(
+Entity* OpenVRControllerModelSystem::AddControllerEntity(
     phx::Scene* scene, ResourcePointer<Mesh> mesh,
-    ResourcePointer<Material> material, OpenVRControllerBehavior::Side side) {
+    ResourcePointer<Material> material, VRController::ControllerSide side) {
   Entity* controller = scene->CreateEntity();
   controller->AddComponent<MeshHandle>()->SetMesh(mesh);
   controller->AddComponent<Transform>();
@@ -98,10 +92,9 @@ Entity* OpenVRControllerSystem::AddControllerEntity(
 #pragma clang diagnostic pop
 #endif
 
-void OpenVRControllerSystem::Update(const FrameTimer::TimeInfo&) {
+void OpenVRControllerModelSystem::Update(const FrameTimer::TimeInfo&) {
   // check which controllers are active and update their scene representation,
   // if necessary
-  if (!hmd_) return;
   auto scene = GetEngine()->GetScene();
   if (scene == nullptr) return;
 
@@ -112,31 +105,30 @@ void OpenVRControllerSystem::Update(const FrameTimer::TimeInfo&) {
       GetEngine()->GetEntitiesWithComponents<OpenVRControllerBehavior>();
   for (auto entity : controller_entities) {
     if (entity->GetFirstComponent<OpenVRControllerBehavior>()->GetSide() ==
-        OpenVRControllerBehavior::LEFT) {
+        VRController::LEFT_CONTROLLER) {
       left_controller_entity = entity;
     } else {
       right_controller_entity = entity;
     }
   }
 
-  auto controller_indices = hmd_->GetControllerIndices();
   bool left_controller_active = false;
   bool right_controller_active = false;
-  for (auto idx : controller_indices) {
-    // is it a left controller?
-    auto role = hmd_->GetControllerRoleForTrackedDeviceIndex(idx);
-    if (role == vr::TrackedControllerRole_LeftHand) {
+
+  for (auto controller : device_system_->GetDevices<VRController>()) {
+    if (!controller->IsActive()) continue;
+    if (controller->GetSide() == VRController::LEFT_CONTROLLER) {
       // do we have a left controller in the scene?
       if (left_controller_entity == nullptr) {
         // create that controller
         left_controller_entity =
-            AddController(scene.get(), OpenVRControllerBehavior::LEFT);
+            AddController(scene.get(), VRController::LEFT_CONTROLLER);
       }
       left_controller_active = true;
-    } else if (role == vr::TrackedControllerRole_RightHand) {
+    } else if (controller->GetSide() == VRController::RIGHT_CONTROLLER) {
       if (right_controller_entity == nullptr) {
         right_controller_entity =
-            AddController(scene.get(), OpenVRControllerBehavior::RIGHT);
+            AddController(scene.get(), VRController::RIGHT_CONTROLLER);
       }
       right_controller_active = true;
     }
diff --git a/library/phx/input/openvr_controller_system.hpp b/library/phx/input/openvr_controller_model_system.hpp
similarity index 64%
rename from library/phx/input/openvr_controller_system.hpp
rename to library/phx/input/openvr_controller_model_system.hpp
index 607f72a46f02a0db1a33f1457ada5583f27f3e97..693e4d4d864091a269509d3f3f94a082536b5fc6 100644
--- a/library/phx/input/openvr_controller_system.hpp
+++ b/library/phx/input/openvr_controller_model_system.hpp
@@ -20,8 +20,8 @@
 // limitations under the License.
 //------------------------------------------------------------------------------
 
-#ifndef LIBRARY_PHX_INPUT_OPENVR_CONTROLLER_SYSTEM_HPP_
-#define LIBRARY_PHX_INPUT_OPENVR_CONTROLLER_SYSTEM_HPP_
+#ifndef LIBRARY_PHX_INPUT_OPENVR_CONTROLLER_MODEL_SYSTEM_HPP_
+#define LIBRARY_PHX_INPUT_OPENVR_CONTROLLER_MODEL_SYSTEM_HPP_
 
 #include <memory>
 #include <vector>
@@ -34,40 +34,43 @@ SUPPRESS_WARNINGS_END
 
 #include "phx/core/engine.hpp"
 #include "phx/core/system.hpp"
-#include "phx/display/display_system.hpp"
 #include "phx/display/hmd.hpp"
-#include "phx/input/openvr_controller_behavior.hpp"
 #include "phx/export.hpp"
+#include "phx/input/device_system.hpp"
+#include "phx/input/openvr_controller_behavior.hpp"
+#include "phx/input/vr_controller.hpp"
 
 namespace phx {
-class PHOENIX_EXPORT OpenVRControllerSystem : public System {
+class PHOENIX_EXPORT OpenVRControllerModelSystem : public System {
  public:
-  OpenVRControllerSystem() = delete;
-  OpenVRControllerSystem(const OpenVRControllerSystem&) = delete;
-  OpenVRControllerSystem(OpenVRControllerSystem&&) = default;
-  ~OpenVRControllerSystem() override = default;
+  OpenVRControllerModelSystem() = delete;
+  OpenVRControllerModelSystem(const OpenVRControllerModelSystem&) = delete;
+  OpenVRControllerModelSystem(OpenVRControllerModelSystem&&) = default;
+  ~OpenVRControllerModelSystem() override = default;
 
-  OpenVRControllerSystem& operator=(const OpenVRControllerSystem&) = delete;
-  OpenVRControllerSystem& operator=(OpenVRControllerSystem&&) = default;
+  OpenVRControllerModelSystem& operator=(const OpenVRControllerModelSystem&) =
+      delete;
+  OpenVRControllerModelSystem& operator=(OpenVRControllerModelSystem&&) =
+      default;
 
   void Update(const FrameTimer::TimeInfo&) override;
 
  protected:
   template <typename SystemType, typename... SystemArguments>
   friend SystemType* Engine::CreateSystem(SystemArguments&&... arguments);
-  OpenVRControllerSystem(Engine* engine, DisplaySystem* display_system);
+  OpenVRControllerModelSystem(Engine* engine, DeviceSystem* device_system);
 
  private:
   static Entity* AddControllerEntity(phx::Scene* scene,
                                      ResourcePointer<Mesh> mesh,
                                      ResourcePointer<Material> material,
-                                     OpenVRControllerBehavior::Side side);
+                                     VRController::ControllerSide side);
   static Entity* AddController(phx::Scene* scene,
-                               OpenVRControllerBehavior::Side side);
+                               VRController::ControllerSide side);
 
-  HMD* hmd_ = nullptr;
+  DeviceSystem* device_system_ = nullptr;
 };
 
 }  // namespace phx
 
-#endif  // LIBRARY_PHX_INPUT_OPENVR_CONTROLLER_SYSTEM_HPP_
+#endif  // LIBRARY_PHX_INPUT_OPENVR_CONTROLLER_MODEL_SYSTEM_HPP_
diff --git a/library/phx/input/tracked_device.cpp b/library/phx/input/tracked_device.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0800a10dc0068f407972ce018bb7a60a2d17636a
--- /dev/null
+++ b/library/phx/input/tracked_device.cpp
@@ -0,0 +1,139 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#include "phx/input/tracked_device.hpp"
+
+#include <vector>
+
+#include "phx/core/entity.hpp"
+#include "phx/core/logger.hpp"
+
+namespace phx {
+
+vr::IVRSystem* TrackedDevice::vr_system_ = nullptr;
+int TrackedDevice::reference_counter_ = 0;
+std::mutex TrackedDevice::openVR_init_mutex_;
+TrackedDevice::OpenVREventDistributer TrackedDevice::vr_event_distributer;
+std::vector<vr::TrackedDevicePose_t> TrackedDevice::last_wait_get_poses_;
+
+TrackedDevice::TrackedDevice() {
+  openVR_init_mutex_.lock();
+  if (vr_system_ == nullptr) {
+    vr::EVRInitError init_error = vr::VRInitError_None;
+    vr_system_ = vr::VR_Init(&init_error, vr::VRApplication_Scene);
+    if (vr_system_ == nullptr || init_error != vr::VRInitError_None) {
+      error("OpenVR cannot be initialized with error-code: {}", init_error);
+      throw std::runtime_error("OpenVR cannot be initialized!");
+    }
+  }
+  vr_event_distributer.AddDevice(this);
+  reference_counter_++;
+  openVR_init_mutex_.unlock();
+}
+
+TrackedDevice::~TrackedDevice() {
+  openVR_init_mutex_.lock();
+  reference_counter_--;
+  if (reference_counter_ == 0) {
+    vr::VR_Shutdown();
+    vr_system_ = nullptr;
+  }
+  vr_event_distributer.RemoveDevice(this);
+  openVR_init_mutex_.unlock();
+}
+
+void TrackedDevice::Update() {
+  if (id_ == vr::k_unTrackedDeviceIndexInvalid) return;
+
+  vr::TrackedDevicePose_t tracked_device_pose;
+  if (last_wait_get_poses_.size() == vr::k_unMaxTrackedDeviceCount) {
+    // the HMD has aquired the poses of all devices with WaitGetPoses()
+    tracked_device_pose = last_wait_get_poses_[id_];
+  } else {
+    // this tracked device has to get its pose itself directly from openVR
+    // this method however has a higher lag
+    vr::VRControllerState_t controller_state;
+    vr_system_->GetControllerStateWithPose(
+        vr::TrackingUniverseStanding, id_, &controller_state,
+        sizeof(controller_state), &tracked_device_pose);
+  }
+  if (tracked_device_pose.bPoseIsValid) {
+    pose_ = TransformToGlmMatrix(tracked_device_pose.mDeviceToAbsoluteTracking);
+  } else {
+    debug(
+        "[TrackedDevice] Tracked Device pose is invalid, device is marked as "
+        "invalid. The actual implementation has to find another valid index");
+    id_ = vr::k_unTrackedDeviceIndexInvalid;
+  }
+
+  vr_event_distributer.Update();
+}
+
+glm::mat4 TrackedDevice::GetPose() const { return pose_; }
+
+bool TrackedDevice::IsActive() const {
+  return id_ != vr::k_unTrackedDeviceIndexInvalid;
+}
+
+void TrackedDevice::OpenVREventDistributer::AddDevice(TrackedDevice* device) {
+  tracked_devices_.push_back(device);
+}
+
+void TrackedDevice::OpenVREventDistributer::RemoveDevice(
+    TrackedDevice* device) {
+  auto iterator =
+      std::remove_if(tracked_devices_.begin(), tracked_devices_.end(),
+                     [device](Device* iteratee) { return device == iteratee; });
+  tracked_devices_.erase(iterator, tracked_devices_.end());
+}
+
+void TrackedDevice::OpenVREventDistributer::Update() {
+  vr::VREvent_t event;
+  while (vr::VRSystem()->PollNextEvent(&event, sizeof(event))) {
+    for (TrackedDevice* device : tracked_devices_) {
+      device->OnOpenVREvent(event);
+    }
+  }
+}
+glm::mat4 TrackedDevice::TransformToGlmMatrix(const vr::HmdMatrix34_t& mat) {
+  return glm::mat4(mat.m[0][0], mat.m[1][0], mat.m[2][0], 0.0f, mat.m[0][1],
+                   mat.m[1][1], mat.m[2][1], 0.0f, mat.m[0][2], mat.m[1][2],
+                   mat.m[2][2], 0.0f, mat.m[0][3], mat.m[1][3], mat.m[2][3],
+                   1.0f);
+}
+
+glm::mat4 TrackedDevice::TransformToGlmMatrix(const vr::HmdMatrix44_t& mat) {
+  return glm::mat4(mat.m[0][0], mat.m[1][0], mat.m[2][0], mat.m[3][0],
+                   mat.m[0][1], mat.m[1][1], mat.m[2][1], mat.m[3][1],
+                   mat.m[0][2], mat.m[1][2], mat.m[2][2], mat.m[3][2],
+                   mat.m[0][3], mat.m[1][3], mat.m[2][3], mat.m[3][3]);
+}
+
+std::vector<vr::TrackedDeviceIndex_t> TrackedDevice::GetDeviceIndicesForClass(
+    vr::ETrackedDeviceClass device_class) {
+  std::vector<std::uint32_t> indices(vr::k_unMaxTrackedDeviceCount);
+  vr::VRSystem()->GetSortedTrackedDeviceIndicesOfClass(
+      device_class, indices.data(), static_cast<std::uint32_t>(indices.size()));
+  return indices;
+}
+
+}  // namespace phx
diff --git a/library/phx/input/tracked_device.hpp b/library/phx/input/tracked_device.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d02805708c3326d5f9e6cbf24954f938b4e8c79f
--- /dev/null
+++ b/library/phx/input/tracked_device.hpp
@@ -0,0 +1,87 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#ifndef LIBRARY_PHX_INPUT_TRACKED_DEVICE_HPP_
+#define LIBRARY_PHX_INPUT_TRACKED_DEVICE_HPP_
+
+#include <vector>
+
+#include "phx/input/device.hpp"
+#include "phx/resources/types/material.hpp"
+#include "phx/resources/types/mesh.hpp"
+#include "phx/suppress_warnings.hpp"
+
+SUPPRESS_WARNINGS_BEGIN
+#include "glm/mat4x4.hpp"
+
+#include "openvr.h"  //NOLINT
+SUPPRESS_WARNINGS_END
+
+#include "phx/export.hpp"
+
+namespace phx {
+
+class PHOENIX_EXPORT TrackedDevice : public Device {
+ public:
+  TrackedDevice();
+  virtual ~TrackedDevice();
+
+  virtual void Update();
+
+  glm::mat4 GetPose() const;
+  bool IsActive() const;
+
+  virtual void UpdateDeviceIndex() = 0;
+  virtual void OnOpenVREvent(const vr::VREvent_t& event) = 0;
+
+ protected:
+  class OpenVREventDistributer {
+   public:
+    void Update();
+    void AddDevice(TrackedDevice* device);
+    void RemoveDevice(TrackedDevice* device);
+
+   private:
+    std::vector<TrackedDevice*> tracked_devices_;
+  };
+
+  static vr::IVRSystem* vr_system_;
+  static OpenVREventDistributer vr_event_distributer;
+  static std::vector<vr::TrackedDevicePose_t> last_wait_get_poses_;
+  glm::mat4 pose_;
+
+  static glm::mat4 TransformToGlmMatrix(const vr::HmdMatrix34_t& mat);
+  static glm::mat4 TransformToGlmMatrix(const vr::HmdMatrix44_t& mat);
+
+  std::vector<vr::TrackedDeviceIndex_t> GetDeviceIndicesForClass(
+      vr::ETrackedDeviceClass device_class);
+
+  vr::TrackedDeviceIndex_t id_ = vr::k_unTrackedDeviceIndexInvalid;
+
+ private:
+  static int reference_counter_;
+  static std::mutex openVR_init_mutex_;
+};
+
+}  // namespace phx
+
+#endif  // LIBRARY_PHX_INPUT_TRACKED_DEVICE_HPP_
diff --git a/library/phx/input/vr_controller.cpp b/library/phx/input/vr_controller.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..97d39c52d8aa704808f833300da0e80049152fa4
--- /dev/null
+++ b/library/phx/input/vr_controller.cpp
@@ -0,0 +1,141 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#include "phx/input/vr_controller.hpp"
+
+#include <string>
+#include <vector>
+
+#include "phx/core/logger.hpp"
+
+namespace phx {
+
+VRController::VRController(ControllerSide side) : TrackedDevice(), side_(side) {
+  VRController::Update();
+}
+
+void VRController::Update() {
+  if (id_ == vr::k_unTrackedDeviceIndexInvalid ||
+      GetControllerRoleForTrackedDeviceIndex(id_) != side_) {
+    UpdateDeviceIndex();
+  }
+  TrackedDevice::Update();
+}
+
+void VRController::OnOpenVREvent(const vr::VREvent_t& event) {
+  if (event.trackedDeviceIndex != id_) return;
+  if (event.eventType == vr::VREvent_ButtonPress) {
+    button_signal_(static_cast<ButtonId>(event.data.controller.button),
+                   ButtonEvent::BUTTON_PRESSED);
+  } else if (event.eventType == vr::VREvent_ButtonUnpress) {
+    button_signal_(static_cast<ButtonId>(event.data.controller.button),
+                   ButtonEvent::BUTTON_RELEASED);
+  } else if (event.eventType == vr::VREvent_ButtonTouch) {
+    button_signal_(static_cast<ButtonId>(event.data.controller.button),
+                   ButtonEvent::BUTTON_TOUCH);
+  } else if (event.eventType == vr::VREvent_ButtonUntouch) {
+    button_signal_(static_cast<ButtonId>(event.data.controller.button),
+                   ButtonEvent::BUTTON_UNTOUCH);
+  }
+}
+
+void VRController::UpdateDeviceIndex() {
+  auto controller_indices =
+      GetDeviceIndicesForClass(vr::TrackedDeviceClass_Controller);
+  id_ = vr::k_unTrackedDeviceIndexInvalid;
+  for (auto index : controller_indices) {
+    if (index != vr::k_unTrackedDeviceIndex_Hmd &&
+        GetControllerRoleForTrackedDeviceIndex(index) == side_) {
+      id_ = index;
+      return;
+    }
+  }
+}
+
+VRController::ControllerSide VRController::GetSide() const { return side_; }
+
+vr::RenderModel_t* VRController::GetModel() {
+  if (id_ == vr::k_unTrackedDeviceIndexInvalid) {
+    warn("Try to obtain controller model of an invalid controller");
+    return nullptr;
+  }
+
+  std::string rendermodel_name;
+  rendermodel_name.resize(vr::k_unMaxPropertyStringSize);
+
+  vr::VRSystem()->GetStringTrackedDeviceProperty(
+      id_, vr::Prop_RenderModelName_String, &rendermodel_name[0],
+      vr::k_unMaxPropertyStringSize);
+
+  vr::RenderModel_t* model = nullptr;
+  while (vr::VRRenderModels()->LoadRenderModel_Async(
+             &rendermodel_name[0], &model) == vr::VRRenderModelError_Loading) {
+    std::this_thread::sleep_for(std::chrono::milliseconds(50));
+  }
+
+  return model;
+}
+
+boost::signals2::connection VRController::RegisterButtonSignal(
+    const std::function<void(ButtonId, ButtonEvent)>& callback) {
+  return button_signal_.connect(callback);
+}
+
+glm::vec2 VRController::GetAxesValue(AxesId axes_id) {
+  if (!IsActive()) return glm::vec2(0.0f);
+
+  vr::TrackedDevicePose_t tracked_device_pose;
+  vr::VRControllerState_t controller_state;
+  vr_system_->GetControllerStateWithPose(
+      vr::TrackingUniverseStanding, id_, &controller_state,
+      sizeof(controller_state), &tracked_device_pose);
+
+  for (uint32_t i = 0; i < vr::k_unControllerStateAxisCount; i++) {
+    vr::EVRControllerAxisType type = static_cast<vr::EVRControllerAxisType>(
+        vr_system_->GetInt32TrackedDeviceProperty(
+            id_, static_cast<vr::ETrackedDeviceProperty>(
+                     vr::Prop_Axis0Type_Int32 + i)));
+    if (type == static_cast<vr::EVRControllerAxisType>(axes_id)) {
+      vr::VRControllerAxis_t axes = controller_state.rAxis[i];
+      return glm::vec2(axes.x, axes.y);
+    }
+  }
+  warn("VRController device  does not provide the requested axes: {} ",
+       vr_system_->GetControllerAxisTypeNameFromEnum(
+           static_cast<vr::EVRControllerAxisType>(axes_id)));
+  return glm::vec2(0.0f);
+}
+
+VRController::ControllerSide
+VRController::GetControllerRoleForTrackedDeviceIndex(
+    vr::TrackedDeviceIndex_t device_index) const {
+  switch (vr_system_->GetControllerRoleForTrackedDeviceIndex(device_index)) {
+    case vr::TrackedControllerRole_LeftHand:
+      return LEFT_CONTROLLER;
+    case vr::TrackedControllerRole_RightHand:
+      return RIGHT_CONTROLLER;
+    case vr::TrackedControllerRole_Invalid:
+      break;
+  }
+  return INVALID_CONTROLLER;
+}
+}  // namespace phx
diff --git a/library/phx/input/vr_controller.hpp b/library/phx/input/vr_controller.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..e9402b97223a14dd81f67606f35316c9be572e9a
--- /dev/null
+++ b/library/phx/input/vr_controller.hpp
@@ -0,0 +1,106 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#ifndef LIBRARY_PHX_INPUT_VR_CONTROLLER_HPP_
+#define LIBRARY_PHX_INPUT_VR_CONTROLLER_HPP_
+
+#include <memory>
+
+#define BOOST_BIND_NO_PLACEHOLDERS
+// otherwise boosts _ placeholders conflict with trompeloeil ones
+#include "boost/signals2/connection.hpp"
+#include "boost/signals2/signal.hpp"
+
+#include "phx/input/tracked_device.hpp"
+#include "phx/resources/types/material.hpp"
+#include "phx/resources/types/mesh.hpp"
+#include "phx/suppress_warnings.hpp"
+
+SUPPRESS_WARNINGS_BEGIN
+#include "glm/mat4x4.hpp"
+
+#include "openvr.h"  //NOLINT
+SUPPRESS_WARNINGS_END
+
+#include "phx/export.hpp"
+
+namespace phx {
+
+class OpenVRResourceLoader;
+
+class PHOENIX_EXPORT VRController : public TrackedDevice {
+ public:
+  enum ControllerSide {
+    RIGHT_CONTROLLER = vr::TrackedControllerRole_RightHand,
+    LEFT_CONTROLLER = vr::TrackedControllerRole_LeftHand,
+    INVALID_CONTROLLER = vr::TrackedControllerRole_Invalid
+  };
+
+  typedef vr::EVRButtonId ButtonId;
+
+  enum ButtonEvent {
+    BUTTON_PRESSED,
+    BUTTON_RELEASED,
+    BUTTON_TOUCH,
+    BUTTON_UNTOUCH
+  };
+
+  enum AxesId {
+    AXES_TRACKPAD = vr::k_eControllerAxis_TrackPad,
+    AXES_JOYSTICK = vr::k_eControllerAxis_Joystick,
+    AXES_TRIGGER = vr::k_eControllerAxis_Trigger
+  };
+
+  explicit VRController(ControllerSide side);
+
+  virtual ~VRController() = default;
+
+  void Update() override;
+  void OnOpenVREvent(const vr::VREvent_t& event) override;
+
+  void UpdateDeviceIndex() override;
+
+  ControllerSide GetSide() const;
+
+  vr::RenderModel_t* GetModel();
+
+  boost::signals2::connection RegisterButtonSignal(
+      const std::function<void(ButtonId, ButtonEvent)>& callback);
+
+  // x ranges from -1.0 to 1.0 for joysticks and track pads and from 0.0 to 1.0
+  // for triggers were 0 is fully released.
+  // y ranges from -1.0 to 1.0 for joysticks and track pads and is always 0.0
+  // for triggers.
+  glm::vec2 GetAxesValue(AxesId id);
+
+ private:
+  ControllerSide GetControllerRoleForTrackedDeviceIndex(
+      vr::TrackedDeviceIndex_t device_index) const;
+
+  boost::signals2::signal<void(ButtonId, ButtonEvent)> button_signal_;
+
+  ControllerSide side_;
+};
+
+}  // namespace phx
+
+#endif  // LIBRARY_PHX_INPUT_VR_CONTROLLER_HPP_
diff --git a/library/phx/resources/loaders/openvr_resource_loader.cpp b/library/phx/resources/loaders/openvr_resource_loader.cpp
index 3c24d99ef7846cdd8f5e5e6e9005e0bbf055c9e5..230337716f6077f804325f84ba4bc459cb6d415c 100644
--- a/library/phx/resources/loaders/openvr_resource_loader.cpp
+++ b/library/phx/resources/loaders/openvr_resource_loader.cpp
@@ -22,18 +22,25 @@
 
 #include "phx/resources/loaders/openvr_resource_loader.hpp"
 
+#include <algorithm>
 #include <memory>
 #include <string>
+#include <utility>
+#include <vector>
 
 #include "phx/core/logger.hpp"
+#include "phx/input/device_system.hpp"
+#include "phx/input/vr_controller.hpp"
+#include "phx/resources/resource_manager.hpp"
 #include "phx/resources/types/material.hpp"
 
 namespace phx {
 
-OpenVRResourceLoader::OpenVRResourceLoader(HMD *hmd) : hmd_(hmd) {}
+OpenVRResourceLoader::OpenVRResourceLoader(DeviceSystem* device_system)
+    : device_system_(device_system) {}
 
 std::unique_ptr<phx::Resource> OpenVRResourceLoader::Load(
-    const ResourceDeclaration &declaration) {
+    const ResourceDeclaration& declaration) {
   if (declaration.find("OpenVR_type") == declaration.end() ||
       !declaration["OpenVR_type"].is_string()) {
     warn(
@@ -43,15 +50,15 @@ std::unique_ptr<phx::Resource> OpenVRResourceLoader::Load(
     return nullptr;
   }
   std::string type = declaration["OpenVR_type"];
-  HMD::Controller controller_side = HMD::Controller::LEFT_CONTROLLER;
+  VRController::ControllerSide controller_side = VRController::LEFT_CONTROLLER;
   if (declaration.find("side") != declaration.end() &&
       declaration["side"] == "right") {
-    controller_side = HMD::Controller::RIGHT_CONTROLLER;
+    controller_side = VRController::RIGHT_CONTROLLER;
   }
   // otherwise default left
 
   if (type == "material") {
-    return hmd_->GetControllerMaterial(controller_side);
+    return GetMaterial(controller_side);
   }
   if (type == "texture") {
     int texture_id = 0;
@@ -64,12 +71,11 @@ std::unique_ptr<phx::Resource> OpenVRResourceLoader::Load(
     } else {
       texture_id = declaration["texture_id"];
     }
-    return hmd_->GetControllerTexture(texture_id);
+    return GetTexture(texture_id);
   }
   if (type == "mesh") {
-    return hmd_->GetControllerMesh(controller_side);
+    return GetMesh(controller_side);
   }
-
   warn(
       "OpenVRResource with OpenVR_type {}, cannot be loaded. Full declaration: "
       "{}",
@@ -77,4 +83,92 @@ std::unique_ptr<phx::Resource> OpenVRResourceLoader::Load(
   return nullptr;
 }
 
+vr::RenderModel_t* OpenVRResourceLoader::GetModel(
+    VRController::ControllerSide side) {
+  for (auto controller : device_system_->GetDevices<VRController>()) {
+    if (controller->GetSide() == side) return controller->GetModel();
+  }
+
+  warn(
+      "[OpenVRResourceLoader::GetModel] unable to find controller with side {}",
+      side == VRController::RIGHT_CONTROLLER ? "right" : "left");
+  return nullptr;
+}
+
+std::unique_ptr<phx::Mesh> OpenVRResourceLoader::GetMesh(
+    VRController::ControllerSide side) {
+  auto model = GetModel(side);
+  if (model == nullptr) {
+    return nullptr;
+  }
+  auto mesh = std::make_unique<phx::Mesh>();
+  std::vector<glm::vec3> vertices;
+  std::vector<glm::vec3> normals;
+  std::vector<glm::vec2> texcoords;
+  for (std::size_t i = 0; i < model->unVertexCount; i++) {
+    vertices.push_back(glm::vec3(model->rVertexData[i].vPosition.v[0],
+                                 model->rVertexData[i].vPosition.v[1],
+                                 model->rVertexData[i].vPosition.v[2]));
+    normals.push_back(glm::vec3(model->rVertexData[i].vNormal.v[0],
+                                model->rVertexData[i].vNormal.v[1],
+                                model->rVertexData[i].vNormal.v[2]));
+    texcoords.push_back(glm::vec2(model->rVertexData[i].rfTextureCoord[0],
+                                  model->rVertexData[i].rfTextureCoord[1]));
+  }
+  std::vector<unsigned int> indices;
+  for (std::size_t i = 0; i < model->unTriangleCount * 3; i++) {
+    indices.push_back(model->rIndexData[i]);
+  }
+  mesh->SetVertices(std::move(vertices));
+  mesh->SetNormals(std::move(normals));
+  mesh->SetTextureCoords(std::move(texcoords));
+  mesh->SetIndices(std::move(indices));
+  return mesh;
+}
+
+std::unique_ptr<Material> OpenVRResourceLoader::GetMaterial(
+    VRController::ControllerSide side) {
+  auto model = GetModel(side);
+  if (model == nullptr) return nullptr;
+
+  auto material = std::make_unique<phx::Material>();
+  material->SetAmbientColor(glm::vec3(0.1, 0.1, 0.1));
+  material->SetSpecularColor(glm::vec3(0.3, 0.3, 0.3));
+
+  auto texture = ResourceManager::instance().DeclareResource<Image>(
+      {{"TYPE", "openVR"},
+       {"OpenVR_type", "texture"},
+       {"texture_id", model->diffuseTextureId},
+       {"side", side == VRController::LEFT_CONTROLLER ? "left" : "right"}});
+  texture.Load();
+  material->SetDiffuseImage(texture);
+
+  return material;
+}
+
+std::unique_ptr<Image> OpenVRResourceLoader::GetTexture(int id) {
+  vr::RenderModel_TextureMap_t* texture_map;
+  while (vr::VRRenderModels()->LoadTexture_Async(id, &texture_map) ==
+         vr::VRRenderModelError_Loading) {
+    std::this_thread::sleep_for(std::chrono::milliseconds(50));
+  }
+  if (texture_map == nullptr) {
+    return nullptr;
+  }
+
+  std::vector<unsigned char> image_data(texture_map->unWidth *
+                                        texture_map->unHeight * 4);
+  std::copy(texture_map->rubTextureMapData,
+            texture_map->rubTextureMapData + image_data.size(),
+            image_data.begin());
+  auto image = std::make_unique<phx::Image>(
+      &image_data[0],
+      std::array<std::size_t, 2>{
+          {static_cast<std::size_t>(texture_map->unWidth),
+           static_cast<std::size_t>(texture_map->unHeight)}},
+      32);
+
+  return image;
+}
+
 }  // namespace phx
diff --git a/library/phx/resources/loaders/openvr_resource_loader.hpp b/library/phx/resources/loaders/openvr_resource_loader.hpp
index 357ff8ad9489e2ac269f962b8a48979d884c26d9..3ccd32ea744e23e21de87a83617d63d739bec047 100644
--- a/library/phx/resources/loaders/openvr_resource_loader.hpp
+++ b/library/phx/resources/loaders/openvr_resource_loader.hpp
@@ -33,17 +33,18 @@ SUPPRESS_WARNINGS_BEGIN
 SUPPRESS_WARNINGS_END
 
 #include "phx/export.hpp"
-#include "phx/display/hmd.hpp"
-#include "phx/resources/types/mesh.hpp"
+#include "phx/input/device_system.hpp"
+#include "phx/input/vr_controller.hpp"
 #include "phx/resources/resource_declaration.hpp"
 #include "phx/resources/resource_load_strategy.hpp"
+#include "phx/resources/types/mesh.hpp"
 
 namespace phx {
 class Mesh;
 
 class PHOENIX_EXPORT OpenVRResourceLoader final : public ResourceLoadStrategy {
  public:
-  explicit OpenVRResourceLoader(HMD *hmd);
+  explicit OpenVRResourceLoader(DeviceSystem *device_system);
   OpenVRResourceLoader(const OpenVRResourceLoader &) = delete;
   OpenVRResourceLoader(OpenVRResourceLoader &&) = delete;
   ~OpenVRResourceLoader() override = default;
@@ -53,8 +54,12 @@ class PHOENIX_EXPORT OpenVRResourceLoader final : public ResourceLoadStrategy {
 
   std::unique_ptr<Resource> Load(const ResourceDeclaration &file_name) override;
 
- private:
-  HMD *hmd_;
+  vr::RenderModel_t *GetModel(VRController::ControllerSide side);
+  std::unique_ptr<Mesh> GetMesh(VRController::ControllerSide side);
+  std::unique_ptr<Material> GetMaterial(VRController::ControllerSide side);
+  std::unique_ptr<Image> GetTexture(int id);
+
+  DeviceSystem *device_system_ = nullptr;
 };
 
 }  // namespace phx
diff --git a/library/phx/setup.cpp b/library/phx/setup.cpp
index 0c0a862450954383f9621a8cc40f27a5473e5449..895d4eb3251b1257004b50e5222aeb558c984487 100644
--- a/library/phx/setup.cpp
+++ b/library/phx/setup.cpp
@@ -27,6 +27,8 @@
 #include <string>
 #include <utility>
 
+#include "input/device_system.hpp"
+#include "input/vr_controller.hpp"
 #include "phx/core/component.hpp"
 #include "phx/core/engine.hpp"
 #include "phx/core/logger.hpp"
@@ -36,7 +38,7 @@
 #include "phx/display/display_system_window.hpp"
 #include "phx/display/hmd.hpp"
 #include "phx/input/input_system.hpp"
-#include "phx/input/openvr_controller_system.hpp"
+#include "phx/input/openvr_controller_model_system.hpp"
 #include "phx/rendering/backend/render_target.hpp"
 #include "phx/rendering/render_passes/blit_pass.hpp"
 #include "phx/rendering/render_passes/clear_pass.hpp"
@@ -56,16 +58,21 @@ std::unique_ptr<Engine> Setup::CreateDefaultEngine(bool use_hmd_if_available) {
   engine->CreateSystem<InputSystem>()->AddQuitCallback(
       [engine_ptr]() { engine_ptr->Stop(); });
 
+  auto device_system = engine->CreateSystem<DeviceSystem>();
+
   auto displaysys_window = engine->CreateSystem<DisplaySystemWindow>();
-  DisplaySystemOpenVR* displaysys_hmd = nullptr;
+  DisplaySystemOpenVR* displaysys_openVR = nullptr;
   bool using_hmd = false;
 
   if (HMD::IsHMDPresent() && use_hmd_if_available) {
     info("An HMD is present so we use it");
     using_hmd = true;
 
-    displaysys_hmd = engine->CreateSystem<DisplaySystemOpenVR>();
-    displaysys_hmd->CreateHMD();
+    device_system->AddDevice<HMD>();
+    device_system->AddDevice<VRController>(VRController::LEFT_CONTROLLER);
+    device_system->AddDevice<VRController>(VRController::RIGHT_CONTROLLER);
+
+    displaysys_openVR = engine->CreateSystem<DisplaySystemOpenVR>();
   }
 
   const std::string window_title{using_hmd ? "Phoenix -- HMD Companion"
@@ -79,21 +86,24 @@ std::unique_ptr<Engine> Setup::CreateDefaultEngine(bool use_hmd_if_available) {
 
   // fix update order
   engine->MoveSystemToBack(displaysys_window);
+  if (displaysys_openVR != nullptr) engine->MoveSystemToBack(displaysys_openVR);
   engine->MoveSystemBefore(rendering_system, displaysys_window);
+  engine->MoveSystemBefore(device_system, rendering_system);
 
   // setup rendering and frame graph
-  if (using_hmd) {
-    auto tracking_system =
-        engine->CreateSystem<TrackingSystemOpenVR>(displaysys_hmd);
-    auto controller_system = engine->CreateSystem<OpenVRControllerSystem>(
-        engine->GetSystem<DisplaySystem>());
+  if (HMD::IsHMDPresent() && use_hmd_if_available) {
+    auto controller_model_system =
+        engine->CreateSystem<OpenVRControllerModelSystem>(
+            engine->GetSystem<DeviceSystem>());
+    auto tracking_system = engine->CreateSystem<TrackingSystemOpenVR>(
+        engine->GetSystem<DeviceSystem>());
 
-    displaysys_hmd->CreateRenderTargets(engine->GetScene().get());
+    displaysys_openVR->CreateRenderTargets(engine->GetScene().get());
     SetupDefaultFrameGraphOpenVR(rendering_system, engine.get());
 
     engine->MoveSystemBefore(tracking_system, rendering_system);
     engine->MoveSystemAfter(behavior_system, tracking_system);
-    engine->MoveSystemAfter(controller_system, tracking_system);
+    engine->MoveSystemAfter(controller_model_system, tracking_system);
   } else {
     displaysys_window->CreateRenderTarget(engine->GetScene().get(), 68.0f,
                                           0.01f, 1000.0f);
diff --git a/library/phx/tracking/tracking_system_openvr.cpp b/library/phx/tracking/tracking_system_openvr.cpp
index e101095e0ff421af0d03b59666e79bc2684abb70..8156bc3c4f16e7fb9a100611004a9b28a8f68932 100644
--- a/library/phx/tracking/tracking_system_openvr.cpp
+++ b/library/phx/tracking/tracking_system_openvr.cpp
@@ -25,23 +25,22 @@
 #include <memory>
 #include <string>
 
-#include "phx/core/logger.hpp"
 #include "phx/core/runtime_component.hpp"
 #include "phx/display/display_system_openvr.hpp"
 #include "phx/display/hmd.hpp"
+#include "phx/input/device_system.hpp"
+#include "phx/input/vr_controller.hpp"
 #include "phx/rendering/components/projection.hpp"
 #include "phx/rendering/components/transform.hpp"
 
 namespace phx {
 
 TrackingSystemOpenVR::TrackingSystemOpenVR(Engine* engine,
-                                           DisplaySystemOpenVR* display_system)
+                                           DeviceSystem* device_system)
     : System(engine) {
-  if (!display_system)
-    error("TrackingSystemOpenVR needs a valid DisplaySystemOpenVR.");
-  if (display_system->GetHMD()) {
-    CreateRuntimeEntities(engine->GetScene().get());
-    scene_changed_connection_ = engine->AddSceneChangedCallback(
+  if (device_system->GetDevices<HMD>().size() != 0) {
+    CreateRuntimeEntities(engine_->GetScene().get());
+    scene_changed_connection_ = engine_->AddSceneChangedCallback(
         [this](std::shared_ptr<Scene> old_scene,
                std::shared_ptr<Scene> new_scene) {
           OnSceneChanged(old_scene, new_scene);
@@ -54,43 +53,34 @@ TrackingSystemOpenVR::~TrackingSystemOpenVR() {
 }
 
 void TrackingSystemOpenVR::Update(const FrameTimer::TimeInfo&) {
-  const auto dispsys_openvr = engine_->GetSystem<DisplaySystemOpenVR>();
-  if (dispsys_openvr == nullptr) {
+  const auto device_system = engine_->GetSystem<DeviceSystem>();
+  if (device_system == nullptr) {
     return;
   }
-  const auto hmd = dispsys_openvr->GetHMD();
-  if (hmd == nullptr) {
-    return;
-  }
-  hmd->UpdateTrackedDevices();
-  const auto head_transformation = hmd->GetHeadTransformation();
-  if (hmd_entity_ != nullptr) {
-    hmd_entity_->GetFirstComponent<Transform>()->SetLocalMatrix(
-        head_transformation);
-    const auto left_eye_transformation = hmd->GetEyeToHeadMatrix(HMD::LEFT_EYE);
-    left_eye_entity_->GetFirstComponent<Transform>()->SetLocalMatrix(
-        left_eye_transformation);
-    const auto right_eye_transformation =
-        hmd->GetEyeToHeadMatrix(HMD::RIGHT_EYE);
-    right_eye_entity_->GetFirstComponent<Transform>()->SetLocalMatrix(
-        right_eye_transformation);
-
-    left_controller_entity_->GetFirstComponent<Transform>()->SetLocalMatrix(
-        hmd->GetLeftControllerTransformation());
-
-    right_controller_entity_->GetFirstComponent<Transform>()->SetLocalMatrix(
-        hmd->GetRightControllerTransformation());
-  }
+
+  auto hmds = device_system->GetDevices<HMD>();
+  if (hmds.size() != 1) return;
+  auto hmd = hmds[0];
+
+  hmd_entity_->GetFirstComponent<Transform>()->SetLocalMatrix(hmd->GetPose());
+  left_eye_entity_->GetFirstComponent<Transform>()->SetLocalMatrix(
+      hmd->GetEyeToHeadMatrix(HMD::LEFT_EYE));
+  right_eye_entity_->GetFirstComponent<Transform>()->SetLocalMatrix(
+      hmd->GetEyeToHeadMatrix(HMD::RIGHT_EYE));
+
+  left_controller_entity_->GetFirstComponent<Transform>()->SetLocalMatrix(
+      GetController(VRController::LEFT_CONTROLLER)->GetPose());
+
+  right_controller_entity_->GetFirstComponent<Transform>()->SetLocalMatrix(
+      GetController(VRController::RIGHT_CONTROLLER)->GetPose());
 }
 
 void TrackingSystemOpenVR::CreateRuntimeEntities(Scene* scene) {
-  const auto hmd = engine_->GetSystem<DisplaySystemOpenVR>()->GetHMD();
-  if (hmd == nullptr) {
-    return;
-  }
-  if (scene == nullptr) {
+  const auto device_system = engine_->GetSystem<DeviceSystem>();
+  if (device_system == nullptr || scene == nullptr) {
     return;
   }
+
   const auto virtual_platforms = scene->GetEntitiesWithComponents<
       phx::RuntimeComponent<phx::USER_PLATFORM>>();
   if (!virtual_platforms.empty()) {
@@ -102,18 +92,18 @@ void TrackingSystemOpenVR::CreateRuntimeEntities(Scene* scene) {
     hmd_entity_->AddComponent<RuntimeComponent<HEAD>>();
     auto hmd_transform = hmd_entity_->AddComponent<Transform>();
     hmd_transform->SetParent(virtual_platform_transform);
-    left_eye_entity_ = scene->CreateEntity();
 
+    left_eye_entity_ = scene->CreateEntity();
     left_eye_entity_->AddComponent<RuntimeComponent<LEFT_EYE>>();
     auto left_eye_transform = left_eye_entity_->AddComponent<Transform>();
     left_eye_transform->SetParent(hmd_transform);
     left_eye_entity_->AddComponent<Projection>();
-    right_eye_entity_ = scene->CreateEntity();
 
+    right_eye_entity_ = scene->CreateEntity();
     right_eye_entity_->AddComponent<RuntimeComponent<RIGHT_EYE>>();
     auto right_eye_transform = right_eye_entity_->AddComponent<Transform>();
-    right_eye_entity_->AddComponent<Projection>();
     right_eye_transform->SetParent(hmd_transform);
+    right_eye_entity_->AddComponent<Projection>();
 
     left_controller_entity_ = scene->CreateEntity();
     left_controller_entity_->AddComponent<RuntimeComponent<LEFT_CONTROLLER>>();
@@ -132,18 +122,34 @@ void TrackingSystemOpenVR::CreateRuntimeEntities(Scene* scene) {
 
 void TrackingSystemOpenVR::RemoveRuntimeEntities(Scene* scene) {
   scene->RemoveEntity(hmd_entity_);
+  hmd_entity_ = nullptr;
   scene->RemoveEntity(left_eye_entity_);
+  left_eye_entity_ = nullptr;
   scene->RemoveEntity(right_eye_entity_);
+  right_eye_entity_ = nullptr;
   scene->RemoveEntity(left_controller_entity_);
+  left_controller_entity_ = nullptr;
   scene->RemoveEntity(right_controller_entity_);
+  right_controller_entity_ = nullptr;
 }
 
 void TrackingSystemOpenVR::OnSceneChanged(std::shared_ptr<Scene> old_scene,
-                                    std::shared_ptr<Scene> new_scene) {
+                                          std::shared_ptr<Scene> new_scene) {
   RemoveRuntimeEntities(old_scene.get());
   CreateRuntimeEntities(new_scene.get());
 }
 
+VRController* TrackingSystemOpenVR::GetController(
+    VRController::ControllerSide side) {
+  for (auto controller :
+       engine_->GetSystem<DeviceSystem>()->GetDevices<VRController>()) {
+    if (controller->GetSide() == side) {
+      return controller;
+    }
+  }
+  return nullptr;
+}
+
 std::string TrackingSystemOpenVR::ToString() const { return "Tracking System"; }
 
 }  // namespace phx
diff --git a/library/phx/tracking/tracking_system_openvr.hpp b/library/phx/tracking/tracking_system_openvr.hpp
index d2cd2172a18097ebe5c7d6d48c7720993663a9b1..fd76bce5588bf3018e16ea8fb33dbc9ffbf253a9 100644
--- a/library/phx/tracking/tracking_system_openvr.hpp
+++ b/library/phx/tracking/tracking_system_openvr.hpp
@@ -30,8 +30,8 @@
 
 #include "phx/core/engine.hpp"
 #include "phx/core/system.hpp"
-#include "phx/display/display_system_openvr.hpp"
 #include "phx/export.hpp"
+#include "phx/input/vr_controller.hpp"
 
 SUPPRESS_WARNINGS_BEGIN
 #define BOOST_BIND_NO_PLACEHOLDERS
@@ -39,6 +39,7 @@ SUPPRESS_WARNINGS_BEGIN
 SUPPRESS_WARNINGS_END
 
 namespace phx {
+class DeviceSystem;
 
 class PHOENIX_EXPORT TrackingSystemOpenVR : public System {
  public:
@@ -55,7 +56,7 @@ class PHOENIX_EXPORT TrackingSystemOpenVR : public System {
   std::string ToString() const override;
 
  protected:
-  TrackingSystemOpenVR(Engine* engine, DisplaySystemOpenVR* display_system);
+  TrackingSystemOpenVR(Engine* engine, DeviceSystem* device_system);
 
  private:
   template <typename SystemType, typename... SystemArguments>
@@ -66,6 +67,8 @@ class PHOENIX_EXPORT TrackingSystemOpenVR : public System {
   void OnSceneChanged(std::shared_ptr<Scene> old_scene,
                       std::shared_ptr<Scene> new_scene);
 
+  VRController* GetController(VRController::ControllerSide side);
+
   Entity* hmd_entity_ = nullptr;
   Entity* left_eye_entity_ = nullptr;
   Entity* right_eye_entity_ = nullptr;
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index b1ed5f2b63fde5bf248365046d9842d0bcb17323..f63ed905d8e76ba508e35cf4c75092d0c56031d9 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -173,73 +173,25 @@ add_test_cpplint(NAME "phoenix-tests--cpplint"
 # specified via AUTOREMOVE_MOCKED_TEST_SOURCE_FROM
 autoremove_mocked_test_source_from(${PHOENIX_TEST_SOURCES})
 
-add_mocked_test(test_clear_pass
-  LIBRARIES phoenix
-  MOCKS opengl_mock
-)
-add_mocked_test(test_input_system
-  LIBRARIES phoenix
-  MOCKS openvr_mock sdl_mock
-)
-add_mocked_test(test_geometry_pass
-  LIBRARIES phoenix test_utilities
-  MOCKS opengl_mock sdl_mock
-)
-add_mocked_test(test_rendering_system
-  LIBRARIES phoenix
-  MOCKS opengl_mock sdl_mock
-)
-add_mocked_test(test_shader
-  LIBRARIES phoenix
-  MOCKS opengl_mock
-)
-add_mocked_test(test_display_system
-  LIBRARIES phoenix
-  MOCKS sdl_mock
-)
-add_mocked_test(test_engine
-  LIBRARIES phoenix test_utilities
-  MOCKS opengl_mock openvr_mock sdl_mock
-)
-add_mocked_test(test_tracking_system
-  LIBRARIES phoenix test_utilities
-  MOCKS openvr_mock sdl_mock
-)
-add_mocked_test(test_openvr_controller_system
-  LIBRARIES phoenix
-  MOCKS opengl_mock openvr_mock sdl_mock
-)
-add_mocked_test(test_assimp_loader
-  LIBRARIES phoenix test_utilities
-  MOCKS opengl_mock
-)
-add_mocked_test(test_model
-  LIBRARIES phoenix
-  MOCKS opengl_mock
-)
-add_mocked_test(test_scene_loader
-  LIBRARIES phoenix
-  MOCKS opengl_mock
-)
-
-add_mocked_test(integration_test_model_rendering
-  LIBRARIES phoenix test_utilities
-  MOCKS openvr_mock
-)
-add_mocked_test(integration_test_opengl_buffer_data_download
-  LIBRARIES phoenix test_utilities
-  MOCKS openvr_mock
-)
-add_mocked_test(integration_test_rendering
-  LIBRARIES phoenix test_utilities
-  MOCKS openvr_mock
-)
-add_mocked_test(integration_test_hmd
-  LIBRARIES phoenix test_utilities
-  MOCKS openvr_mock
-)
-
-
+add_mocked_test(test_clear_pass                     LIBRARIES phoenix                MOCKS opengl_mock)
+add_mocked_test(test_input_system                   LIBRARIES phoenix                MOCKS openvr_mock sdl_mock)
+add_mocked_test(test_geometry_pass                  LIBRARIES phoenix test_utilities MOCKS opengl_mock sdl_mock)
+add_mocked_test(test_rendering_system               LIBRARIES phoenix                MOCKS opengl_mock sdl_mock)
+add_mocked_test(test_shader                         LIBRARIES phoenix                MOCKS opengl_mock)
+add_mocked_test(test_display_system                 LIBRARIES phoenix                MOCKS sdl_mock)
+add_mocked_test(test_engine                         LIBRARIES phoenix test_utilities MOCKS opengl_mock openvr_mock sdl_mock)
+add_mocked_test(test_tracking_system                LIBRARIES phoenix test_utilities MOCKS openvr_mock sdl_mock)
+add_mocked_test(test_openvr_controller_model_system LIBRARIES phoenix                MOCKS opengl_mock openvr_mock sdl_mock)
+add_mocked_test(test_tracked_device                 LIBRARIES phoenix                MOCKS openvr_mock)
+add_mocked_test(test_vr_controller                  LIBRARIES phoenix test_utilities MOCKS openvr_mock)
+add_mocked_test(test_assimp_loader                  LIBRARIES phoenix test_utilities MOCKS opengl_mock)
+add_mocked_test(test_model                          LIBRARIES phoenix                MOCKS opengl_mock)
+add_mocked_test(test_scene_loader                   LIBRARIES phoenix                MOCKS opengl_mock)
+
+add_mocked_test(integration_test_model_rendering             LIBRARIES phoenix test_utilities MOCKS openvr_mock)
+add_mocked_test(integration_test_opengl_buffer_data_download LIBRARIES phoenix test_utilities MOCKS openvr_mock)
+add_mocked_test(integration_test_rendering                   LIBRARIES phoenix test_utilities MOCKS openvr_mock)
+add_mocked_test(integration_test_hmd                         LIBRARIES phoenix test_utilities MOCKS openvr_mock)
 
 
 get_unmocked_test_sources(PHOENIX_TEST_SOURCES)
diff --git a/tests/reference_images/hmd_test_left.png b/tests/reference_images/hmd_test_left.png
index 9989f7ae4601b0eb1fcd961ccba454ddf6fdd45d..2f975671215cd6917f0d62a78ec76a8ea84191be 100644
--- a/tests/reference_images/hmd_test_left.png
+++ b/tests/reference_images/hmd_test_left.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:e4a6d5d6c1eed96056d964381617ec97dd3b6c9d26717cdc55c1e66c8343feed
-size 115516
+oid sha256:52c050bf258acaf19f3914bcc2792e4b130cc07e8c7017b8a4c5b69dcccec76f
+size 116885
diff --git a/tests/reference_images/hmd_test_right.png b/tests/reference_images/hmd_test_right.png
index 33f5fa2758442f23f4bcf04d353ea2e6661cc9a6..2519d5d6775c6b2fec9dbdf1083ac6ad7cd6fbf4 100644
--- a/tests/reference_images/hmd_test_right.png
+++ b/tests/reference_images/hmd_test_right.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:e07eb1a9c5bfea732801cbd4b6d6321c7797d815645b23fa205a14fa50d76b3d
-size 115419
+oid sha256:a9ea7817c7c1254c1765bf4a0977dd7b430d8419d1355f60b92dce461befae35
+size 116057
diff --git a/tests/src/integration_test_hmd.cpp b/tests/src/integration_test_hmd.cpp
index 7babf1ca229f2eb11ea665b3b9ce7ba0b4e2c82d..ee8600182ee2cb73f9084dcdd4d389e9e660ebc0 100644
--- a/tests/src/integration_test_hmd.cpp
+++ b/tests/src/integration_test_hmd.cpp
@@ -27,10 +27,10 @@
 
 #include "catch/catch.hpp"
 
-#include "phx/suppress_warnings.hpp"
-
 #include "phx/core/engine.hpp"
 #include "phx/core/runtime_component.hpp"
+#include "phx/display/hmd.hpp"
+#include "phx/input/device_system.hpp"
 #include "phx/rendering/backend/opengl_image_buffer_data.hpp"
 #include "phx/rendering/components/light.hpp"
 #include "phx/rendering/components/transform.hpp"
@@ -38,12 +38,11 @@
 #include "phx/resources/resource_utils.hpp"
 #include "phx/setup.hpp"
 
-SUPPRESS_WARNINGS_BEGIN
 #include "mocks/openvr_mock.hpp"
 
 #include "gl/texture.hpp"
-SUPPRESS_WARNINGS_END
 
+#include "test_utilities/glm_mat4.hpp"
 #include "test_utilities/opengl_buffer_data_comparison.hpp"
 
 #include "trompeloeil.hpp"
@@ -158,7 +157,6 @@ SCENARIO("If a HMD is present we render for both eyes including controllers",
     ::MoveUserPlatform(engine.get());
 
     WHEN("We run the engine") {
-      // TODO(JW): controller models should also be rendered
       engine->Run();
       THEN("Submit() is called for both eyes submitting the expected images") {
         ::CheckSimilarity(::left_tex_id, "hmd_test_left.png");
@@ -167,3 +165,68 @@ SCENARIO("If a HMD is present we render for both eyes including controllers",
     }
   }
 }
+
+namespace {
+
+glm::mat4 ToMat4(float mat[12]) {
+  glm::mat4 result;
+  for (int i = 0; i < 12; ++i) {
+    result[i % 4][i / 4] = mat[i];
+  }
+  return result;
+}
+}  // namespace
+
+SCENARIO("The Eye to distance is changed via vive's eye distance knob",
+         "[phx][phx::HMD]") {
+  GIVEN("A standard engine that runs one frame") {
+    OPENVR_MOCK_ALLOW_ANY_CALL
+    phx::Engine engine;
+    phx::DeviceSystem* device_system = engine.CreateSystem<phx::DeviceSystem>();
+    auto hmd = device_system->AddDevice<phx::HMD>();
+
+    REQUIRE(hmd->GetEyeToHeadMatrix(phx::HMD::LEFT_EYE) ==
+            ToMat4(openvr_mock.eye_to_head_left_));
+    REQUIRE(hmd->GetEyeToHeadMatrix(phx::HMD::RIGHT_EYE) ==
+            ToMat4(openvr_mock.eye_to_head_right_));
+
+    WHEN("We change the eye distance while the engine is running") {
+      float eye_to_head_left[12] = {1.0f, 0.0f, 0.0f, 0.03f, 0.0f, 1.0f,
+                                    0.0f, 0.0f, 0.0f, 0.0f,  1.0f, 12.0f};
+      float eye_to_head_right[12] = {1.0f, 0.0f, 0.0f, -0.03f, 0.0f, 1.0f,
+                                     0.0f, 0.0f, 0.0f, 0.0f,   1.0f, 13.0f};
+      ALLOW_CALL(openvr_mock.GetSystem(),
+                 GetEyeToHeadTransformArray(vr::EVREye::Eye_Left))
+          .LR_RETURN(eye_to_head_left);
+      ALLOW_CALL(openvr_mock.GetSystem(),
+                 GetEyeToHeadTransformArray(vr::EVREye::Eye_Right))
+          .LR_RETURN(eye_to_head_right);
+      THEN("The eye distances are different") {
+        REQUIRE_FALSE(hmd->GetEyeToHeadMatrix(phx::HMD::LEFT_EYE) ==
+                      ToMat4(eye_to_head_left));
+        REQUIRE_FALSE(hmd->GetEyeToHeadMatrix(phx::HMD::RIGHT_EYE) ==
+                      ToMat4(eye_to_head_right));
+        WHEN("The HMD is Updated and an event is received") {
+          vr::VREvent_t event;
+          event.eventType = vr::VREvent_IpdChanged;
+          auto first_call = std::make_shared<bool>(true);
+          ALLOW_CALL(openvr_mock.GetSystem(), PollNextEvent(_, _))
+              .SIDE_EFFECT(*_1 = event)
+              .SIDE_EFFECT(*first_call = false)
+              .WITH(*first_call == true)
+              .RETURN(true);
+          ALLOW_CALL(openvr_mock.GetSystem(), PollNextEvent(_, _))
+              .WITH(*first_call == false)
+              .RETURN(false);
+          hmd->Update();
+          THEN("The eye to head matrix has changed accordingly") {
+            REQUIRE(hmd->GetEyeToHeadMatrix(phx::HMD::LEFT_EYE) ==
+                    ToMat4(eye_to_head_left));
+            REQUIRE(hmd->GetEyeToHeadMatrix(phx::HMD::RIGHT_EYE) ==
+                    ToMat4(eye_to_head_right));
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/tests/src/mocks/openvr_mock.hpp b/tests/src/mocks/openvr_mock.hpp
index 855e2b48c5bf4951cc359382ca7a2d55e374535a..2b0d15f92f91a2acb820db4f3f1fd871ba923aeb 100644
--- a/tests/src/mocks/openvr_mock.hpp
+++ b/tests/src/mocks/openvr_mock.hpp
@@ -114,8 +114,9 @@ class OPENVR_MOCK_EXPORT IVRSystemMock : public IVRSystem {
   MAKE_MOCK3(GetControllerState, bool(vr::TrackedDeviceIndex_t,
                                       vr::VRControllerState_t*, uint32_t));
   MAKE_MOCK5(GetControllerStateWithPose,
-             bool(ETrackingUniverseOrigin, vr::TrackedDeviceIndex_t,
-                  vr::VRControllerState_t*, uint32_t, TrackedDevicePose_t*));
+             bool(vr::ETrackingUniverseOrigin, vr::TrackedDeviceIndex_t,
+                  vr::VRControllerState_t*, uint32_t,
+                  vr::TrackedDevicePose_t*));
   MAKE_MOCK3(TriggerHapticPulse,
              void(vr::TrackedDeviceIndex_t, uint32_t, unsigned short));
   MAKE_MOCK1(GetButtonIdNameFromEnum, const char*(EVRButtonId));
@@ -401,6 +402,24 @@ extern OPENVR_MOCK_EXPORT OpenVRMock openvr_mock;
               OpenVRMock::toHMDMatrix34_t(openvr_mock.l_cont_transformation_)) \
       .SIDE_EFFECT(_1[openvr_mock.index_left_controller_].bPoseIsValid =       \
                        true);                                                  \
+  ALLOW_CALL(openvr_mock.GetSystem(),                                          \
+             GetControllerStateWithPose(vr::TrackingUniverseStanding,          \
+                                        openvr_mock.index_right_controller_,   \
+                                        _, _, _))                              \
+      .RETURN(true)                                                            \
+      .SIDE_EFFECT(                                                            \
+          _5->mDeviceToAbsoluteTracking =                                      \
+              OpenVRMock::toHMDMatrix34_t(openvr_mock.r_cont_transformation_)) \
+      .SIDE_EFFECT(_5->bPoseIsValid = true);                                   \
+  ALLOW_CALL(                                                                  \
+      openvr_mock.GetSystem(),                                                 \
+      GetControllerStateWithPose(vr::TrackingUniverseStanding,                 \
+                                 openvr_mock.index_left_controller_, _, _, _)) \
+      .RETURN(true)                                                            \
+      .SIDE_EFFECT(                                                            \
+          _5->mDeviceToAbsoluteTracking =                                      \
+              OpenVRMock::toHMDMatrix34_t(openvr_mock.l_cont_transformation_)) \
+      .SIDE_EFFECT(_5->bPoseIsValid = true);                                   \
   ALLOW_CALL(openvr_mock.GetCompositor(), Submit(_, _, _, _))                  \
       .RETURN(vr::EVRCompositorError::VRCompositorError_None);                 \
   ALLOW_CALL(openvr_mock.Get(), VR_GetGenericInterface(_, _))                  \
@@ -447,6 +466,22 @@ extern OPENVR_MOCK_EXPORT OpenVRMock openvr_mock;
   ALLOW_CALL(openvr_mock.GetRenderModels(), LoadTexture_Async(_, _))           \
       .RETURN(vr::VRRenderModelError_None)                                     \
       .SIDE_EFFECT(*_2 = openvr_mock.CreateDummyTexture(                       \
-                       static_cast<unsigned int>(_1)));
+                       static_cast<unsigned int>(_1)));                        \
+  ALLOW_CALL(openvr_mock.GetSystem(), PollNextEvent(_, _)).RETURN(false);      \
+  ALLOW_CALL(openvr_mock.GetSystem(),                                          \
+             GetInt32TrackedDeviceProperty(_, vr::Prop_Axis0Type_Int32, _))    \
+      .RETURN(vr::k_eControllerAxis_None);                                     \
+  ALLOW_CALL(openvr_mock.GetSystem(),                                          \
+             GetInt32TrackedDeviceProperty(_, vr::Prop_Axis1Type_Int32, _))    \
+      .RETURN(vr::k_eControllerAxis_None);                                     \
+  ALLOW_CALL(openvr_mock.GetSystem(),                                          \
+             GetInt32TrackedDeviceProperty(_, vr::Prop_Axis2Type_Int32, _))    \
+      .RETURN(vr::k_eControllerAxis_None);                                     \
+  ALLOW_CALL(openvr_mock.GetSystem(),                                          \
+             GetInt32TrackedDeviceProperty(_, vr::Prop_Axis3Type_Int32, _))    \
+      .RETURN(vr::k_eControllerAxis_None);                                     \
+  ALLOW_CALL(openvr_mock.GetSystem(),                                          \
+             GetInt32TrackedDeviceProperty(_, vr::Prop_Axis4Type_Int32, _))    \
+      .RETURN(vr::k_eControllerAxis_None);
 
 #endif  // TESTS_SRC_MOCKS_OPENVR_MOCK_HPP_
diff --git a/tests/src/test-transform.cpp b/tests/src/test-transform.cpp
index 38e66cf05e85e2e07bd72e61c4eb7c4a865e166f..a35aea6909dd71327875cae8dd57c77a3cf11f0f 100644
--- a/tests/src/test-transform.cpp
+++ b/tests/src/test-transform.cpp
@@ -39,7 +39,7 @@ SUPPRESS_WARNINGS_END
 
 #include "test_utilities/glm_mat4.hpp"
 #include "test_utilities/glm_quat.hpp"
-#include "test_utilities/glm_vec3.hpp"
+#include "test_utilities/glm_vec.hpp"
 #include "test_utilities/log_capture.hpp"
 
 SCENARIO(
diff --git a/tests/src/test_device_system.cpp b/tests/src/test_device_system.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..dc5b0401fcbaf186a710783e4bc007b8187bb31f
--- /dev/null
+++ b/tests/src/test_device_system.cpp
@@ -0,0 +1,83 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#include <memory>
+
+#include "catch/catch.hpp"
+
+#include "trompeloeil.hpp"
+
+#include "phx/core/engine.hpp"
+#include "phx/input/device.hpp"
+#include "phx/input/device_system.hpp"
+
+using trompeloeil::_;
+using trompeloeil::ne;
+
+extern template struct trompeloeil::reporter<trompeloeil::specialized>;
+
+class MockDevice : public phx::Device {
+ public:
+  MAKE_MOCK0(Update, void());
+};
+
+class MockDevice2 : public MockDevice {
+ public:
+  MAKE_MOCK0(Update, void());
+};
+
+SCENARIO("The device system manages and updates its devices",
+         "[phx][phx::DeviceSystem]") {
+  GIVEN("an device system") {
+    phx::Engine engine;
+    auto device_system = engine.CreateSystem<phx::DeviceSystem>();
+
+    WHEN("We add devices") {
+      MockDevice* device1 = device_system->AddDevice<MockDevice>();
+      MockDevice2* device2 = device_system->AddDevice<MockDevice2>();
+
+      THEN("their update method is called when the system is updated") {
+        REQUIRE_CALL(*device1, Update()).TIMES(1);
+        REQUIRE_CALL(*device2, Update()).TIMES(1);
+        device_system->Update(phx::FrameTimer::TimeInfo());
+      }
+
+      THEN("we can get these devices by type") {
+        auto devices = device_system->GetDevices<MockDevice>();
+        REQUIRE(devices.size() == 2);
+        REQUIRE(std::find(devices.begin(), devices.end(), device1) !=
+                devices.end());
+        REQUIRE(std::find(devices.begin(), devices.end(), device2) !=
+                devices.end());
+
+        auto devices2 = device_system->GetDevices<MockDevice2>();
+        REQUIRE(devices2.size() == 1);
+        REQUIRE(devices2[0] == device2);
+      }
+
+      THEN("devices can be removed again") {
+        device_system->RemoveDevice(device1);
+        REQUIRE(device_system->GetDevices<MockDevice>().size() == 1);
+      }
+    }
+  }
+}
diff --git a/tests/src/test_material.cpp b/tests/src/test_material.cpp
index 843bd61e2fde6e39861566ecc0735b02ce29e633..93eb5ccb3eb8442d7417c346af9bea174c996904 100644
--- a/tests/src/test_material.cpp
+++ b/tests/src/test_material.cpp
@@ -29,7 +29,7 @@
 #include "phx/core/logger.hpp"
 #include "phx/rendering/components/material_handle.hpp"
 
-#include "test_utilities/glm_vec3.hpp"
+#include "test_utilities/glm_vec.hpp"
 
 SCENARIO("The material component keeps track of its attributes",
          "[phx][phx::Material]") {
diff --git a/tests/src/test_openvr_controller_system.cpp b/tests/src/test_openvr_controller_model_system.cpp
similarity index 86%
rename from tests/src/test_openvr_controller_system.cpp
rename to tests/src/test_openvr_controller_model_system.cpp
index 0b2987fe9e6899e0ed6e1434137415228c94a054..1a90a5d68a079ee0d76f4d212626d0083e456326 100644
--- a/tests/src/test_openvr_controller_system.cpp
+++ b/tests/src/test_openvr_controller_model_system.cpp
@@ -34,9 +34,9 @@ SUPPRESS_WARNINGS_END
 
 #include "phx/core/entity.hpp"
 #include "phx/core/scene.hpp"
-#include "phx/display/display_system_openvr.hpp"
-#include "phx/input/openvr_controller_behavior.hpp"
-#include "phx/input/openvr_controller_system.hpp"
+#include "phx/input/device_system.hpp"
+#include "phx/input/openvr_controller_model_system.hpp"
+#include "phx/input/vr_controller.hpp"
 #include "phx/rendering/components/material_handle.hpp"
 #include "phx/rendering/components/mesh_handle.hpp"
 #include "phx/rendering/rendering_system.hpp"
@@ -73,23 +73,23 @@ class EngineStopTestSystem : public phx::System {
 SCENARIO(
     "The OpenVRControllerSystem automatically inserts controller entities into "
     "a scene if controllers are connected",
-    "[phx][phx::OpenVRControllerSystem]") {
+    "[phx][phx::OpenVRControllerModelSystem]") {
   OPENGL_MOCK_ALLOW_ANY_CALL;
   OPENVR_MOCK_ALLOW_ANY_CALL;
   SDL_MOCK_ALLOW_ANY_CALL;
 
   phx::Engine engine;
-  auto display_system = engine.CreateSystem<phx::DisplaySystemOpenVR>();
+  auto device_system = engine.CreateSystem<phx::DeviceSystem>();
 
-  GIVEN(
-      "The display system has an HMD and the engine has a scene. Also, there's "
-      "an OpenVRControllerSystem.") {
-    display_system->CreateHMD();
+  GIVEN("a device system with two vr controllers") {
+    device_system->AddDevice<phx::VRController>(
+        phx::VRController::LEFT_CONTROLLER);
+    device_system->AddDevice<phx::VRController>(
+        phx::VRController::RIGHT_CONTROLLER);
     auto scene = std::make_shared<phx::Scene>();
     engine.SetScene(scene);
-    engine.CreateSystem<phx::OpenVRControllerSystem>(display_system);
+    engine.CreateSystem<phx::OpenVRControllerModelSystem>(device_system);
     engine.CreateSystem<EngineStopTestSystem>();
-    engine.CreateSystem<phx::RenderingSystem>(display_system);
 
     WHEN("We run the engine for once frame (updating each system once)") {
       engine.Run();
diff --git a/tests/src/test_tracked_device.cpp b/tests/src/test_tracked_device.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3166d86284093d5bb116345a8e69eeabdc5d2731
--- /dev/null
+++ b/tests/src/test_tracked_device.cpp
@@ -0,0 +1,135 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#include <memory>
+
+#include "catch/catch.hpp"
+
+#include "phx/display/hmd.hpp"
+#include "phx/input/tracked_device.hpp"
+#include "phx/input/vr_controller.hpp"
+
+#include "mocks/openvr_mock.hpp"
+
+#include "trompeloeil.hpp"
+
+#undef _
+
+extern template struct trompeloeil::reporter<trompeloeil::specialized>;
+using trompeloeil::_;
+
+SCENARIO("OpenVR is only initialized and shutdown once",
+         "[phx][phx::TrackedDevice]") {
+  OPENVR_MOCK_ALLOW_ANY_CALL
+
+  WHEN("we initialize 2 derrived tracked devices") {
+    THEN("openVR is initialized exactly once") {
+      REQUIRE_CALL(openvr_mock.Get(),
+                   VR_InitInternal2(_, vr::VRApplication_Scene, _))
+          .TIMES(1)
+          .RETURN(static_cast<uint32_t>(1337))
+          .SIDE_EFFECT(*_1 = vr::VRInitError_None);
+      auto device1 = std::make_unique<phx::HMD>();
+      auto device2 = std::make_unique<phx::VRController>(
+          phx::VRController::LEFT_CONTROLLER);
+
+      WHEN("we destroy one") {
+        THEN("openVR shutdown is not called") {
+          FORBID_CALL(openvr_mock.Get(), VR_ShutdownInternal());
+          device1.reset();
+          WHEN("we destroy the second one") {
+            THEN("openVR shutdown is called") {
+              REQUIRE_CALL(openvr_mock.Get(), VR_ShutdownInternal()).TIMES(1);
+              device2.reset();
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+#define ALLOW_EVENT_POLL_CALLS                             \
+  ALLOW_CALL(openvr_mock.GetSystem(), PollNextEvent(_, _)) \
+      .SIDE_EFFECT(*_1 = event)                            \
+      .SIDE_EFFECT(*first_call = false)                    \
+      .WITH(*first_call == true)                           \
+      .RETURN(true);                                       \
+  ALLOW_CALL(openvr_mock.GetSystem(), PollNextEvent(_, _)) \
+      .WITH(*first_call == false)                          \
+      .RETURN(false);
+
+class ActualDevice : public phx::TrackedDevice {
+ public:
+  ActualDevice() : TrackedDevice() { id_ = 1; }
+  void Update() override { TrackedDevice::Update(); }
+  void UpdateDeviceIndex() override {}
+  MAKE_MOCK1(OnOpenVREvent, void(const vr::VREvent_t&));
+};
+
+SCENARIO("OpenVR events are forwarded as signals to all tracked devices",
+         "[phx][phx::TrackedDevice]") {
+  OPENVR_MOCK_ALLOW_ANY_CALL
+  GIVEN("a derrived tracked device and some OpenVR event") {
+    auto device = std::make_unique<ActualDevice>();
+
+    auto first_call = std::make_shared<bool>(true);
+    vr::VREvent_t event;
+    event.eventType = vr::EVREventType::VREvent_ButtonPress;
+    WHEN("Update is called") {
+      THEN("OnOpenVREvent is called") {
+        *first_call = true;
+        ALLOW_EVENT_POLL_CALLS
+        REQUIRE_CALL(*device, OnOpenVREvent(_))
+            .WITH(_1.eventType == vr::EVREventType::VREvent_ButtonPress);
+
+        device->Update();
+      }
+    }
+    WHEN("we create a second device") {
+      auto device2 = std::make_unique<ActualDevice>();
+      THEN("the onOpenVREvent is called for both devices") {
+        *first_call = true;
+        ALLOW_EVENT_POLL_CALLS
+        REQUIRE_CALL(*device, OnOpenVREvent(_))
+            .WITH(_1.eventType == vr::EVREventType::VREvent_ButtonPress);
+        REQUIRE_CALL(*device2, OnOpenVREvent(_))
+            .WITH(_1.eventType == vr::EVREventType::VREvent_ButtonPress);
+        device->Update();
+        device2->Update();
+
+        WHEN("the first device is deleted") {
+          device.reset();
+          THEN("the second device still gets the event") {
+            *first_call = true;
+            ALLOW_EVENT_POLL_CALLS
+            REQUIRE_CALL(*device2, OnOpenVREvent(_))
+                .WITH(_1.eventType == vr::EVREventType::VREvent_ButtonPress);
+            device2->Update();
+          }
+        }
+      }
+    }
+  }
+}
+
+// the pose etc. is tested in integration_test_hmd
diff --git a/tests/src/test_tracking_system.cpp b/tests/src/test_tracking_system.cpp
index 44124c76815edf17b45827619708cb257c698973..40de8072af27d14242bdd5fca156463bedf50692 100644
--- a/tests/src/test_tracking_system.cpp
+++ b/tests/src/test_tracking_system.cpp
@@ -27,6 +27,7 @@
 
 #include "trompeloeil.hpp"
 
+#include "phx/display/hmd.hpp"
 #include "phx/suppress_warnings.hpp"
 
 SUPPRESS_WARNINGS_BEGIN
@@ -37,7 +38,7 @@ SUPPRESS_WARNINGS_END
 #include "phx/core/entity.hpp"
 #include "phx/core/runtime_component.hpp"
 #include "phx/core/scene.hpp"
-#include "phx/display/display_system_openvr.hpp"
+#include "phx/input/device_system.hpp"
 #include "phx/rendering/components/projection.hpp"
 #include "phx/rendering/components/transform.hpp"
 #include "phx/tracking/tracking_system_openvr.hpp"
@@ -325,12 +326,17 @@ SCENARIO(
   SDL_MOCK_ALLOW_ANY_CALL;
 
   phx::Engine engine;
-  auto display_system = engine.CreateSystem<phx::DisplaySystemOpenVR>();
+  auto device_system = engine.CreateSystem<phx::DeviceSystem>();
 
   GIVEN(
       "The display system has an HMD and the engine has a scene with a "
       "user platform.") {
-    display_system->CreateHMD();
+    device_system->AddDevice<phx::HMD>();
+    device_system->AddDevice<phx::VRController>(
+        phx::VRController::LEFT_CONTROLLER);
+    device_system->AddDevice<phx::VRController>(
+        phx::VRController::RIGHT_CONTROLLER);
+    device_system->Update(phx::FrameTimer::TimeInfo());
     auto first_scene = std::make_shared<phx::Scene>();
     engine.SetScene(first_scene);
     auto platform_trans_mat =
@@ -339,7 +345,7 @@ SCENARIO(
     ::SetPlatformMatrix(first_scene.get(), platform_trans_mat);
     WHEN("A tracking system is created and initialized.") {
       auto tracking_system =
-          engine.CreateSystem<phx::TrackingSystemOpenVR>(display_system);
+          engine.CreateSystem<phx::TrackingSystemOpenVR>(device_system);
 
       ::TestRuntimeEntityStructure(first_scene);
       ::TestRuntimeEntityUpdate(first_scene, tracking_system);
diff --git a/tests/src/test_vr_controller.cpp b/tests/src/test_vr_controller.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..628db4de69c0c41f45edd95c60b701181d830823
--- /dev/null
+++ b/tests/src/test_vr_controller.cpp
@@ -0,0 +1,166 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#include <memory>
+#include <string>
+
+#include "catch/catch.hpp"
+
+#include "phx/core/logger.hpp"
+#include "phx/input/vr_controller.hpp"
+
+#include "mocks/openvr_mock.hpp"
+
+#include "test_utilities/glm_vec.hpp"
+#include "test_utilities/log_capture.hpp"
+
+#include "trompeloeil.hpp"
+
+extern template struct trompeloeil::reporter<trompeloeil::specialized>;
+using trompeloeil::_;
+
+namespace {
+
+auto first_call = std::make_shared<bool>(true);
+
+void CheckForButtonEvent(uint32_t openVR_event_type,
+                         phx::VRController::ButtonEvent vr_cont_event,
+                         uint32_t button_id, std::string side,
+                         bool expect_to_fire,
+                         phx::VRController* vr_controller) {
+  WHEN("a " + side +
+       " openVR event (type: " + std::to_string(openVR_event_type) +
+       " button id: " + std::to_string(button_id) + " is fired") {
+    vr::VREvent_t event;
+    event.eventType = openVR_event_type;
+    event.data.controller.button = button_id;
+    event.trackedDeviceIndex =
+        (side == "left" ? 2    // in the mock 2 is given to the left controller
+                        : 1);  // in the mock 1 is given to the right controller
+    *first_call = true;
+    ALLOW_CALL(openvr_mock.GetSystem(), PollNextEvent(_, _))
+        .SIDE_EFFECT(*_1 = event)
+        .SIDE_EFFECT(*first_call = false)
+        .WITH(*first_call == true)
+        .RETURN(true);
+    ALLOW_CALL(openvr_mock.GetSystem(), PollNextEvent(_, _))
+        .WITH(*first_call == false)
+        .RETURN(false);
+    THEN("we expect the signal to " + (expect_to_fire ? "" : "not") + " fire") {
+      bool signal_fired = false;
+      vr_controller->RegisterButtonSignal(
+          [&signal_fired, vr_cont_event, button_id](
+              phx::VRController::ButtonId id,
+              phx::VRController::ButtonEvent input_event) {
+            if (input_event == vr_cont_event &&
+                id == static_cast<phx::VRController::ButtonId>(button_id))
+              signal_fired = true;
+          });
+      vr_controller->Update();
+      REQUIRE(signal_fired == expect_to_fire);
+    }
+  }
+}
+}  // namespace
+
+SCENARIO("Button click events of openVR get forwarded as signals",
+         "[phx][phx::VRController]") {
+  OPENVR_MOCK_ALLOW_ANY_CALL
+  GIVEN("a VRController") {
+    auto vr_controller =
+        std::make_unique<phx::VRController>(phx::VRController::LEFT_CONTROLLER);
+    THEN("all button events are mapped correctly") {
+      CheckForButtonEvent(vr::VREvent_ButtonPress,
+                          phx::VRController::BUTTON_PRESSED, vr::k_EButton_Grip,
+                          "left", true, vr_controller.get());
+      CheckForButtonEvent(
+          vr::VREvent_ButtonUnpress, phx::VRController::BUTTON_RELEASED,
+          vr::k_EButton_Grip, "left", true, vr_controller.get());
+      CheckForButtonEvent(vr::VREvent_ButtonTouch,
+                          phx::VRController::BUTTON_TOUCH, vr::k_EButton_Grip,
+                          "left", true, vr_controller.get());
+      CheckForButtonEvent(vr::VREvent_ButtonUntouch,
+                          phx::VRController::BUTTON_UNTOUCH, vr::k_EButton_Grip,
+                          "left", true, vr_controller.get());
+    }
+    THEN("wrong sided events are ignored") {
+      CheckForButtonEvent(vr::VREvent_ButtonPress,
+                          phx::VRController::BUTTON_PRESSED, vr::k_EButton_Grip,
+                          "right", false, vr_controller.get());
+    }
+
+    THEN(
+        "events on other buttons are ignored, respectively the correct button "
+        "id is forwarded") {
+      CheckForButtonEvent(
+          vr::VREvent_ButtonPress, phx::VRController::BUTTON_PRESSED,
+          vr::k_EButton_SteamVR_Trigger, "right", false, vr_controller.get());
+    }
+  }
+}
+
+SCENARIO("we can get the actual position on the analog axes, like the trackpad",
+         "[phx][phx::VRController]") {
+  OPENVR_MOCK_ALLOW_ANY_CALL
+  GIVEN("a VRController") {
+    auto vr_controller =
+        std::make_unique<phx::VRController>(phx::VRController::LEFT_CONTROLLER);
+
+    WHEN("for an axis is asked that does not exist for this mocked device") {
+      auto log_capture = std::make_shared<test_utilities::LogCapture>();
+      phx::logger = std::make_shared<spdlog::logger>("logcapture", log_capture);
+      phx::logger->set_pattern("%w");
+      ALLOW_CALL(openvr_mock.GetSystem(), GetControllerAxisTypeNameFromEnum(
+                                              vr::k_eControllerAxis_Trigger))
+          .RETURN("k_eControllerAxis_Trigger");
+
+      vr_controller->GetAxesValue(phx::VRController::AXES_TRIGGER);
+
+      THEN("we expect a warning") {
+        REQUIRE(*log_capture ==
+                "VRController device  does not provide the requested axes: "
+                "k_eControllerAxis_Trigger ");
+      }
+    }
+
+    WHEN("we ask for an existing axis the values are returned") {
+      ALLOW_CALL(
+          openvr_mock.GetSystem(),
+          GetControllerStateWithPose(vr::TrackingUniverseStanding, 2, _, _, _))
+          .RETURN(true)
+          .SIDE_EFFECT(_3->rAxis[0].x = 0.5f)
+          .SIDE_EFFECT(_3->rAxis[0].y = 0.7f);
+      ALLOW_CALL(openvr_mock.GetSystem(),
+                 GetInt32TrackedDeviceProperty(_, vr::Prop_Axis0Type_Int32, _))
+          .RETURN(vr::k_eControllerAxis_Trigger);
+
+      glm::vec2 value =
+          vr_controller->GetAxesValue(phx::VRController::AXES_TRIGGER);
+
+      THEN("we get the expected values") {
+        REQUIRE(value == glm::vec2(0.5f, 0.7f));
+      }
+    }
+  }
+}
+
+// TODO(JW) try also axes
diff --git a/tests/test_utilities/glm_vec3.hpp b/tests/test_utilities/glm_vec.hpp
similarity index 58%
rename from tests/test_utilities/glm_vec3.hpp
rename to tests/test_utilities/glm_vec.hpp
index d33ee272edb8b709e0fdb86e2d6f3f443d5a7f8f..2c56f39eae5c91ba404fcedf5269ada536f32e7f 100644
--- a/tests/test_utilities/glm_vec3.hpp
+++ b/tests/test_utilities/glm_vec.hpp
@@ -20,8 +20,8 @@
 // limitations under the License.
 //------------------------------------------------------------------------------
 
-#ifndef TESTS_TEST_UTILITIES_GLM_VEC3_HPP_
-#define TESTS_TEST_UTILITIES_GLM_VEC3_HPP_
+#ifndef TESTS_TEST_UTILITIES_GLM_VEC_HPP_
+#define TESTS_TEST_UTILITIES_GLM_VEC_HPP_
 
 #include <algorithm>
 #include <limits>
@@ -42,6 +42,15 @@ SUPPRESS_WARNINGS_END
 
 namespace Catch {
 
+template <>
+struct StringMaker<glm::vec2> {
+  static std::string convert(const glm::vec2& vector) {
+    std::ostringstream sstr;
+    sstr << vector;
+    return sstr.str();
+  }
+};
+
 template <>
 struct StringMaker<glm::vec3> {
   static std::string convert(const glm::vec3& vector) {
@@ -51,25 +60,50 @@ struct StringMaker<glm::vec3> {
   }
 };
 
+template <>
+struct StringMaker<glm::vec4> {
+  static std::string convert(const glm::vec4& vector) {
+    std::ostringstream sstr;
+    sstr << vector;
+    return sstr.str();
+  }
+};
+
 }  // namespace Catch
 
 namespace test_utilities {
 
-template <>
-inline bool Approx<glm::vec3>::operator==(const glm::vec3& rhs) {
+template <class VectorType>
+bool CompareVectors(VectorType a, VectorType b, int rows, float eps,
+                    float margin, float scale) {
   bool result = true;
-  for (int row = 0; row < 3; ++row) {
-    const auto this_element = value_[row];
+  for (int row = 0; row < rows; ++row) {
+    const auto this_element = a[row];
     auto this_approx_element = Catch::Detail::Approx(this_element);
-    this_approx_element.epsilon(epsilon_);
-    this_approx_element.margin(margin_);
-    this_approx_element.scale(scale_);
-    const auto other_element = rhs[row];
+    this_approx_element.epsilon(eps);
+    this_approx_element.margin(margin);
+    this_approx_element.scale(scale);
+    const auto other_element = b[row];
     result &= (other_element == this_approx_element);
   }
   return result;
 }
 
+template <>
+inline bool Approx<glm::vec2>::operator==(const glm::vec2& rhs) {
+  return CompareVectors<glm::vec2>(value_, rhs, 2, epsilon_, margin_, scale_);
+}
+
+template <>
+inline bool Approx<glm::vec3>::operator==(const glm::vec3& rhs) {
+  return CompareVectors<glm::vec3>(value_, rhs, 3, epsilon_, margin_, scale_);
+}
+
+template <>
+inline bool Approx<glm::vec4>::operator==(const glm::vec4& rhs) {
+  return CompareVectors<glm::vec4>(value_, rhs, 4, epsilon_, margin_, scale_);
+}
+
 }  // namespace test_utilities
 
-#endif  // TESTS_TEST_UTILITIES_GLM_VEC3_HPP_
+#endif  // TESTS_TEST_UTILITIES_GLM_VEC_HPP_