diff --git a/build.sh b/build.sh
index 3c5eda867566b27e12f5c77eb38642a82b1dc9cc..b9b5e312a2663cd087eed9732ee64b858cdf6501 100755
--- a/build.sh
+++ b/build.sh
@@ -6,6 +6,9 @@ then
 	mkdir build
 fi
 
+# if doing shader development, disable the cache to make sure changes take affect
+rm -rf /tmp/fggl/
+
 pushd build
 cmake ..
 make
diff --git a/demo/data/normals_frag.glsl b/demo/data/normals_frag.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..fe2187ef67f65d92de05c2c3c341b3ccc3a18960
--- /dev/null
+++ b/demo/data/normals_frag.glsl
@@ -0,0 +1,9 @@
+#version 330 core
+// credit: https://learnopengl.com/Advanced-OpenGL/Geometry-Shader
+out vec4 FragColor;
+
+void main()
+{
+    FragColor = vec4(1.0, 1.0, 0.0, 1.0);
+}  
+
diff --git a/demo/data/normals_geom.glsl b/demo/data/normals_geom.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..6f1aa168791ddfd357f6b4e566c687ef40b99115
--- /dev/null
+++ b/demo/data/normals_geom.glsl
@@ -0,0 +1,30 @@
+#version 330 core
+// credit: https://learnopengl.com/Advanced-OpenGL/Geometry-Shader
+
+layout (triangles) in;
+layout (line_strip, max_vertices = 6) out;
+
+in VS_OUT {
+    vec3 normal;
+} gs_in[];
+
+const float MAGNITUDE = 0.4;
+  
+uniform mat4 projection;
+
+void GenerateLine(int index)
+{
+    gl_Position = projection * gl_in[index].gl_Position;
+    EmitVertex();
+    gl_Position = projection * (gl_in[index].gl_Position + 
+                                vec4(gs_in[index].normal, 0.0) * MAGNITUDE);
+    EmitVertex();
+    EndPrimitive();
+}
+
+void main()
+{
+    GenerateLine(0); // first vertex normal
+    GenerateLine(1); // second vertex normal
+    GenerateLine(2); // third vertex normal
+}
diff --git a/demo/data/normals_vert.glsl b/demo/data/normals_vert.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..36a36b7f534169b78154ba6a7280183234f19d70
--- /dev/null
+++ b/demo/data/normals_vert.glsl
@@ -0,0 +1,18 @@
+#version 330 core
+// credit: https://learnopengl.com/Advanced-OpenGL/Geometry-Shader
+layout (location = 0) in vec3 aPos;
+layout (location = 1) in vec3 aNormal;
+
+out VS_OUT {
+    vec3 normal;
+} vs_out;
+
+uniform mat4 view;
+uniform mat4 model;
+
+void main()
+{
+    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/demo/data/phong_frag.glsl b/demo/data/phong_frag.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..0d4df5e53b15dd65e1d30162932df4a81e8b5ee2
--- /dev/null
+++ b/demo/data/phong_frag.glsl
@@ -0,0 +1,42 @@
+#version 330 core
+// based on http://www.opengl-tutorial.org, WTFPL
+
+
+uniform vec3 lightPos;
+
+in vec3 normal;
+
+in vec3 pos_ws;
+in vec3 normal_cs;
+in vec3 lightdir_cs;
+in vec3 eyedir_cs;
+
+out vec4 FragColor;
+
+void main()
+{
+    vec3 lightColour = vec3( 1.0, 1.0, 1.0 );
+    vec3 objColour = vec3(1.0f, 0.6f, 0.2f);
+    float lightPower = 200.0;
+
+    // material colours
+    vec3 matDiff = vec3(1.0, 0.6, 0.2 );
+    vec3 matAmb = vec3(0.1) * matDiff;
+    vec3 matSpec = vec3(0.3);
+
+    vec3 n = normalize( normal_cs );
+    vec3 l = normalize( lightdir_cs );
+    float distance = length( lightPos - pos_ws );
+
+    vec3 e = normalize( eyedir_cs );
+    vec3 r = reflect( -l, n );
+    float cosAlpha = clamp( dot(e, r), 0, 1);
+
+    float cosTheta = clamp( dot( n, l ), 0, 1 );
+    vec3 colour = 
+       ( matAmb ) +
+       ( matDiff * lightColour * lightPower * cosTheta / ( distance*distance ) ) + 
+       ( matSpec * lightColour * lightPower * pow( cosAlpha, 5 ) / (distance*distance) );
+
+    FragColor = vec4(colour, 1);
+} 
diff --git a/demo/data/phong_normals_frag.glsl b/demo/data/phong_normals_frag.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..f39c8fc040794f29141c1d15299016366710ef45
--- /dev/null
+++ b/demo/data/phong_normals_frag.glsl
@@ -0,0 +1,41 @@
+#version 330 core
+// based on http://www.opengl-tutorial.org, WTFPL
+
+uniform vec3 lightPos;
+
+in vec3 normal;
+
+in vec3 pos_ws;
+in vec3 normal_cs;
+in vec3 lightdir_cs;
+in vec3 eyedir_cs;
+
+out vec4 FragColor;
+
+void main()
+{
+    vec3 lightColour = vec3( 1.0, 1.0, 1.0 );
+    vec3 objColour = vec3(1.0f, 0.6f, 0.2f);
+    float lightPower = 50.0;
+
+    // material colours
+    vec3 matDiff = vec3(1.0, 0.6, 0.2 );
+    vec3 matAmb = vec3(0.1) * matDiff;
+    vec3 matSpec = vec3(0.3);
+
+    vec3 n = normalize( normal_cs );
+    vec3 l = normalize( lightdir_cs );
+    float distance = length( lightPos - pos_ws );
+
+    vec3 e = normalize( eyedir_cs );
+    vec3 r = reflect( -l, n );
+    float cosAlpha = clamp( dot(e, r), 0, 1);
+
+    float cosTheta = clamp( dot( n, l ), 0, 1 );
+//    vec3 colour = 
+//       ( matAmb ) +
+//       ( matDiff * lightColour * lightPower * cosTheta / ( distance*distance ) ) + 
+//       ( matSpec * lightColour * lightPower * pow( cosAlpha, 5 ) / (distance*distance) );
+
+    FragColor = vec4(normal, 1);
+} 
diff --git a/demo/data/phong_vert.glsl b/demo/data/phong_vert.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..c4a5f6956e4d7774c277055b9da2cde7114ca581
--- /dev/null
+++ b/demo/data/phong_vert.glsl
@@ -0,0 +1,34 @@
+#version 330 core
+// based on http://www.opengl-tutorial.org, WTFPL
+
+layout (location = 0) in vec3 aPos;
+layout (location = 1) in vec3 aNormal;
+
+uniform mat4 model;
+uniform mat4 view;
+uniform mat4 projection;
+uniform vec3 lightPos;
+
+out vec3 normal;
+
+out vec3 pos_ws;
+out vec3 normal_cs;
+out vec3 lightdir_cs;
+out vec3 eyedir_cs;
+
+void main()
+{
+    gl_Position = projection * view * model * vec4(aPos, 1.0);
+    normal = aNormal;
+
+    // world space
+    pos_ws = ( model * vec4( aPos, 1 ) ).xyz;
+    
+    // camera space
+    vec3 pos_cs = ( view * model * vec4(aPos, 1) ).xyz;
+    eyedir_cs = vec3(0, 0, 0) - pos_cs;
+
+    vec3 light_cs = ( view * vec4( lightPos, 1) ).xyz;
+    lightdir_cs = light_cs + eyedir_cs;
+    normal_cs = ( view * model * vec4( aNormal, 0 ) ).xyz;
+}
diff --git a/demo/main.cpp b/demo/main.cpp
index a5a456194edde6fd6b6b8997834dac5715f13145..324c1acd48f86e1ca3ba71df0177f0c48aaf7cc2 100644
--- a/demo/main.cpp
+++ b/demo/main.cpp
@@ -1,4 +1,5 @@
 #include <filesystem>
+#include <glm/trigonometric.hpp>
 #include <iostream>
 
 #include <fggl/gfx/window.hpp>
@@ -61,28 +62,46 @@ int main(int argc, char* argv[]) {
 	config.fragment = "unlit_frag.glsl";
 	auto shader = cache.load(config);
 
+	fggl::gfx::ShaderConfig configPhong;
+	configPhong.name = "phong";
+	configPhong.vertex = configPhong.name + "_vert.glsl";
+	configPhong.fragment = configPhong.name + "_frag.glsl";
+//	configPhong.fragment = configPhong.name + "_normals_frag.glsl";
+	auto shaderPhong = cache.load(configPhong);
+
+	fggl::gfx::ShaderConfig configNormals;
+	configNormals.name = "normals";
+	configNormals.hasGeom = true;
+	configNormals.vertex = configNormals.name + "_vert.glsl";
+	configNormals.geometry = configNormals.name + "_geom.glsl";
+	configNormals.fragment = configNormals.name + "_frag.glsl";
+	auto shaderNormals = cache.load( configNormals );
+
 	// create ECS
 	fggl::ecs::ECS ecs;
 	ecs.registerComponent<fggl::gfx::MeshToken>();
 	ecs.registerComponent<fggl::math::Transform>();
 
-	for (int i=0; i<3; i++) {
+	int nCubes = 3;
+	int nSlopes = 3;
+
+	for (int i=0; i<nCubes; i++) {
 		// create an entity
 		auto entity = ecs.createEntity();
 
 		// set the position
 		auto result = ecs.addComponent<fggl::math::Transform>(entity);
-		result->origin( glm::vec3( 0.0f, 0.0f, i * -1.0f) );
+		result->origin( glm::vec3( 0.0f, 0.0f, i  * -1.0f) );
 
 		// in a supprise to no one it's a triangle
 		auto mesh = fggl::data::make_cube();
 		auto token = meshRenderer.upload(mesh);
-		token.pipeline = shader;
+		token.pipeline = shaderNormals;
 		ecs.addComponent<fggl::gfx::MeshToken>(entity, token);
 	}
 
 	constexpr float HALF_PI = M_PI / 2.0f;
-	for (int i=0; i<3; i++) {
+	for (int i=0; i<nSlopes; i++) {
 		// create an entity
 		auto entity = ecs.createEntity();
 
@@ -94,20 +113,44 @@ int main(int argc, char* argv[]) {
 		// in a supprise to no one it's a triangle
 		auto mesh = fggl::data::make_slope();
 		auto token = meshRenderer.upload(mesh);
-		token.pipeline = shader;
+		token.pipeline = shaderNormals;
 		ecs.addComponent<fggl::gfx::MeshToken>(entity, token);
 	}
 
+	float time = 0.0f;
+	float dt = 16.0f;
 	while( !win.closeRequested() ) {
 		ctx.pollEvents();
 		debug.frameStart();
 
 		// update step
+		time += dt;
+
+/*		float amount = glm::radians( time / 2048.0f * 360.0f );
+		auto spinners = ecs.getEntityWith<fggl::math::Transform>();
+		for ( auto entity : spinners ) {
+			auto transform = ecs.getComponent<fggl::math::Transform>(entity);
+			transform->euler(glm::vec3(0.0f, amount, 0.0f));
+		}*/
 
 		// render step
 		ogl.clear();
+
+		// render using real shader
+		auto renderables = ecs.getEntityWith<fggl::gfx::MeshToken>();
+		for ( auto renderable : renderables ) {
+			auto token = ecs.getComponent<fggl::gfx::MeshToken>(renderable);
+			token->pipeline = shaderPhong;
+		}
 		meshRenderer.render(win, ecs, 16.0f);
 
+		// render using normals shader
+		/*for ( auto renderable : renderables ) {
+			auto token = ecs.getComponent<fggl::gfx::MeshToken>(renderable);
+			token->pipeline = shaderNormals;
+		}
+		meshRenderer.render(win, ecs, 16.0f);*/
+
 		debug.draw();
 		win.swap();
 	}
diff --git a/fggl/data/procedural.cpp b/fggl/data/procedural.cpp
index e9a7e82145c5f8b7713af395097b08a219c27367..35c26de2b53ad6e1150cb3a15a1a4bb420143b3d 100644
--- a/fggl/data/procedural.cpp
+++ b/fggl/data/procedural.cpp
@@ -1,9 +1,43 @@
 
 #include "procedural.hpp"
 #include "model.hpp"
+#include <glm/geometric.hpp>
 
 using namespace fggl::data;
 
+// from https://www.khronos.org/opengl/wiki/Calculating_a_Surface_Normal
+static glm::vec3 calcSurfaceNormal(glm::vec3 a, glm::vec3 b, glm::vec3 c) {
+	glm::vec3 u = b - a;
+	glm::vec3 v = c - a;
+	return glm::normalize( glm::cross( u, v ) );
+}
+
+static void computeNormals( fggl::data::Mesh& mesh, const int* idx, const int* colIdx, int points) {
+
+	// we're assuming all the normals are zero...
+	for (auto& vertex : mesh.vertexList() ) {
+		vertex.normal = glm::vec3(0.0f);
+	}
+
+	// each vertex normal should be the sum of the triangles it's part of.
+	for (int i=0; i<points; i += 3) {
+		auto& v1 = mesh.vertex( colIdx[ idx[i] ] );
+		auto& v2 = mesh.vertex( colIdx[ idx[i + 1] ] );
+		auto& v3 = mesh.vertex( colIdx[ idx[i + 2] ] );
+
+		v1.normal += calcSurfaceNormal( v1.posititon, v2.posititon, v3.posititon );
+		v2.normal += calcSurfaceNormal( v2.posititon, v3.posititon, v1.posititon );
+		v3.normal += calcSurfaceNormal( v3.posititon, v1.posititon, v2.posititon );
+//		v3.normal += calcSurfaceNormal( v3.posititon, v1.posititon, v2.posititon );
+	}
+
+	// each vertex should currently be the sum of every triangle it's part of
+	// so we need to normalize them
+	for (auto& vertex : mesh.vertexList() ) {
+		vertex.normal = glm::normalize( vertex.normal );
+	}
+}
+
 fggl::data::Mesh fggl::data::make_triangle() {
     constexpr fggl::math::vec3 pos[]{
             {-0.5f, -0.5f, 0.0f},
@@ -16,6 +50,7 @@ fggl::data::Mesh fggl::data::make_triangle() {
     for (auto po : pos) {
         Vertex vert{};
         vert.posititon = po;
+	vert.normal = glm::vec3(0.0f, 0.0f, 0.0f);
         vert.colour = fggl::math::vec3(1.0f, 1.0f, 1.0f);
         mesh.push(vert);
     }
@@ -45,6 +80,7 @@ fggl::data::Mesh fggl::data::make_quad_xy() {
 	for (int i = 0; i < 4; ++i){
 		Vertex vert{};
 		vert.posititon = pos[i];
+		vert.normal = glm::vec3(0.0f, 0.0f, 0.0f);
 		vert.colour = fggl::math::vec3(1.0f, 1.0f, 1.0f);
 		colIdx[ i ] = mesh.pushVertex(vert);
 	}
@@ -73,6 +109,7 @@ fggl::data::Mesh fggl::data::make_quad_xz() {
 	for (int i = 0; i < 4; ++i){
 		Vertex vert{};
 		vert.posititon = pos[i];
+		vert.normal = glm::vec3(0.0f, 0.0f, 0.0f);
 		vert.colour = fggl::math::vec3(1.0f, 1.0f, 1.0f);
 		colIdx[ i ] = mesh.pushVertex(vert);
 	}
@@ -119,13 +156,17 @@ fggl::data::Mesh fggl::data::make_cube() {
 	for ( int i=0; i < 8; ++i ) {
 		Vertex vert{};
 		vert.posititon = pos[i];
+		vert.normal = glm::vec3(0.0f, 0.0f, 0.0f);
 		vert.colour = fggl::math::vec3(1.0f, 1.0f, 1.0f);
 		colIdx[ i ] = mesh.pushVertex( vert );
 	}
 
+	int tris = 0;
 	for ( auto i : idx ){
 		mesh.pushIndex( colIdx[i] );
+		tris++;
 	}
+	computeNormals( mesh, idx, colIdx, tris );
 
 	return mesh;
 }
@@ -161,13 +202,17 @@ fggl::data::Mesh fggl::data::make_slope() {
 	for ( int i=0; i < 8; ++i ) {
 		Vertex vert{};
 		vert.posititon = pos[i];
+		vert.normal = glm::vec3(0.0f, 0.0f, 0.0f);
 		vert.colour = fggl::math::vec3(1.0f, 1.0f, 1.0f);
 		colIdx[ i ] = mesh.pushVertex( vert );
 	}
 
+	int tris = 0;
 	for ( auto i : idx ){
 		mesh.pushIndex( colIdx[i] );
+		tris++;
 	}
+	computeNormals( mesh, idx, colIdx, tris );
 
 	return mesh;
 }
@@ -205,13 +250,17 @@ fggl::data::Mesh fggl::data::make_ditch() {
 	for ( int i=0; i < 8; ++i ) {
 		Vertex vert{};
 		vert.posititon = pos[i];
+		vert.normal = glm::vec3(0.0f, 0.0f, 0.0f);
 		vert.colour = fggl::math::vec3(1.0f, 1.0f, 1.0f);
 		colIdx[ i ] = mesh.pushVertex( vert );
 	}
 
+	int tris = 0;
 	for ( auto i : idx ){
 		mesh.pushIndex( colIdx[i] );
+		tris++;
 	}
+	computeNormals( mesh, idx, colIdx, tris );
 
 	return mesh;
 }
@@ -244,13 +293,17 @@ fggl::data::Mesh fggl::data::make_point() {
 	for ( int i=0; i < 8; ++i ) {
 		Vertex vert{};
 		vert.posititon = pos[i];
+		vert.normal = glm::vec3(0.0f, 0.0f, 0.0f);
 		vert.colour = fggl::math::vec3(1.0f, 1.0f, 1.0f);
 		colIdx[ i ] = mesh.pushVertex( vert );
 	}
 
+	int tris = 0;
 	for ( auto i : idx ){
 		mesh.pushIndex( colIdx[i] );
+		tris++;
 	}
+	computeNormals( mesh, idx, colIdx, tris );
 
 	return mesh;
 }
diff --git a/fggl/gfx/renderer.cpp b/fggl/gfx/renderer.cpp
index fe3e9d07728ceb7bd3698d0599a5f7a8b6002f37..0a74213660573b296a228593f7eb533aad0fbaf6 100644
--- a/fggl/gfx/renderer.cpp
+++ b/fggl/gfx/renderer.cpp
@@ -74,6 +74,8 @@ void MeshRenderer::render(const Window& window, const fggl::ecs::ECS& ecs, float
     glEnable(GL_CULL_FACE);
     glCullFace(GL_BACK);
 
+    glEnable(GL_DEPTH_TEST);
+
     // make sure the correct rendering context is active
     window.activate();
 
@@ -82,6 +84,7 @@ void MeshRenderer::render(const Window& window, const fggl::ecs::ECS& ecs, float
     glm::mat4 view = glm::lookAt( glm::vec3 ( 0.0f, 3.0f, 3.0f ), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f) );
     glm::mat4 proj = glm::perspective( glm::radians(45.0f), 1280.0f/720.0f, 0.1f, 100.0f);
 
+    glm::vec3 lightPos(20.0f, 20.0f, 15.0f);
 
     // TODO better performance if grouped by vao first
     // TODO the nvidia performance presentation said I shouldn't use uniforms for large data
@@ -99,6 +102,11 @@ void MeshRenderer::render(const Window& window, const fggl::ecs::ECS& ecs, float
 	    glUniformMatrix4fv( glGetUniformLocation(shader, "view"), 1, GL_FALSE, glm::value_ptr( view ) );
 	    glUniformMatrix4fv( glGetUniformLocation(shader, "projection"), 1, GL_FALSE, glm::value_ptr( proj ) );
 
+	    // lighting
+	    GLint lightID = glGetUniformLocation(shader, "lightPos");
+	    if ( lightID != -1 )
+		    glUniform3fv( lightID, 1, glm::value_ptr( lightPos ) );
+
 	    glBindVertexArray( mesh->vao );
 	    glDrawElements( GL_TRIANGLES, mesh->idxSize, GL_UNSIGNED_INT, reinterpret_cast<void*>(mesh->idxOffset) );
     }
diff --git a/fggl/gfx/shader.cpp b/fggl/gfx/shader.cpp
index 7aa478cf1f5d852705943f800a1d48737d3d9f6b..144bfa7cefa884cb9bf3da669822545e7c04a9a1 100644
--- a/fggl/gfx/shader.cpp
+++ b/fggl/gfx/shader.cpp
@@ -110,7 +110,7 @@ GLuint ShaderCache::load(const ShaderConfig& config) {
 	GLuint geomShader;
 	if ( config.hasGeom ) {
 		geomShader = glCreateShader(GL_GEOMETRY_SHADER);
-		compileShader( config.fragment, geomShader );
+		compileShader( config.geometry, geomShader );
 		glAttachShader(pid, geomShader);
 	}