diff --git a/demo/data/normals_vert.glsl b/demo/data/normals_vert.glsl
index 36a36b7f534169b78154ba6a7280183234f19d70..56fb7caf41884801fe46535c46872709937c791f 100644
--- a/demo/data/normals_vert.glsl
+++ b/demo/data/normals_vert.glsl
@@ -12,7 +12,7 @@ uniform mat4 model;
 
 void main()
 {
-    gl_Position = view * model * vec4(aPos, 1.0); 
+    gl_Position = view * model * vec4(aPos, 1.0);
     mat3 normalMatrix = mat3(transpose(inverse(view * model)));
     vs_out.normal = normalize(vec3(vec4(normalMatrix * aNormal, 0.0)));
 }
diff --git a/fggl/gfx/ogl/renderer.cpp b/fggl/gfx/ogl/renderer.cpp
index e95da195a8e0f373a82ceb212c3f18c13120d9f7..8a9b1fe2fbfcd998e4d0d9013a8d8ef373bf9ea4 100644
--- a/fggl/gfx/ogl/renderer.cpp
+++ b/fggl/gfx/ogl/renderer.cpp
@@ -178,6 +178,7 @@ namespace fggl::gfx {
 		m_cache->load(ShaderConfig::named("phong"));
 		m_cache->load(ShaderConfig::named("redbook/lighting"));
 		m_cache->load(ShaderConfig::named("redbook/debug"));
+		m_cache->load(ShaderConfig::named("normals", true));
 
 		// rendering helpers
 		m_canvasRenderer = std::make_unique<ogl4::CanvasRenderer>(fonts);
diff --git a/fggl/gfx/ogl4/models.cpp b/fggl/gfx/ogl4/models.cpp
index bacdc59c38ddaf3d243e4e4563981db5192f4cdd..189ccec1b32cc1184d6aec8fd7f5628b33ecf075 100644
--- a/fggl/gfx/ogl4/models.cpp
+++ b/fggl/gfx/ogl4/models.cpp
@@ -282,6 +282,58 @@ namespace fggl::gfx::ogl4 {
 		}
 	}
 
+	static void forward_normal_pass(const entity::EntityID& camera, const fggl::entity::EntityManager& world, std::shared_ptr<ogl::Shader> shader) {
+		// enable required OpenGL state
+		glEnable(GL_CULL_FACE);
+		glCullFace(GL_BACK);
+
+		// enable depth testing
+		glEnable(GL_DEPTH_TEST);
+
+		// set-up camera matrices
+		const auto &camTransform = world.get<fggl::math::Transform>(camera);
+		const auto &camComp = world.get<fggl::gfx::Camera>(camera);
+
+		const math::mat4 projectionMatrix =
+			glm::perspective(camComp.fov, camComp.aspectRatio, camComp.nearPlane, camComp.farPlane);
+		const math::mat4 viewMatrix = glm::lookAt(camTransform.origin(), camComp.target, camTransform.up());
+
+		ogl::Location modelUniform = shader->uniform("model");
+		ogl::Location viewUniform = shader->uniform("view");
+		ogl::Location projUniform = shader->uniform("projection");
+
+		shader->use();
+		shader->setUniformMtx(projUniform, projectionMatrix);
+		shader->setUniformMtx(viewUniform, viewMatrix);
+
+		auto renderables = world.find<StaticModel>();
+		for (const auto &entity : renderables) {
+
+			// ensure that the model pipeline actually exists...
+			const auto &model = world.get<StaticModel>(entity);
+
+			// set model transform
+			const auto &transform = world.get<math::Transform>(entity);
+			shader->setUniformMtx(modelUniform, transform.model());
+
+			// render model
+			auto vao = model.vao;
+			vao->bind();
+
+			model.vertexData->bind();
+			if (model.restartIndex != NO_RESTART_IDX) {
+				glEnable(GL_PRIMITIVE_RESTART);
+				glPrimitiveRestartIndex(model.restartIndex);
+			}
+
+			auto *elements = model.elements.get();
+			vao->drawElements(*elements, model.drawType, model.elementCount);
+			if (model.restartIndex != NO_RESTART_IDX) {
+				glDisable(GL_PRIMITIVE_RESTART);
+			}
+		}
+	}
+
 	void StaticModelRenderer::renderModelsForward(const entity::EntityManager &world) {
 
 		// fetch cameras we will need to render with
@@ -297,6 +349,11 @@ namespace fggl::gfx::ogl4 {
 		for (const auto &cameraEnt : cameras) {
 			//TODO should be clipping this to only visible objects
 			forward_camera_pass(cameraEnt, world);
+
+			// enable rendering normals
+			if ( m_renderNormals ) {
+				forward_normal_pass(cameraEnt, world, m_shaders->get("normals"));
+			}
 		}
 	}
 
diff --git a/include/fggl/gfx/ogl4/models.hpp b/include/fggl/gfx/ogl4/models.hpp
index 42241dce6de66403c6d72b3a34fe2d220f5a96e3..625f0f01a1976c5fb1057a4a0e6500f7ca87f6e2 100644
--- a/include/fggl/gfx/ogl4/models.hpp
+++ b/include/fggl/gfx/ogl4/models.hpp
@@ -91,6 +91,8 @@ namespace fggl::gfx::ogl4 {
 				renderModelsForward(world);
 			}
 
+			bool m_renderNormals = true;
+
 		private:
 
 			#ifdef FGGL_ALLOW_DEFERRED_UPLOAD