diff --git a/demo/main.cpp b/demo/main.cpp
index db2e3c73c20fb23518b2639941b051d50d096395..e14e2d5175f4901e4fcc9e895ee9d2f1a8575ff0 100644
--- a/demo/main.cpp
+++ b/demo/main.cpp
@@ -133,6 +133,20 @@ public:
             cameraKeys->rotate_ccw = glfwGetKeyScancode(GLFW_KEY_E);
         }
 
+        {
+            fggl::ecs3::entity_t terrain = m_world.create(false);
+            m_world.add(terrain, types.find(fggl::math::Transform::name));
+
+            auto camTf = m_world.get<fggl::math::Transform>(terrain);
+            camTf->origin( glm::vec3(0.0f, 0.0f, 3.0f) );
+
+            //auto terrainData = m_world.get<fggl::data::HeightMap>(terrain);
+            fggl::data::HeightMap terrainData{};
+            terrainData.clear();
+            terrainData.heightValues[50 * 255 + 50] = 50.0f;
+            m_world.set<fggl::data::HeightMap>(terrain, &terrainData);
+        }
+
         // create building prototype
         fggl::ecs3::entity_t bunker;
         {
@@ -146,7 +160,7 @@ public:
 
             fggl::data::Mesh mesh;
             for (int j=-(nSections/2); j<=nSections/2; j++) {
-                const auto shapeOffset = glm::vec3( 0.0f, 0.0f, (float)j * 1.0f );
+                const auto shapeOffset = glm::vec3( 0.0f, 0.5f, (float)j * 1.0f );
 
                 const auto cubeMat = glm::translate( fggl::math::mat4( 1.0f ) , shapeOffset );
                 const auto leftSlope = fggl::math::modelMatrix(
@@ -170,7 +184,7 @@ public:
         for ( int i=0; i<nCubes; i++ ) {
             auto bunkerClone = m_world.copy(bunker);
             auto result = m_world.get<fggl::math::Transform>(bunkerClone);
-            result->origin( glm::vec3( (float)i * 5.0f, 0.0f, 0.0f) );
+            result->origin( glm::vec3( (float)i * 5.0f + 1.0f, 0.0f, 0.0f) );
         }
     }
 
diff --git a/fggl/CMakeLists.txt b/fggl/CMakeLists.txt
index 19de7ba3c24a5bd9d5b4ea8fff119608278d96b0..00b4c99b34b109ac41f89dbf51caf3a22624ef95 100644
--- a/fggl/CMakeLists.txt
+++ b/fggl/CMakeLists.txt
@@ -4,12 +4,13 @@ add_library(fggl fggl.cpp
 	ecs/ecs.cpp
 	data/model.cpp
 	data/procedural.cpp
+	data/heightmap.cpp
 	ecs3/fast/Container.cpp
 	ecs3/prototype/world.cpp
 	scenes/Scene.cpp
 	ecs3/module/module.cpp
-	input/camera_input.cpp
-)
+	input/camera_input.cpp data/heightmap.cpp)
+
 target_include_directories(fggl PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../)
 
 # Graphics backend
diff --git a/fggl/data/heightmap.cpp b/fggl/data/heightmap.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1009cb677539ef392bcaaf9b3a8b80bfa2233826
--- /dev/null
+++ b/fggl/data/heightmap.cpp
@@ -0,0 +1,107 @@
+//
+// Created by webpigeon on 20/11/2021.
+//
+
+#include <fggl/data/model.hpp>
+#include <fggl/data/heightmap.h>
+
+// adapted from  https://www.mbsoftworks.sk/tutorials/opengl4/016-heightmap-pt1-random-terrain/
+
+namespace fggl::data {
+
+    void gridVertexNormals(data::Vertex *locations) {
+        int sizeX = data::heightMaxX;
+        int sizeY = data::heightMaxZ;
+        const int gridOffset = sizeX * sizeY;
+
+        // calculate normals for each triangle
+        math::vec3 triNormals[sizeX * sizeY * 2];
+        for (int i = 0; i < sizeX - 1; i++) {
+            for (int j = 0; j < sizeY - 1; j++) {
+                // calculate vertex
+                const auto &a = locations[i * sizeY + j].posititon;
+                const auto &b = locations[(i + 1) * sizeY + j].posititon;
+                const auto &c = locations[i * sizeY + (j + 1)].posititon;
+                const auto &d = locations[(i + 1) * sizeY + (j + 1)].posititon;
+
+                const auto normalA = glm::cross(b - a, a - d);
+                const auto normalB = glm::cross(d - c, c - b);
+
+                // store the normals
+                int idx1 = idx(i, j, sizeY);
+                int idx2 = idx1 + gridOffset;
+                triNormals[idx1] = glm::normalize(normalA);
+                triNormals[idx2] = glm::normalize(normalB);
+            }
+        }
+
+        // calculate normals for each vertex
+        for (int i = 0; i < sizeX; i++) {
+            for (int j = 0; j < sizeY; j++) {
+                const auto firstRow = (i == 0);
+                const auto firstCol = (j == 0);
+                const auto lastRow = (i == sizeX - 1);
+                const auto lastCol = (i == sizeY - 1);
+
+                auto finalNormal = glm::vec3(0.0f, 0.0f, 0.0f);
+
+                if (!firstRow && !firstCol) {
+                    finalNormal += triNormals[idx(i - 1, j - 1, sizeY)];
+                }
+
+                if (!lastRow && !lastCol) {
+                    finalNormal += triNormals[idx(i, j, sizeY)];
+                }
+
+                if (!firstRow && lastCol) {
+                    finalNormal += triNormals[idx(i - 1, j, sizeY)];
+                    finalNormal += triNormals[idx(i - 1, j, sizeY) + gridOffset];
+                }
+
+                if (!lastRow && !firstCol) {
+                    finalNormal += triNormals[idx(i, j - 1, sizeY)];
+                    finalNormal += triNormals[idx(i, j - 1, sizeY) + gridOffset];
+                }
+
+                locations[idx(i, j, sizeY)].normal = glm::normalize(finalNormal) * -1.0f; //FIXME the normals seem wrong.
+            }
+        }
+    }
+
+    void generateHeightMesh(const data::HeightMap *heights, data::Mesh &mesh) {
+
+        // step 1: convert height data into vertex locations
+        data::Vertex locations[data::heightMaxZ * data::heightMaxZ];
+        for (std::size_t x = 0; x < data::heightMaxX; x++) {
+            for (std::size_t z = 0; z < data::heightMaxZ; z++) {
+                float level = heights->getValue(x, z);
+                auto xPos = float(x);
+                auto zPos = float(z);
+
+                std::size_t idx1 = idx(x, z, data::heightMaxZ);
+                locations[idx1].colour = fggl::math::vec3(1.0f, 1.0f, 1.0f);
+                locations[idx1].posititon = math::vec3(-0.5f + xPos, level, -0.5f - zPos);
+            }
+        }
+        gridVertexNormals(locations);
+
+        mesh.restartVertex = data::heightMaxZ * data::heightMaxX;
+
+        // populate mesh
+        for (auto & location : locations) {
+            mesh.pushVertex(location);
+        }
+
+        for (std::size_t x = 0; x < data::heightMaxX - 1; x++) {
+            for (std::size_t z = 0; z < data::heightMaxZ; z++) {
+                for (int k=0; k < 2; k++) {
+                    int idx = (x+k) * data::heightMaxZ + z;
+                    mesh.pushIndex(idx);
+                }
+            }
+            mesh.pushIndex(mesh.restartVertex);
+        }
+
+    }
+
+}
diff --git a/fggl/data/heightmap.h b/fggl/data/heightmap.h
new file mode 100644
index 0000000000000000000000000000000000000000..48908bde83b4c8876acf134313ab45f2157f4798
--- /dev/null
+++ b/fggl/data/heightmap.h
@@ -0,0 +1,44 @@
+//
+// Created by webpigeon on 20/11/2021.
+//
+
+#ifndef FGGL_HEIGHTMAP_H
+#define FGGL_HEIGHTMAP_H
+
+#include <cstdint>
+
+namespace fggl::data {
+
+    constexpr std::size_t heightMaxX = 255;
+    constexpr std::size_t heightMaxZ = 255;
+    constexpr float heightSeaLevel = 0.0f;
+
+    struct HeightMap {
+        constexpr static const char name[] = "Heightmap";
+        float heightValues[heightMaxX * heightMaxZ];
+
+        void clear() {
+            for (float & heightValue : heightValues){
+                heightValue = heightSeaLevel;
+            }
+        }
+
+        [[nodiscard]]
+        inline float getValue(std::size_t x, std::size_t z) const {
+            return heightValues[x * heightMaxZ + z];
+        }
+
+        inline void setValue(std::size_t x, std::size_t z, float value) {
+            heightValues[x * heightMaxZ + z] = value;
+        }
+    };
+
+
+    inline int idx(int x, int z, int zMax) {
+        return x * zMax + z;
+    }
+
+    void generateHeightMesh(const data::HeightMap* heights, data::Mesh &mesh);
+}
+
+#endif //FGGL_HEIGHTMAP_H
diff --git a/fggl/data/model.cpp b/fggl/data/model.cpp
index 0cded14df73ec6693b4ae7664089e48934c20ada..6c0a08abc069e3b2a92525af32ed4274b41692a0 100644
--- a/fggl/data/model.cpp
+++ b/fggl/data/model.cpp
@@ -9,7 +9,7 @@ Mesh::Mesh() : m_verts(0), m_index(0) {
 }
 
 void Mesh::pushIndex(unsigned int idx) {
-	assert( idx < m_verts.size() );
+	assert( idx < m_verts.size() || idx == this->restartVertex );
 	m_index.push_back(idx);
 }
 
diff --git a/fggl/data/model.hpp b/fggl/data/model.hpp
index 2f57d00b9def77166f3a3cd60ad8ca71770d6a18..bc90ed10d0c934fcba8c89bc3a45c96876e807cd 100644
--- a/fggl/data/model.hpp
+++ b/fggl/data/model.hpp
@@ -43,10 +43,12 @@ namespace fggl::data {
 			 */
 			inline void push(Vertex vert) {
 				auto idx = indexOf(vert);
-				if ( idx == -1 )
-					pushVertex(vert);
-				else
-					pushIndex(idx);
+				if ( idx == -1 ) {
+                    idx = pushVertex(vert);
+                    pushIndex(idx);
+                } else {
+                    pushIndex(idx);
+                }
 			}
 
 			/**
@@ -97,6 +99,7 @@ namespace fggl::data {
 			inline Vertex& vertex(int idx) {
 				return m_verts[idx];
 			}
+            unsigned int restartVertex;
 
 		private:
 			std::vector<Vertex> m_verts;
diff --git a/fggl/gfx/ogl/compat.hpp b/fggl/gfx/ogl/compat.hpp
index 9c792ebac623b81355ff164e1bb9f9f85a58373e..6108dadf6a4e102e08f17567a920ccf14e3ae587 100644
--- a/fggl/gfx/ogl/compat.hpp
+++ b/fggl/gfx/ogl/compat.hpp
@@ -20,9 +20,12 @@
 #include <fggl/ecs/ecs.hpp>
 #include <utility>
 #include <fggl/input/camera_input.h>
+#include <fggl/data/heightmap.h>
 
 namespace fggl::gfx {
 
+    void generateHeightMesh(data::HeightMap* heightMap, data::Mesh);
+
 	//
 	// fake module support - allows us to still RAII
 	//
@@ -48,9 +51,23 @@ namespace fggl::gfx {
             world->set<fggl::gfx::GlRenderToken>(entity, &glMesh);
         }
 
+        void uploadHeightmap(ecs3::World *world, ecs::entity_t entity) {
+            const auto heightmap = world->get<data::HeightMap>(entity);
+
+            data::Mesh tmpMesh{};
+            data::generateHeightMesh(heightmap, tmpMesh);
+            auto glMesh = renderer.upload( tmpMesh );
+
+            auto pipeline = cache.get("phong");
+            glMesh.pipeline = pipeline;
+            glMesh.renderType = GlRenderType::trinagle_strip;
+            world->set<fggl::gfx::GlRenderToken>(entity, &glMesh);
+        }
+
         void onLoad(ecs3::ModuleManager& manager, ecs3::TypeRegistry& types) override {
             // TODO implement dependencies
             types.make<fggl::gfx::StaticMesh>();
+            types.make<fggl::data::HeightMap>();
             types.make<fggl::gfx::Camera>();
 
             // FIXME probably shouldn't be doing this...
@@ -62,6 +79,7 @@ namespace fggl::gfx {
             // callbacks
             auto upload_cb = [this](auto a, auto b) { this->uploadMesh(a, b); };
             manager.onAdd<fggl::gfx::StaticMesh>( upload_cb );
+            manager.onAdd<fggl::data::HeightMap>( [this](auto a, auto b) { this->uploadHeightmap(a, b); });
         }
 
 	};
@@ -110,11 +128,7 @@ namespace fggl::gfx {
 		auto camera = cameras[0];
 
 		// get the models
-		auto renderables = ecs.findMatching<fggl::gfx::GlRenderToken>();
-
-		for ( auto renderable : renderables ) {
-			mod->renderer.render(ecs, camera, dt);
-		}
+        mod->renderer.render(ecs, camera, dt);
 	}
 
 }
diff --git a/fggl/gfx/ogl/renderer.cpp b/fggl/gfx/ogl/renderer.cpp
index eff7e8e9cdf06b7a1712c54a0d9d6f2dd2175003..1132df950a168e7bdfc684afd629a062587dc5fd 100644
--- a/fggl/gfx/ogl/renderer.cpp
+++ b/fggl/gfx/ogl/renderer.cpp
@@ -47,6 +47,7 @@ static GLuint createIndexBuffer(std::vector<uint32_t>& indexData) {
 	return buffId;
 }
 
+
 GlRenderToken MeshRenderer::upload(fggl::data::Mesh& mesh) {
 	GlRenderToken token{};
 	glGenVertexArrays(1, &token.vao);
@@ -61,6 +62,7 @@ GlRenderToken MeshRenderer::upload(fggl::data::Mesh& mesh) {
 	token.buffs[1] = createIndexBuffer( mesh.indexList() );
 	token.idxOffset = 0;
 	token.idxSize = mesh.indexCount();
+    token.restartVertex = mesh.restartVertex;
 	glBindVertexArray(0);
 
 	return token;
@@ -116,10 +118,18 @@ void MeshRenderer::render(fggl::ecs3::World& ecs, ecs3::entity_t camera, float d
 		    glUniform3fv( lightID, 1, glm::value_ptr( lightPos ) );
 
 	    glBindVertexArray( mesh->vao );
-	    glDrawElements( GL_TRIANGLES, mesh->idxSize, GL_UNSIGNED_INT, reinterpret_cast<void*>(mesh->idxOffset) );
+
+        if ( mesh->renderType == GlRenderType::trinagle_strip) {
+            glEnable(GL_PRIMITIVE_RESTART);
+            glPrimitiveRestartIndex(mesh->restartVertex);
+        }
+
+        glDrawElements( mesh->renderType, mesh->idxSize, GL_UNSIGNED_INT, reinterpret_cast<void*>(mesh->idxOffset) );
+        if ( mesh->renderType == GlRenderType::trinagle_strip) {
+            glDisable(GL_PRIMITIVE_RESTART);
+        }
     }
 
     glBindVertexArray(0);
-
 }
 
diff --git a/fggl/gfx/ogl/renderer.hpp b/fggl/gfx/ogl/renderer.hpp
index b13647f27a844bc007f27776d809f322843b137f..2e900cb4df7297d2f1444ed20c5b986d7d942164 100644
--- a/fggl/gfx/ogl/renderer.hpp
+++ b/fggl/gfx/ogl/renderer.hpp
@@ -9,6 +9,10 @@
 
 namespace fggl::gfx {
 
+    enum GlRenderType {
+        triangles = GL_TRIANGLES, trinagle_strip = GL_TRIANGLE_STRIP
+    };
+
 	struct GlRenderToken {
         constexpr static const char name[] = "RenderToken";
 		GLuint vao;
@@ -16,6 +20,8 @@ namespace fggl::gfx {
 		GLuint idxOffset;
 		GLuint idxSize;
 		GLuint pipeline;
+        GLuint restartVertex;
+        GlRenderType renderType = triangles;
 	};
 
 	class GlMeshRenderer {