diff --git a/demo/main.cpp b/demo/main.cpp
index f58a6351a3fcf66ac05e68ec6afd9c947cb8f87f..c0ef3059d754485e3b2738d65b908f3a590b6393 100644
--- a/demo/main.cpp
+++ b/demo/main.cpp
@@ -3,6 +3,7 @@
 
 #include <fggl/gfx/window.hpp>
 #include <fggl/gfx/camera.hpp>
+#include <fggl/input/camera_input.h>
 
 #include <fggl/gfx/compat.hpp>
 #include <fggl/gfx/ogl/compat.hpp>
@@ -17,10 +18,6 @@
 
 constexpr bool showNormals = false;
 
-template <typename T> int sgn(T val) {
-    return (T(0) < val) - (val < T(0));
-}
-
 // prototype of resource discovery
 void discover(const std::filesystem::path& base) {
 
@@ -52,161 +49,7 @@ camera_type cam_mode = cam_free;
 using namespace fggl::input;
 using InputManager = std::shared_ptr<fggl::input::Input>;
 
-void process_arcball(fggl::ecs3::World& ecs, InputManager input, fggl::ecs::entity_t cam) {
-	// see https://asliceofrendering.com/camera/2019/11/30/ArcballCamera/
-	auto* camTransform = ecs.get<fggl::math::Transform>(cam);
-	auto* camComp = ecs.get<fggl::gfx::Camera>(cam);
-	auto& mouse = input->mouse;
-
-	glm::vec4 position(camTransform->origin(), 1.0f);
-	glm::vec4 pivot(camComp->target, 1.0f);
-	glm::mat4 view = glm::lookAt( camTransform->origin(), camComp->target, camTransform->up() );
-	glm::vec3 viewDir = -glm::transpose(view)[2];
-	glm::vec3 rightDir = glm::transpose(view)[0];
-
-	float deltaAngleX = ( 2 * M_PI ); 
-	float deltaAngleY = ( M_PI );
-	float xAngle = ( -mouse.axisDelta(fggl::input::MouseAxis::X) ) * deltaAngleX;
-	float yAngle = ( -mouse.axisDelta(fggl::input::MouseAxis::Y) ) * deltaAngleY;
-
-	auto cosAngle = glm::dot( viewDir, fggl::math::UP );
-	if ( cosAngle * sgn(deltaAngleY) > 0.99f ) {
-		deltaAngleY = 0;
-	}
-
-	// rotate the camera around the pivot on the first axis
-	glm::mat4x4 rotationMatrixX(1.0f);
-	rotationMatrixX = glm::rotate( rotationMatrixX, xAngle, fggl::math::UP );
-	position = ( rotationMatrixX * ( position - pivot ) ) + pivot;
-
-	// rotate the camera aroud the pivot on the second axis
-	glm::mat4x4 rotationMatrixY(1.0f);
-	rotationMatrixY = glm::rotate(rotationMatrixY, yAngle, rightDir );
-	glm::vec3 finalPos = ( rotationMatrixY * ( position - pivot ) ) + pivot;
-
-	camTransform->origin( finalPos );
-}
-
-constexpr float ROT_SPEED = 0.05f;
-constexpr float PAN_SPEED = 0.05f;
-constexpr glm::mat4 MAT_IDENTITY(1.0f);
-
-
-void process_freecam(fggl::ecs3::World& ecs, InputManager input, fggl::ecs::entity_t cam) {
-	float rotationValue = 0.0f;
-	glm::vec3 translation(0.0f);
-
-	auto& keyboard = input->keyboard;
-	auto code_q = glfwGetKeyScancode(GLFW_KEY_Q);
-	auto code_e = glfwGetKeyScancode(GLFW_KEY_E);
-	auto code_w = glfwGetKeyScancode(GLFW_KEY_W);
-	auto code_s = glfwGetKeyScancode(GLFW_KEY_S);
-	auto code_d = glfwGetKeyScancode(GLFW_KEY_D);
-	auto code_a = glfwGetKeyScancode(GLFW_KEY_A);
-
-	// calulate rotation (user input)
-	if ( keyboard.down( code_q ) ) {
-		rotationValue = ROT_SPEED; 
-	} else if ( keyboard.down(code_e) ) {
-		rotationValue = -ROT_SPEED;
-	}
-
-	// calulate movement (user input)
-	if ( keyboard.down(code_w) ) {
-		translation -= fggl::math::RIGHT;
-	}
-
-	if ( keyboard.down(code_s) ) {
-		translation += fggl::math::RIGHT;
-	}
-
-	if ( keyboard.down(code_d) ) {
-		translation += fggl::math::FORWARD;
-	}
-
-	if ( keyboard.down(code_a) ) {
-		translation -= fggl::math::FORWARD;
-	}
-
-	// apply rotation/movement
-	auto camTransform = ecs.get<fggl::math::Transform>(cam);
-	auto camComp = ecs.get<fggl::gfx::Camera>(cam);
-
-	glm::vec4 position( camTransform->origin(), 1.0f );
-	glm::vec4 pivot( camComp->target, 1.0f );
-
-	// apply movement
-	if ( translation != glm::vec3(0.0f) ) {
-		const auto rotation = (position - pivot);
-		const float angle = atan2( rotation.x, rotation.z );
-		const auto rotationMat = glm::rotate( MAT_IDENTITY, angle, fggl::math::UP );
-
-		auto deltaMove = (rotationMat * glm::vec4( translation, 1.0f )) * PAN_SPEED;
-		deltaMove.w = 0.0f;
-
-		position += deltaMove;
-		pivot += deltaMove;
-	}
-
-	// apply rotation
-	if ( rotationValue != 0.0f ) {
-		glm::mat4 rotation = glm::rotate( MAT_IDENTITY, rotationValue, fggl::math::UP );
-		position = ( rotation * ( position - pivot ) ) + pivot;
-	}
-
-	camTransform->origin( position );
-	camComp->target = pivot;
-}
-
-void process_edgescroll(fggl::ecs3::World& ecs, InputManager input, fggl::ecs::entity_t cam) {
-	glm::vec3 translation(0.0f);
-
-	auto& mouse = input->mouse;
-
-	// calulate movement (user input)
-	if ( mouse.axis( MouseAxis::Y ) < 0.9f ) {
-		translation -= fggl::math::RIGHT;
-	}
-
-	if ( mouse.axis( MouseAxis::Y) > -0.9f ) {
-		translation += fggl::math::RIGHT;
-	}
-
-	if ( mouse.axis( MouseAxis::X) > -0.9f ) {
-		translation += fggl::math::FORWARD;
-	}
-
-	if ( mouse.axis( MouseAxis::X ) < 0.9f ) {
-		translation -= fggl::math::FORWARD;
-	}
-
-	// apply rotation/movement
-	auto camTransform = ecs.get<fggl::math::Transform>(cam);
-	auto camComp = ecs.get<fggl::gfx::Camera>(cam);
-
-	glm::vec4 position( camTransform->origin(), 1.0f );
-	glm::vec4 pivot( camComp->target, 1.0f );
-
-	// apply movement
-	if ( translation != glm::vec3(0.0f) ) {
-		const auto rotation = (position - pivot);
-		const float angle = atan2( rotation.x, rotation.z );
-		const auto rotationMat = glm::rotate( MAT_IDENTITY, angle, fggl::math::UP );
-
-		auto deltaMove = (rotationMat * glm::vec4( translation, 1.0f )) * PAN_SPEED;
-		deltaMove.w = 0.0f;
-
-		position += deltaMove;
-		pivot += deltaMove;
-	}
-
-	// move camera
-	camTransform->origin( position );
-	camComp->target = pivot;
-}
-
-
-void process_camera(fggl::gfx::Window& window, fggl::ecs3::World& ecs, InputManager input) {
+void process_camera(fggl::gfx::Window& window, fggl::ecs3::World& ecs, const InputManager& input) {
     auto cameras = ecs.findMatching<fggl::gfx::Camera>();
     fggl::ecs3::entity_t cam = cameras[0];
 
@@ -221,18 +64,18 @@ void process_camera(fggl::gfx::Window& window, fggl::ecs3::World& ecs, InputMana
 	float delta = input->mouse.axis( fggl::input::MouseAxis::SCROLL_Y );
 	if ( (glm::length( dir ) < 25.0f && delta < 0.0f) || (glm::length( dir ) > 2.5f && delta > 0.0f) )
 		motion -= (forward * delta);
-
 	camTransform->origin( camTransform->origin() + motion );
 
 	if ( cam_mode == cam_arcball || input->mouse.button( fggl::input::MouseButton::MIDDLE ) ) { 
-		process_arcball(ecs, input, cam);
+		fggl::input::process_arcball(ecs, *input, cam);
 	} else if ( cam_mode == cam_free ) {
-		process_freecam(ecs, input, cam);
+		fggl::input::process_freecam(ecs, *input, cam);
 	}
-	process_edgescroll( ecs, input, cam );
+	fggl::input::process_edgescroll( ecs, *input, cam );
 }
 
 int main(int argc, char* argv[]) {
+
 	// setup ECS
     fggl::ecs3::TypeRegistry types;
     fggl::ecs3::ModuleManager modules(types);
@@ -269,9 +112,18 @@ int main(int argc, char* argv[]) {
         auto prototype = ecs.create(false);
         ecs.add(prototype, types.find(fggl::math::Transform::name));
         ecs.add(prototype, types.find(fggl::gfx::Camera::name));
+        ecs.add(prototype, types.find(fggl::input::FreeCamKeys::name));
 
         auto camTf = ecs.get<fggl::math::Transform>(prototype);
         camTf->origin( glm::vec3(0.0f, 3.0f, 3.0f) );
+
+        auto cameraKeys = ecs.get<fggl::input::FreeCamKeys>(prototype);
+        cameraKeys->forward = glfwGetKeyScancode(GLFW_KEY_W);
+        cameraKeys->backward = glfwGetKeyScancode(GLFW_KEY_S);
+        cameraKeys->left = glfwGetKeyScancode(GLFW_KEY_A);
+        cameraKeys->right = glfwGetKeyScancode(GLFW_KEY_D);
+        cameraKeys->rotate_cw = glfwGetKeyScancode(GLFW_KEY_Q);
+        cameraKeys->rotate_ccw = glfwGetKeyScancode(GLFW_KEY_E);
     }
 
     // create building prototype
diff --git a/fggl/CMakeLists.txt b/fggl/CMakeLists.txt
index 1bd8aec5da984df76d65488a9b019a21d15734be..000d4172a89c1b52c2d6d971cdbaaa8a0ce563f2 100644
--- a/fggl/CMakeLists.txt
+++ b/fggl/CMakeLists.txt
@@ -6,7 +6,7 @@ add_library(fggl fggl.cpp
 	data/procedural.cpp
 	ecs3/fast/Container.cpp
 	ecs3/prototype/world.cpp
-	ecs3/module/module.cpp)
+	ecs3/module/module.cpp input/camera_input.h input/camera_input.cpp)
 target_include_directories(fggl PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../)
 
 # Graphics backend
diff --git a/fggl/gfx/ogl/compat.hpp b/fggl/gfx/ogl/compat.hpp
index fd761c62bf38044fc31ab4b82c22ca4780694405..e489790b501ff29c8c35325a6d184656a4d24bc1 100644
--- a/fggl/gfx/ogl/compat.hpp
+++ b/fggl/gfx/ogl/compat.hpp
@@ -18,6 +18,7 @@
 #include <fggl/gfx/common.hpp>
 #include <fggl/gfx/camera.hpp>
 #include <fggl/ecs/ecs.hpp>
+#include <fggl/input/camera_input.h>
 
 namespace fggl::gfx {
 
@@ -51,6 +52,9 @@ namespace fggl::gfx {
             types.make<fggl::gfx::StaticMesh>();
             types.make<fggl::gfx::Camera>();
 
+            // FIXME probably shouldn't be doing this...
+            types.make<fggl::input::FreeCamKeys>();
+
             // opengl
             types.make<fggl::gfx::GlRenderToken>();
 
diff --git a/fggl/input/camera_input.cpp b/fggl/input/camera_input.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..bdfffcdf65fa552eea6e36f47c028a26667d3caa
--- /dev/null
+++ b/fggl/input/camera_input.cpp
@@ -0,0 +1,150 @@
+//
+// Created by webpigeon on 20/11/2021.
+//
+
+#include <fggl/ecs3/ecs.hpp>
+#include <fggl/input/input.hpp>
+
+#include <fggl/gfx/camera.hpp>
+#include <fggl/input/camera_input.h>
+
+namespace fggl::input {
+
+    void process_arcball(fggl::ecs3::World &ecs, const Input &input, fggl::ecs::entity_t cam) {
+        // see https://asliceofrendering.com/camera/2019/11/30/ArcballCamera/
+        auto *camTransform = ecs.get<fggl::math::Transform>(cam);
+        auto *camComp = ecs.get<fggl::gfx::Camera>(cam);
+        auto &mouse = input.mouse;
+
+        glm::vec4 position(camTransform->origin(), 1.0f);
+        glm::vec4 pivot(camComp->target, 1.0f);
+        glm::mat4 view = glm::lookAt(camTransform->origin(), camComp->target, camTransform->up());
+        glm::vec3 viewDir = -glm::transpose(view)[2];
+        glm::vec3 rightDir = glm::transpose(view)[0];
+
+        float deltaAngleX = (2 * M_PI);
+        float deltaAngleY = (M_PI);
+        float xAngle = (-mouse.axisDelta(fggl::input::MouseAxis::X)) * deltaAngleX;
+        float yAngle = (-mouse.axisDelta(fggl::input::MouseAxis::Y)) * deltaAngleY;
+
+        // rotate the camera around the pivot on the first axis
+        glm::mat4x4 rotationMatrixX(1.0f);
+        rotationMatrixX = glm::rotate(rotationMatrixX, xAngle, fggl::math::UP);
+        position = (rotationMatrixX * (position - pivot)) + pivot;
+
+        // rotate the camera aroud the pivot on the second axis
+        glm::mat4x4 rotationMatrixY(1.0f);
+        rotationMatrixY = glm::rotate(rotationMatrixY, yAngle, rightDir);
+        glm::vec3 finalPos = (rotationMatrixY * (position - pivot)) + pivot;
+
+        camTransform->origin(finalPos);
+    }
+
+    void process_freecam(fggl::ecs3::World &ecs, const Input &input, fggl::ecs::entity_t cam) {
+        float rotationValue = 0.0f;
+        glm::vec3 translation(0.0f);
+
+        auto &keyboard = input.keyboard;
+        auto* settings = ecs.get<FreeCamKeys>(cam);
+
+        // calculate rotation (user input)
+        if (keyboard.down(settings->rotate_cw)) {
+            rotationValue = ROT_SPEED;
+        } else if (keyboard.down(settings->rotate_ccw)) {
+            rotationValue = -ROT_SPEED;
+        }
+
+        // calculate movement (user input)
+        if (keyboard.down(settings->forward)) {
+            translation -= fggl::math::RIGHT;
+        }
+
+        if (keyboard.down(settings->backward)) {
+            translation += fggl::math::RIGHT;
+        }
+
+        if (keyboard.down(settings->right)) {
+            translation += fggl::math::FORWARD;
+        }
+
+        if (keyboard.down(settings->left)) {
+            translation -= fggl::math::FORWARD;
+        }
+
+        // apply rotation/movement
+        auto camTransform = ecs.get<fggl::math::Transform>(cam);
+        auto camComp = ecs.get<fggl::gfx::Camera>(cam);
+
+        glm::vec4 position(camTransform->origin(), 1.0f);
+        glm::vec4 pivot(camComp->target, 1.0f);
+
+        // apply movement
+        if (translation != glm::vec3(0.0f)) {
+            const auto rotation = (position - pivot);
+            const float angle = atan2f(rotation.x, rotation.z);
+            const auto rotationMat = glm::rotate(MAT_IDENTITY, angle, fggl::math::UP);
+
+            auto deltaMove = (rotationMat * glm::vec4(translation, 1.0f)) * PAN_SPEED;
+            deltaMove.w = 0.0f;
+
+            position += deltaMove;
+            pivot += deltaMove;
+        }
+
+        // apply rotation
+        if (rotationValue != 0.0f) {
+            glm::mat4 rotation = glm::rotate(MAT_IDENTITY, rotationValue, fggl::math::UP);
+            position = (rotation * (position - pivot)) + pivot;
+        }
+
+        camTransform->origin(position);
+        camComp->target = pivot;
+    }
+
+    void process_edgescroll(fggl::ecs3::World &ecs, const Input &input, fggl::ecs::entity_t cam) {
+        glm::vec3 translation(0.0f);
+
+        auto &mouse = input.mouse;
+
+        // calculate movement (user input)
+        if (mouse.axis(MouseAxis::Y) < 0.9f) {
+            translation -= fggl::math::RIGHT;
+        }
+
+        if (mouse.axis(MouseAxis::Y) > -0.9f) {
+            translation += fggl::math::RIGHT;
+        }
+
+        if (mouse.axis(MouseAxis::X) > -0.9f) {
+            translation += fggl::math::FORWARD;
+        }
+
+        if (mouse.axis(MouseAxis::X) < 0.9f) {
+            translation -= fggl::math::FORWARD;
+        }
+
+        // apply rotation/movement
+        auto camTransform = ecs.get<fggl::math::Transform>(cam);
+        auto camComp = ecs.get<fggl::gfx::Camera>(cam);
+
+        glm::vec4 position(camTransform->origin(), 1.0f);
+        glm::vec4 pivot(camComp->target, 1.0f);
+
+        // apply movement
+        if (translation != glm::vec3(0.0f)) {
+            const auto rotation = (position - pivot);
+            const float angle = atan2f(rotation.x, rotation.z);
+            const auto rotationMat = glm::rotate(MAT_IDENTITY, angle, fggl::math::UP);
+
+            auto deltaMove = (rotationMat * glm::vec4(translation, 1.0f)) * PAN_SPEED;
+            deltaMove.w = 0.0f;
+
+            position += deltaMove;
+            pivot += deltaMove;
+        }
+
+        // move camera
+        camTransform->origin(position);
+        camComp->target = pivot;
+    }
+}
diff --git a/fggl/input/camera_input.h b/fggl/input/camera_input.h
new file mode 100644
index 0000000000000000000000000000000000000000..d24d9093f9fb530c128cbf9abd507c18a31ad74d
--- /dev/null
+++ b/fggl/input/camera_input.h
@@ -0,0 +1,59 @@
+//
+// Created by webpigeon on 20/11/2021.
+//
+
+#ifndef FGGL_CAMERA_INPUT_H
+#define FGGL_CAMERA_INPUT_H
+
+#include <fggl/math/types.hpp>
+
+namespace fggl::input {
+
+    constexpr float ROT_SPEED = 0.05f;
+    constexpr float PAN_SPEED = 0.05f;
+    constexpr math::mat4 MAT_IDENTITY(1.0f);
+
+    struct FreeCamKeys {
+        constexpr const static char name[] = "FreeCameraKeys";
+        scancode_t forward;
+        scancode_t backward;
+        scancode_t left;
+        scancode_t right;
+        scancode_t rotate_cw;
+        scancode_t rotate_ccw;
+    };
+
+    /**
+     * Process the camera based on rotation around a fixed point.
+     *
+     * @param ecs the world that contains the camera
+     * @param input the input module to read the mouse location from
+     * @param cam the ID of the camera entity
+     */
+    void process_arcball(fggl::ecs3::World& ecs, const Input& input, fggl::ecs::entity_t cam);
+
+    /**
+     * Process free (floating) camera movement.
+     *
+     * @param ecs the world that contains the camera
+     * @param input the input module to read the mouse location from
+     * @param cam the ID of the camera entity
+     */
+    void process_freecam(fggl::ecs3::World& ecs, const Input& input, fggl::ecs::entity_t cam);
+
+    /**
+     * Input processing for moving the camera when the mouse is close to the edge of the screen.
+     *
+     * This function deals with ensuring the camera moves if the user moves their mouse button close
+     * to the edge of the screen. It will apply this as movement to the camera entity passed in as
+     * an argument.
+     *
+     * @param ecs the world that contains the camera
+     * @param input the input module to read the mouse location from
+     * @param cam the ID of the camera entity
+     */
+    void process_edgescroll(fggl::ecs3::World& ecs, const Input& input, fggl::ecs::entity_t cam);
+
+}
+
+#endif //FGGL_CAMERA_INPUT_H